增加使用模组中转下载文件 2800/1
authorhcj <1224667075@qq.com>
Tue, 25 Nov 2025 08:15:21 +0000 (16:15 +0800)
committerhcj <1224667075@qq.com>
Tue, 25 Nov 2025 08:15:21 +0000 (16:15 +0800)
cat1modem/src/LeiNiao/httpfiledownload.py [new file with mode: 0644]
cat1modem/src/LeiNiao/main_init.py
code/sdk/zhihui/4g/cat1Service/CMakeLists.txt
code/sdk/zhihui/4g/cat1Service/Service/http_file_download_client.c [new file with mode: 0644]
code/sdk/zhihui/4g/cat1Service/include/http_file_download_client.h [new file with mode: 0644]
code/sdk/zhihui/4gTest/cat1_at_cmd.c

diff --git a/cat1modem/src/LeiNiao/httpfiledownload.py b/cat1modem/src/LeiNiao/httpfiledownload.py
new file mode 100644 (file)
index 0000000..67a52d6
--- /dev/null
@@ -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
index 4eb6359..065fe3b 100644 (file)
@@ -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")
index 691a92a..1cca674 100644 (file)
@@ -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 (file)
index 0000000..502d066
--- /dev/null
@@ -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 <dirent.h>
+#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 <lwip/netdb.h>
+#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 <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#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 (file)
index 0000000..59a9ef1
--- /dev/null
@@ -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 <stdint.h>
+#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 */
+
index 8ad2aeb..f7f949d 100644 (file)
@@ -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]" },
     
 };