From 0cd2ff8de653cececacb3577c39dbc9471bb551f Mon Sep 17 00:00:00 2001 From: hcj <1224667075@qq.com> Date: Tue, 25 Nov 2025 16:15:21 +0800 Subject: [PATCH] =?utf8?q?=E5=A2=9E=E5=8A=A0=E4=BD=BF=E7=94=A8=E6=A8=A1?= =?utf8?q?=E7=BB=84=E4=B8=AD=E8=BD=AC=E4=B8=8B=E8=BD=BD=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- cat1modem/src/LeiNiao/httpfiledownload.py | 514 +++++++++++++++++ cat1modem/src/LeiNiao/main_init.py | 5 + code/sdk/zhihui/4g/cat1Service/CMakeLists.txt | 2 + .../Service/http_file_download_client.c | 528 ++++++++++++++++++ .../include/http_file_download_client.h | 66 +++ code/sdk/zhihui/4gTest/cat1_at_cmd.c | 66 +++ 6 files changed, 1181 insertions(+) create mode 100644 cat1modem/src/LeiNiao/httpfiledownload.py create mode 100644 code/sdk/zhihui/4g/cat1Service/Service/http_file_download_client.c create mode 100644 code/sdk/zhihui/4g/cat1Service/include/http_file_download_client.h diff --git a/cat1modem/src/LeiNiao/httpfiledownload.py b/cat1modem/src/LeiNiao/httpfiledownload.py new file mode 100644 index 000000000..67a52d6a0 --- /dev/null +++ b/cat1modem/src/LeiNiao/httpfiledownload.py @@ -0,0 +1,514 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +HTTP 文件下载模块 +支持下载指定 URL 的文件到本地路径 +""" + +import log + +# 设置日志输出级别 +log.basicConfig(level=log.INFO) +clog = log.getLogger("httpdownload") + +import modem +import request +import utime +import osTimer +import ql_fs +from machine import RTC +from misc import Power +import uos +import ujson +import urandom +from usr.EventMesh import eventMesh +import usocket +import _thread + +REQUESTS_AVAILABLE = True + + +class HttpFileDownloader: + """ + HTTP 文件下载类 + 支持从指定 URL 下载文件并保存到本地 + """ + + def __init__(self, timeout=30, verify=True, allow_redirects=True, sizeof=255): + """ + 初始化下载器 + + Args: + timeout: 请求超时时间(秒),默认 30 秒 + verify: 是否验证 SSL 证书,默认 True + allow_redirects: 是否允许重定向,默认 True + sizeof: 读取缓冲区的数据块大小,默认 255 字节,建议 255-4096 + """ + self.timeout = timeout + self.verify = verify + self.allow_redirects = allow_redirects + self.sizeof = sizeof + + def download(self, url, save_path=None, chunk_size=None, show_progress=False): + """ + 下载文件从指定 URL 到本地路径 + + Args: + url: 要下载的文件 URL + save_path: 保存文件的本地路径,为 None 或空字符串时不保存文件(仅下载并丢弃数据) + chunk_size: 下载时的块大小(字节),默认使用 self.sizeof + show_progress: 是否显示下载进度,默认 False + + Returns: + tuple: (success: bool, download_time: float, file_size: int) + success: 下载是否成功 + download_time: 下载耗时(秒) + file_size: 下载的文件大小(字节),失败时为 -1 + """ + if not REQUESTS_AVAILABLE: + clog.error("Error: request module is not available") + return False, 0.0, -1 + + if not url: + clog.error("Error: URL cannot be empty") + return False, 0.0, -1 + + # 判断是否需要保存文件 + save_file = False + if save_path != None: + save_file = save_path and save_path.strip() + + # 使用指定的 chunk_size 或默认的 sizeof + read_size = chunk_size if chunk_size else self.sizeof + + try: + if save_file: + clog.info("Starting download: {}".format(url)) + clog.info("Save to: {}".format(save_path)) + else: + clog.info("Starting download (discard mode): {}".format(url)) + + # 记录开始时间 + start_time = utime.time() + + # 发送 GET 请求 + # request.get(url, data=None, **kwargs) + # sizeof: 读取缓冲区的数据块大小 + response = request.get( + url, + sizeof=read_size, + decode=False # 不进行 UTF-8 解码,直接获取 bytes + ) + + # 检查响应状态码 + if response.status_code != 200: + clog.error("Error: HTTP error {}: {}".format(response.status_code, url)) + return False, 0.0, -1 + + # 获取文件总大小(如果服务器提供了 Content-Length) + total_size = 0 + if 'Content-Length' in response.headers: + try: + total_size = int(response.headers['Content-Length']) + except (ValueError, TypeError): + total_size = 0 + + # 下载数据 + downloaded_size = 0 + + if save_file: + # 保存到文件 + with open(save_path, 'wb') as f: + # response.content 是 bytes 类型生成器 + for chunk in response.content: + if chunk: # 过滤掉空块 + f.write(chunk) + downloaded_size += len(chunk) + + # 显示进度 + if show_progress and total_size > 0: + progress = (downloaded_size / total_size) * 100 + clog.info("Progress: {:.2f}% ({}/{}) bytes".format(progress, downloaded_size, total_size)) + else: + # 丢弃数据(不保存到文件) + # response.content 是 bytes 类型生成器 + for chunk in response.content: + if chunk: # 过滤掉空块 + downloaded_size += len(chunk) + + # 显示进度 + if show_progress and total_size > 0: + progress = (downloaded_size / total_size) * 100 + clog.info("Progress: {:.2f}% ({}/{}) bytes".format(progress, downloaded_size, total_size)) + + # 记录结束时间并计算耗时 + end_time = utime.time() + download_time = end_time - start_time + + file_size = downloaded_size + + if save_file: + clog.info("Download completed: {} ({} bytes, {:.3f}s)".format(save_path, file_size, download_time)) + else: + clog.info("Download completed (discarded): {} bytes downloaded in {:.3f}s".format(file_size, download_time)) + + return True, download_time, file_size + + except Exception as e: + clog.error("Error: Download failed: {}".format(e)) + return False, 0.0, -1 + + def download_simple(self, url, save_path=None): + """ + 简单的下载方法(兼容原有实现) + + Args: + url: 要下载的文件 URL + save_path: 保存文件的本地路径,为 None 或空字符串时不保存文件 + + Returns: + tuple: (success: bool, download_time: float, file_size: int) + """ + return self.download(url, save_path, show_progress=False) + + def get_file_size(self, url): + """ + 获取远程文件大小(不下载文件) + + Args: + url: 文件的 URL + + Returns: + int: 文件大小(字节),失败返回 -1 + """ + if not REQUESTS_AVAILABLE: + return -1 + + try: + # 使用 GET 请求,但只读取 headers + response = request.get(url, sizeof=1, decode=False) + + if response.status_code != 200: + clog.error("Error: HTTP error {} when getting file size".format(response.status_code)) + return -1 + + # 从响应头获取 Content-Length + if 'Content-Length' in response.headers: + try: + return int(response.headers['Content-Length']) + except (ValueError, TypeError): + return -1 + + return -1 + + except Exception as e: + clog.error("Error getting file size: {}".format(e)) + return -1 + + def close(self): + """关闭下载器会话(request 模块不需要关闭会话)""" + pass + + def __enter__(self): + """上下文管理器入口""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """上下文管理器出口""" + self.close() + + +class HttpFileDownloadTcpServer: + """ + HTTP 文件下载 TCP 服务器 + 监听 TCP 连接,接收 URL 请求,下载文件并通过 socket 发送数据 + """ + + def __init__(self, host='172.16.1.2', port=5002, timeout=30, sizeof=255): + """ + 初始化 TCP 服务器 + + Args: + host: 监听地址,默认 '172.16.1.2' + port: 监听端口,默认 5002 + timeout: 下载超时时间(秒),默认 30 秒 + sizeof: 读取缓冲区的数据块大小,默认 255 字节 + """ + self.host = host + self.port = port + self.timeout = timeout + self.sizeof = sizeof + self.sock = None + self.running = False + self.downloader = HttpFileDownloader(timeout=timeout, sizeof=sizeof) + + def __socket_init(self): + """初始化 TCP socket""" + try: + sock = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM, usocket.IPPROTO_TCP_SER) + sock.setsockopt(usocket.SOL_SOCKET, usocket.SO_REUSEADDR, 1) + bind_addr = (self.host, self.port) + sock.bind(bind_addr) + sock.listen(10) + clog.info("TCP server listening on {}:{}".format(self.host, self.port)) + return sock + except Exception as e: + clog.error("Failed to create TCP socket: {}".format(e)) + return None + + def __handle_client(self, conn, addr): + """ + 处理客户端连接 + + Args: + conn: 客户端连接对象 + addr: 客户端地址 (ip, port) + """ + try: + clog.info("Client connected: {}:{}".format(addr[0], addr[1])) + + # 接收 URL 请求(最大 512 字节) + data = conn.recv(512) + if not data: + clog.info("Client disconnected without sending data") + conn.close() + return + + # 解析 URL(可能是字符串或 JSON) + try: + url_str = data.decode('utf-8').strip() + # 尝试解析为 JSON + try: + url_data = ujson.loads(url_str) + url = url_data.get('url', url_str) + except: + url = url_str + except: + url = str(data) + + if not url: + clog.error("Invalid URL received") + conn.send(b'ERROR: Invalid URL') + conn.close() + return + + clog.info("Received download request for URL: {}".format(url)) + + # 发送确认消息 + conn.send(b'OK: Starting download...\n') + + # 下载文件并通过 socket 发送 + success, download_time, file_size = self.__download_and_send(conn, url) + + if success: + clog.info("File sent successfully: {} bytes in {:.3f}s".format(file_size, download_time)) + conn.send(b'\nOK: Download completed\n') + else: + clog.error("Download failed") + conn.send(b'\nERROR: Download failed\n') + + except Exception as e: + clog.error("Error handling client: {}".format(e)) + finally: + try: + conn.close() + except: + pass + clog.info("Client connection closed: {}:{}".format(addr[0], addr[1])) + + def __download_and_send(self, conn, url): + """ + 下载文件并通过 socket 发送数据 + + Args: + conn: 客户端连接对象 + url: 要下载的文件 URL + + Returns: + tuple: (success: bool, download_time: float, file_size: int) + """ + try: + # 记录开始时间 + start_time = utime.time() + + # 发送 GET 请求 + response = request.get( + url, + sizeof=self.sizeof, + decode=False # 不进行 UTF-8 解码,直接获取 bytes + ) + + # 检查响应状态码 + if response.status_code != 200: + clog.error("Error: HTTP error {}: {}".format(response.status_code, url)) + return False, 0.0, -1 + + # 获取文件总大小(如果服务器提供了 Content-Length) + total_size = 0 + if 'Content-Length' in response.headers: + try: + total_size = int(response.headers['Content-Length']) + except (ValueError, TypeError): + total_size = 0 + + # 发送文件大小信息(可选) + if total_size > 0: + size_info = "Content-Length: {}\n".format(total_size).encode('utf-8') + conn.send(size_info) + + # 下载数据并通过 socket 发送 + downloaded_size = 0 + + # response.content 是 bytes 类型生成器 + for chunk in response.content: + if chunk: # 过滤掉空块 + try: + conn.send(chunk) + downloaded_size += len(chunk) + + # 显示进度(每发送一定数据量后打印一次) + if total_size > 0: + progress = (downloaded_size / total_size) * 100 + # 每发送 64KB 打印一次进度 + if downloaded_size % (64 * 1024) < self.sizeof: + clog.info("Progress: {:.2f}% ({}/{}) bytes".format( + progress, downloaded_size, total_size)) + except Exception as e: + clog.error("Error sending data: {}".format(e)) + return False, 0.0, -1 + + # 记录结束时间并计算耗时 + end_time = utime.time() + download_time = end_time - start_time + + clog.info("Download and send completed: {} bytes in {:.3f}s".format( + downloaded_size, download_time)) + + return True, download_time, downloaded_size + + except Exception as e: + clog.error("Error downloading and sending file: {}".format(e)) + return False, 0.0, -1 + + def __server_loop(self): + """服务器主循环""" + while self.running: + try: + if self.sock is None: + utime.sleep_ms(2000) + continue + + # 接受客户端连接 + cli_conn, cli_ip_addr, cli_port = self.sock.accept() + clog.info("Accepted connection from {}:{}".format(cli_ip_addr, cli_port)) + + # 在新线程中处理客户端连接 + _thread.start_new_thread(self.__handle_client, (cli_conn, (cli_ip_addr, cli_port))) + + except Exception as e: + if self.running: + clog.error("Error in server loop: {}".format(e)) + utime.sleep_ms(1000) + + def start(self): + """启动 TCP 服务器""" + if self.running: + clog.warning("Server is already running") + return False + + self.sock = self.__socket_init() + if self.sock is None: + return False + + self.running = True + # 启动服务器线程 + _thread.start_new_thread(self.__server_loop, ()) + clog.info("HTTP file download TCP server started on {}:{}".format(self.host, self.port)) + return True + + def stop(self): + """停止 TCP 服务器""" + self.running = False + if self.sock: + try: + self.sock.close() + except: + pass + self.sock = None + clog.info("HTTP file download TCP server stopped") + + def is_running(self): + """检查服务器是否在运行""" + return self.running + + +# 保持原有功能的简单函数(向后兼容) + +HttpFileDownloadTcpServer().start() +# 示例用法 +# if __name__ == '__main__': +# # 方式1: 使用类下载并保存文件 +# downloader = HttpFileDownloader(timeout=30) + +# # 下载文件并显示进度 +# success, download_time, file_size = downloader.download( +# url='http://www.baidu.com', +# save_path='/tmp/baidu.html', +# show_progress=True +# ) + +# if success: +# print(f"Download successful! Size: {file_size} bytes, Time: {download_time:.3f}s") +# else: +# print("Download failed!") + +# downloader.close() + +# # 方式2: 下载但不保存文件(仅测试下载速度) +# downloader = HttpFileDownloader(timeout=30) +# success, download_time, file_size = downloader.download( +# url='http://www.baidu.com', +# save_path=None, # 不保存文件,直接丢弃数据 +# show_progress=True +# ) + +# if success: +# print(f"Download test completed! Size: {file_size} bytes, Time: {download_time:.3f}s, Speed: {file_size/download_time/1024:.2f} KB/s") + +# downloader.close() + +# # 方式3: 使用上下文管理器(推荐) +# with HttpFileDownloader() as downloader: +# success, download_time, file_size = downloader.download( +# url='http://www.baidu.com', +# save_path='/tmp/baidu2.html', +# show_progress=True +# ) +# if success: +# print(f"Downloaded {file_size} bytes in {download_time:.3f}s") + +# # 方式4: 使用简单函数(向后兼容) +# download_file('http://www.baidu.com', '/tmp/baidu3.html') + + +# 测试代码示例(直接下载) +# downloader = HttpFileDownloader(timeout=30, sizeof=255) +# success, download_time, file_size = downloader.download( +# url='https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Amber/WorkAndStudy.mp4', +# save_path=None, # 不保存文件,直接丢弃数据 +# show_progress=True +# ) +# +# if success: +# clog.info("Download test completed! Size: {} bytes, Time: {:.3f}s, Speed: {:.2f} KB/s".format( +# file_size, download_time, file_size/download_time/1024)) +# +# downloader.close() + +# TCP 服务器使用示例 +# 创建并启动 TCP 服务器,监听客户端连接并转发下载的文件 +# tcp_server = HttpFileDownloadTcpServer(host='172.16.1.2', port=5002, timeout=30, sizeof=255) +# tcp_server.start() +# +# # 服务器会在后台运行,等待客户端连接 +# # 客户端可以通过 TCP 连接发送 URL,服务器会下载文件并通过 socket 发送数据 \ No newline at end of file diff --git a/cat1modem/src/LeiNiao/main_init.py b/cat1modem/src/LeiNiao/main_init.py index 4eb63593f..065fe3bd3 100644 --- a/cat1modem/src/LeiNiao/main_init.py +++ b/cat1modem/src/LeiNiao/main_init.py @@ -33,6 +33,11 @@ try: print("检测文件成功!!!!!!!\r\n",'cat1service') except Exception as e: print("检测文件失败!!!!!!!{}\r\n",'cat1service') + try: + from usr.httpfiledownload import HttpFileDownloadTcpServer + print("检测文件成功!!!!!!!\r\n",'httpfiledownload') + except Exception as e: + print("检测文件失败!!!!!!!{}\r\n",'httpfiledownload') ota_flag = 1 print("检测文件成功!!!!!!!\r\n") diff --git a/code/sdk/zhihui/4g/cat1Service/CMakeLists.txt b/code/sdk/zhihui/4g/cat1Service/CMakeLists.txt index 691a92a79..1cca67493 100644 --- a/code/sdk/zhihui/4g/cat1Service/CMakeLists.txt +++ b/code/sdk/zhihui/4g/cat1Service/CMakeLists.txt @@ -49,6 +49,8 @@ ${CMAKE_CURRENT_SOURCE_DIR}/Service/api_shell.c ${CMAKE_CURRENT_SOURCE_DIR}/Service/http_client.c ${CMAKE_CURRENT_SOURCE_DIR}/Service/sntp.c ${CMAKE_CURRENT_SOURCE_DIR}/Service/cat1_server_hal.c +${CMAKE_CURRENT_SOURCE_DIR}/Service/http_file_download_client.c + # ${CMAKE_CURRENT_SOURCE_DIR}/slip/uart_slip.c ) diff --git a/code/sdk/zhihui/4g/cat1Service/Service/http_file_download_client.c b/code/sdk/zhihui/4g/cat1Service/Service/http_file_download_client.c new file mode 100644 index 000000000..502d06607 --- /dev/null +++ b/code/sdk/zhihui/4g/cat1Service/Service/http_file_download_client.c @@ -0,0 +1,528 @@ +/* HTTP File Download Client + * + * This file implements a TCP client to connect to the HTTP file download server + * and receive downloaded files. + * + * Reference: cat1_tcpServer.c + */ + +#include +#include "lwip/ip.h" +#include "lwip/netif.h" +#include "lwip/init.h" +#include "lwip/inet_chksum.h" + +#include "lwip/err.h" +#include "lwip/sockets.h" +#include "lwip/sys.h" +#include +#include "lwip/inet.h" + +/* Define htons function if not available */ +#ifndef htons +static inline u16_t htons(u16_t n) +{ + return ((n & 0xFF) << 8) | ((n & 0xFF00) >> 8); +} +#endif + +#include +#include +#include +#include + +#include "cat1_log.h" +#include "osal_timer.h" + +#define __DEFINED_struct_timeval +#include "cat1_runtime.h" +#include "cat1_utils.h" + +#include "errcode.h" +#include "errno.h" +#include "cmsis_os.h" +#include "osal_msgqueue.h" +#include "thread_init.h" +#include "soc_osal.h" +#include "common_def.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ +// 172.16.1.2 +/* Default server configuration */ +#define HTTP_DOWNLOAD_SERVER_IP "172.16.1.2" +#define HTTP_DOWNLOAD_SERVER_PORT 5002 + +#define LOACL_SERVER_IP "172.16.1.5" +#define LOACL_PORT 1002 + +/* Buffer sizes */ +#define MAX_URL_LEN 512 +#define MAX_RECV_BUFFER_SIZE 4096 +#define MAX_RESPONSE_BUFFER_SIZE 256 + +/* Timeout settings */ +#define CONNECT_TIMEOUT_MS 5000 +#define RECV_TIMEOUT_MS 30000 + +static int g_client_sock = -1; +static uint8_t g_client_connected = 0; +static uint8_t g_download_task_running = 0; + +/* Task configuration */ +#define HTTP_DOWNLOAD_TASK_SIZE (0x800 * 5) // 20KB stack +#define HTTP_DOWNLOAD_TASK_PRIO 5 + +/* Download task parameters structure */ +typedef struct { + char url[MAX_URL_LEN]; + char save_path[256]; +} http_download_task_params_t; + +/* Static parameters for async download task */ +static http_download_task_params_t s_task_params; + +#ifndef UNUSED +#define UNUSED(x) ((void)(x)) +#endif + +#define cat1_msleep(ms) osDelay(ms) /* osDelay(1) : 1ms */ + +/** + * Initialize TCP client socket + * + * @return socket file descriptor on success, -1 on failure + */ +static int http_download_client_socket_init(void) +{ + int sock = -1; + int addr_family = AF_INET; + int ip_protocol = IPPROTO_IP; + struct sockaddr_in server_addr; + char addr_str[128]; + int opt = 1; + + // Create TCP socket + sock = socket(addr_family, SOCK_STREAM, ip_protocol); + if (sock < 0) { + cat1_print_err("Unable to create socket: errno %d\n", errno); + return -1; + } + + // Set socket options + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + + // Set receive timeout + struct timeval timeout; + timeout.tv_sec = RECV_TIMEOUT_MS / 1000; + timeout.tv_usec = (RECV_TIMEOUT_MS % 1000) * 1000; + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + // Configure server address + server_addr.sin_addr.s_addr = inet_addr(LOACL_SERVER_IP); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(LOACL_PORT); + + inet_ntoa_r(server_addr.sin_addr, addr_str, sizeof(addr_str) - 1); + cat1_print_info("Connecting to server %s:%d\n", addr_str, LOACL_PORT); + + return sock; +} + +/** + * Connect to the HTTP download server + * + * @return 0 on success, -1 on failure + */ +static int http_download_client_connect(int sock) +{ + struct sockaddr_in server_addr; + + server_addr.sin_addr.s_addr = inet_addr(HTTP_DOWNLOAD_SERVER_IP); + server_addr.sin_family = AF_INET; + server_addr.sin_port = htons(HTTP_DOWNLOAD_SERVER_PORT); + + int ret = connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)); + if (ret < 0) { + cat1_print_err("Unable to connect: errno %d\n", errno); + return -1; + } + + cat1_print_info("Connected to server successfully\n"); + return 0; +} + +/** + * Send URL request to server + * + * @param sock socket file descriptor + * @param url URL to download + * @return 0 on success, -1 on failure + */ +static int http_download_client_send_url(int sock, const char *url) +{ + if (!url || strlen(url) == 0) { + cat1_print_err("Invalid URL\n"); + return -1; + } + + if (strlen(url) >= MAX_URL_LEN) { + cat1_print_err("URL too long (max %d)\n", MAX_URL_LEN - 1); + return -1; + } + + cat1_print_info("Sending URL: %s\n", url); + + int ret = send(sock, url, strlen(url), 0); + if (ret < 0) { + cat1_print_err("Send failed: errno %d\n", errno); + return -1; + } + + cat1_print_info("URL sent successfully (%d bytes)\n", ret); + return 0; +} + +/** + * Receive server response message + * + * @param sock socket file descriptor + * @param buffer buffer to store response + * @param buffer_size buffer size + * @return number of bytes received, -1 on error + */ +static int http_download_client_recv_response(int sock, char *buffer, int buffer_size) +{ + if (!buffer || buffer_size <= 0) { + return -1; + } + + memset(buffer, 0, buffer_size); + + int len = recv(sock, buffer, buffer_size - 1, 0); + if (len < 0) { + cat1_print_err("Receive failed: errno %d\n", errno); + return -1; + } else if (len == 0) { + cat1_print_info("Connection closed by server\n"); + return 0; + } + + buffer[len] = '\0'; + cat1_print_info("Received response: %s\n", buffer); + + return len; +} + +/** + * Receive file data and save to file + * + * @param sock socket file descriptor + * @param save_path file path to save + * @return 0 on success, -1 on failure + */ +static int http_download_client_recv_file_data(int sock, const char *save_path) +{ + FILE *fp = NULL; + char recv_buffer[MAX_RECV_BUFFER_SIZE]; + uint32_t total_received = 0; + int bytes_received = 0; + int file_size = 0; + char content_length_header[64] = {0}; + + // First, receive server response (OK message and Content-Length if available) + char response_buffer[MAX_RESPONSE_BUFFER_SIZE]; + int response_len = http_download_client_recv_response(sock, response_buffer, sizeof(response_buffer)); + + if (response_len <= 0) { + cat1_print_err("Failed to receive server response\n"); + return -1; + } + + // Parse Content-Length if present + if (strstr(response_buffer, "Content-Length:") != NULL) { + sscanf(response_buffer, "Content-Length: %d", &file_size); + cat1_print_info("Expected file size: %d bytes\n", file_size); + + // Skip the header line, read actual data + // The server sends header followed by file data + // We need to find where the header ends + char *data_start = strstr(response_buffer, "\n"); + if (data_start) { + data_start++; // Skip newline + // Some data might already be in the buffer + } + } + + // Open file for writing + if (save_path && strlen(save_path) > 0) { + fp = fopen(save_path, "wb"); + if (fp == NULL) { + cat1_print_err("Failed to open file: %s\n", save_path); + return -1; + } + cat1_print_info("Saving file to: %s\n", save_path); + } else { + cat1_print_info("No save path specified, discarding data\n"); + } + + // Receive file data + while (1) { + memset(recv_buffer, 0, sizeof(recv_buffer)); + bytes_received = recv(sock, recv_buffer, sizeof(recv_buffer), 0); + + if (bytes_received < 0) { + cat1_print_err("Receive error: errno %d\n", errno); + if (fp) { + fclose(fp); + } + return -1; + } else if (bytes_received == 0) { + cat1_print_info("Connection closed by server (file transfer complete)\n"); + break; + } + + total_received += bytes_received; + + // Check if this is a status message (OK: or ERROR:) + recv_buffer[bytes_received] = '\0'; + if (strncmp((char *)recv_buffer, "OK:", 3) == 0 || + strncmp((char *)recv_buffer, "ERROR:", 6) == 0) { + cat1_print_info("Server message: %s\n", recv_buffer); + + // If we've already received some data, the status might be after file data + // In this case, we should have already written the data + if (total_received > bytes_received) { + // This is a final status message + break; + } + continue; + } + + // Write data to file + if (fp) { + size_t written = fwrite(recv_buffer, 1, bytes_received, fp); + if (written != bytes_received) { + cat1_print_err("Write error: written %d, expected %d\n", written, bytes_received); + fclose(fp); + return -1; + } + fflush(fp); + } + + // Print progress every 64KB + if (total_received % (64 * 1024) < sizeof(recv_buffer)) { + if (file_size > 0) { + float progress = (float)total_received / file_size * 100.0f; + cat1_print_info("Progress: %.2f%% (%u/%d bytes)\n", + progress, total_received, file_size); + } else { + cat1_print_info("Received: %u bytes\n", total_received); + } + } + } + + if (fp) { + fclose(fp); + cat1_print_info("File saved successfully: %s (%u bytes)\n", save_path, total_received); + } else { + cat1_print_info("Data discarded successfully (%u bytes)\n", total_received); + } + + return 0; +} + +/** + * Download file from URL via HTTP download server + * + * @param url URL to download + * @param save_path file path to save (NULL to discard data) + * @return 0 on success, -1 on failure + */ +int http_download_client_download_file(const char *url, const char *save_path) +{ + int sock = -1; + int ret = -1; + + if (!url || strlen(url) == 0) { + cat1_print_err("Invalid URL parameter\n"); + return -1; + } + + // Initialize socket + sock = http_download_client_socket_init(); + if (sock < 0) { + return -1; + } + + // Connect to server + if (http_download_client_connect(sock) < 0) { + lwip_close(sock); + return -1; + } + + g_client_sock = sock; + g_client_connected = 1; + + // Send URL request + if (http_download_client_send_url(sock, url) < 0) { + lwip_close(sock); + g_client_sock = -1; + g_client_connected = 0; + return -1; + } + + // Receive and save file data + ret = http_download_client_recv_file_data(sock, save_path); + + // Cleanup + lwip_close(sock); + g_client_sock = -1; + g_client_connected = 0; + + return ret; +} + +/** + * Close client connection + */ +void http_download_client_close(void) +{ + if (g_client_sock >= 0) { + lwip_close(g_client_sock); + g_client_sock = -1; + g_client_connected = 0; + cat1_print_info("Client connection closed\n"); + } +} + +/** + * Check if client is connected + * + * @return 1 if connected, 0 otherwise + */ +uint8_t http_download_client_is_connected(void) +{ + return g_client_connected; +} + +/** + * Task function for HTTP download client + * Executes the download in a separate thread + * + * @param pvParameters unused (parameters are stored in static variable) + */ +static void http_download_client_task_func(void *pvParameters) +{ + UNUSED(pvParameters); + + cat1_print_info("HTTP download client task started\n"); + cat1_print_info("URL: %s\n", s_task_params.url); + if (s_task_params.save_path[0] != '\0') { + cat1_print_info("Save path: %s\n", s_task_params.save_path); + } else { + cat1_print_info("No save path (discard mode)\n"); + } + + // Execute download + const char *save_path_ptr = NULL; + if (s_task_params.save_path[0] != '\0') { + save_path_ptr = s_task_params.save_path; + } + + int ret = http_download_client_download_file(s_task_params.url, save_path_ptr); + + if (ret == 0) { + cat1_print_info("HTTP download client task completed successfully\n"); + } else { + cat1_print_err("HTTP download client task failed\n"); + } + + g_download_task_running = 0; + + cat1_print_info("HTTP download client task ended\n"); +} + +/** + * Start HTTP download in an asynchronous thread + * + * @param url URL to download + * @param save_path file path to save (NULL or empty to discard data) + * @return ERRCODE_SUCC on success, ERRCODE_FAIL on failure + */ +errcode_t http_download_client_download_async(const char *url, const char *save_path) +{ + if (!url || strlen(url) == 0) { + cat1_print_err("Invalid URL parameter\n"); + return ERRCODE_FAIL; + } + + if (strlen(url) >= MAX_URL_LEN) { + cat1_print_err("URL too long (max %d)\n", MAX_URL_LEN - 1); + return ERRCODE_FAIL; + } + + // Check if task is already running + // if (g_download_task_running) { + // cat1_print_err("Download task is already running\n"); + // return ERRCODE_FAIL; + // } + + // Initialize static parameters (stored globally for task function to access) + memset(&s_task_params, 0, sizeof(http_download_task_params_t)); + strncpy(s_task_params.url, url, MAX_URL_LEN - 1); + s_task_params.url[MAX_URL_LEN - 1] = '\0'; + + if (save_path && strlen(save_path) > 0) { + strncpy(s_task_params.save_path, save_path, sizeof(s_task_params.save_path) - 1); + s_task_params.save_path[sizeof(s_task_params.save_path) - 1] = '\0'; + } + + // Configure task + app_task_definition_t taskcfg = {0}; + taskcfg.create_queue_handle = NULL; + taskcfg.attr.name = "http_download_client"; + taskcfg.attr.priority = TASK_PRIORITY_APP; + taskcfg.attr.stack_size = HTTP_DOWNLOAD_TASK_SIZE; + taskcfg.func = http_download_client_task_func; + taskcfg.use_mem = USE_EXTEND_MEM; + + // Mark task as running before creating thread + g_download_task_running = 1; + + // Create thread + errcode_t ret = uapi_create_osThread_custom(&taskcfg); + if (ret != ERRCODE_SUCC) { + cat1_print_err("Failed to create HTTP download client task: %d\n", ret); + g_download_task_running = 0; + return ERRCODE_FAIL; + } + + cat1_print_info("HTTP download client task created successfully\n"); + + return ERRCODE_SUCC; +} + +/** + * Check if download task is running + * + * @return 1 if running, 0 otherwise + */ +uint8_t http_download_client_is_task_running(void) +{ + return g_download_task_running; +} + +/** + * Task function wrapper (legacy, kept for compatibility) + */ +void http_download_client_task(void *pvParameters) +{ + http_download_client_task_func(pvParameters); +} + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + diff --git a/code/sdk/zhihui/4g/cat1Service/include/http_file_download_client.h b/code/sdk/zhihui/4g/cat1Service/include/http_file_download_client.h new file mode 100644 index 000000000..59a9ef1bd --- /dev/null +++ b/code/sdk/zhihui/4g/cat1Service/include/http_file_download_client.h @@ -0,0 +1,66 @@ +/* HTTP File Download Client Header + * + * This header file defines the interface for the HTTP file download client. + */ + +#ifndef __HTTP_FILE_DOWNLOAD_CLIENT_H +#define __HTTP_FILE_DOWNLOAD_CLIENT_H + +#include +#include "errcode.h" + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +/** + * Download file from URL via HTTP download server + * + * @param url URL to download (must be valid, non-empty string) + * @param save_path file path to save (NULL or empty string to discard data) + * @return 0 on success, -1 on failure + */ +int http_download_client_download_file(const char *url, const char *save_path); + +/** + * Close client connection + */ +void http_download_client_close(void); + +/** + * Check if client is connected + * + * @return 1 if connected, 0 otherwise + */ +uint8_t http_download_client_is_connected(void); + +/** + * Task function for HTTP download client + * Can be used in a separate thread + * + * @param pvParameters parameters (can contain URL and save_path) + */ +void http_download_client_task(void *pvParameters); + +/** + * Start HTTP download in an asynchronous thread + * + * @param url URL to download + * @param save_path file path to save (NULL or empty to discard data) + * @return ERRCODE_SUCC on success, ERRCODE_FAIL on failure + */ +errcode_t http_download_client_download_async(const char *url, const char *save_path); + +/** + * Check if download task is running + * + * @return 1 if running, 0 otherwise + */ +uint8_t http_download_client_is_task_running(void); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* __HTTP_FILE_DOWNLOAD_CLIENT_H */ + diff --git a/code/sdk/zhihui/4gTest/cat1_at_cmd.c b/code/sdk/zhihui/4gTest/cat1_at_cmd.c index 8ad2aeb13..f7f949d37 100644 --- a/code/sdk/zhihui/4gTest/cat1_at_cmd.c +++ b/code/sdk/zhihui/4gTest/cat1_at_cmd.c @@ -35,6 +35,7 @@ extern "C" { #ifdef SUPPORT_TCP_TEST #include "../tcptest/include/zhihui_tcp_test.h" #endif +#include "http_file_download_client.h" static int ConvertCmdToParam(uint8_t *para, uint32_t paraLen, uint8_t argv[CUSTOM_UTILS_MAX_ARGC][CUSTOM_UTILS_MAX_ARGV_LEN], uint32_t* argc) @@ -409,6 +410,70 @@ static uint32_t custom_start_ota_download(uint8_t *para, uint32_t paraLen, char return ERRCODE_SUCC; } +/** + * @brief AT^HTTPDLCLIENT - HTTP文件下载客户端测试接口 + * 格式: AT^HTTPDLCLIENT=url,save_path + * 参数: url - 要下载的文件URL + * save_path - 保存文件的路径(可选,为空或"null"则不保存文件) + * 示例: AT^HTTPDLCLIENT=https://example.com/file.zip,/user/file.zip + * AT^HTTPDLCLIENT=https://example.com/file.zip,null + */ +static uint32_t at_http_download_client_test(uint8_t *para, uint32_t paraLen, char *strCommonCmd) +{ + UNUSED(strCommonCmd); + + uint8_t argv[CUSTOM_UTILS_MAX_ARGC][CUSTOM_UTILS_MAX_ARGV_LEN] = { 0 }; + uint32_t argc = 0; + + int ret = ConvertCmdToParam(para, paraLen, argv, &argc); + if (ret != CUSTOM_UTILS_SUCCESS) { + cat1_print_err("HTTPDLCLIENT ATCMD Convert Command Failed\n"); + return ERRCODE_FAIL; + } + + if (argc < 1 || argc > 2) { + cat1_print_err("HTTPDLCLIENT ATCMD Set Parameter Failed argc %d\nshould input:\nAT^HTTPDLCLIENT=url[,save_path]\n", argc); + cat1_print_info("Example: AT^HTTPDLCLIENT=https://example.com/file.zip,/user/file.zip\n"); + cat1_print_info("Example: AT^HTTPDLCLIENT=https://example.com/file.zip,null\n"); + return ERRCODE_FAIL; + } + + const char *url = (const char*)argv[0]; + const char *save_path = NULL; + + if (argc >= 2) { + // 如果第二个参数不为"null"或空,则使用它作为保存路径 + char *path_arg = (char*)argv[1]; + if (strlen(path_arg) > 0) { + // 检查是否为 "null" (不区分大小写) + int is_null = (strcmp(path_arg, "null") == 0 || + strcmp(path_arg, "NULL") == 0 || + strcmp(path_arg, "Null") == 0); + if (!is_null) { + save_path = (const char*)path_arg; + } + } + } + + cat1_print_info("HTTPDLCLIENT: URL=%s\n", url); + if (save_path) { + cat1_print_info("HTTPDLCLIENT: Save path=%s\n", save_path); + } else { + cat1_print_info("HTTPDLCLIENT: No save path (discard mode)\n"); + } + + // 调用HTTP文件下载客户端 + int result = http_download_client_download_async("https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Amber/WorkAndStudy.mp4", save_path); + + if (result == 0) { + cat1_print_info("HTTPDLCLIENT: Download completed successfully\n"); + return ERRCODE_SUCC; + } else { + cat1_print_err("HTTPDLCLIENT: Download failed\n"); + return ERRCODE_FAIL; + } +} + static uint32_t custom_enable_cat1_debug_log(uint8_t *para, uint32_t paraLen, char *strCommonCmd) { @@ -1043,6 +1108,7 @@ static const at_cmd_table_t g_cat1_cmd[] = { { (char *)"AT^PPPTCPSTOP", at_4g_tcp_test_stop, (char *)" " }, { (char *)"AT^PPPTCPSTATS", at_4g_tcp_test_stats, (char *)" " }, { (char *)"AT^CAT1TCPINIT", at_cat1_TCP_init, (char *)" " }, + { (char *)"AT^HTTPDLCLIENT", at_http_download_client_test, (char *)"url[,save_path]" }, }; -- 2.17.1