增加最新版本SPI传输
authorhcj <1224667075@qq.com>
Fri, 12 Dec 2025 09:57:30 +0000 (17:57 +0800)
committerhcj <1224667075@qq.com>
Fri, 12 Dec 2025 09:57:30 +0000 (17:57 +0800)
12 files changed:
cat1modem/src/LeiNiao/cat1Service.py
cat1modem/src/LeiNiao/httpfiledownload.py
code/sdk/zhihui/4g/cat1Service/Service/http_file_download_client.c
code/sdk/zhihui/4g/cat1Service/include/http_file_download_client.h
code/sdk/zhihui/leiniao/imsl_ln/CMakeLists.txt
code/sdk/zhihui/leiniao/imsl_ln/ln_api.c
code/sdk/zhihui/leiniao/imsl_ln/ln_api.h
code/sdk/zhihui/leiniao/imsl_ln/ln_api_websocket.c
code/sdk/zhihui/leiniao/imsl_ln/ln_websoket_api_ext.c [new file with mode: 0644]
code/sdk/zhihui/utils/custom_spi.c
code/sdk/zhihui/utils/custom_spi.h
code/sdk/zhihui/websocket/os_ws/haisi_websocket.c

index 2d00955..5f169bc 100644 (file)
@@ -79,7 +79,7 @@ sms.setCallback(msg_cb)#sms注册回调
 CAT1_SERVICE_HEAD = 'FE' #固定head 
 
 PROJECT_NAME = "Cat1SlipUDPService"
-PROJECT_VERSION = "PY_SRV_V28"
+PROJECT_VERSION = "PY_SRV_V29"
 CONFIG_JSON="/usr/system_config.json"
 CURRENT_APP_TYPE="pysrv"
 
@@ -379,7 +379,25 @@ class Cat1Service:
             TcpService = TcpServiceInit()
             TcpService.start_socket_thread()
         elif cmd== "1E":
-            download_file_spi(str(parm))
+            # 解析参数,支持断点重传模式(格式:URL|resume_from_byte)
+            parm_str = str(parm)
+            if '|' in parm_str:
+                # 断点重传模式:分离URL和重传位置
+                parts = parm_str.split('|', 1)  # 只分割第一个|符号
+                if len(parts) == 2:
+                    url = parts[0]
+                    try:
+                        resume_from_byte = int(parts[1])
+                        download_file_spi(url, resume_from_byte=resume_from_byte)
+                    except ValueError:
+                        # 如果重传位置不是有效数字,使用默认值0
+                        download_file_spi(parts[0], resume_from_byte=0)
+                else:
+                    # 格式错误,使用默认模式
+                    download_file_spi(parm_str)
+            else:
+                # 普通下载模式
+                download_file_spi(parm_str)
         elif cmd == "AC":#
             if self.ack_sem==1:
                 self.ack_sem=0
index f7702c1..957c972 100644 (file)
@@ -26,8 +26,23 @@ import usocket
 import _thread
 from machine import SPI
 from machine import Pin
+from machine import ExtInt
 import uctypes
 
+# 全局标志:中断触发时停止传输
+stop_transmission = False
+
+def ExtIntfun(args):
+    # 中断回调:设置停止传输标志
+    global stop_transmission
+    stop_transmission = True
+    clog.info('### Interrupt received, stopping transmission ###')  # args[0]:gpio号 args[1]:上升沿或下降沿
+# 初始化外部中断(GPIO3,上升/下降沿触发,无上下拉)
+extint = ExtInt(ExtInt.GPIO3, ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_DISABLE, ExtIntfun)
+extint.enable()
+clog.info("External interrupt initialized: GPIO3, RISING_FALLING edge")
+
+
 # spi - 通道选择[0,1],int类型。
 
 # mode - SPI 的工作模式,int类型,说明如下:
@@ -69,8 +84,8 @@ gpioWakeBT.write(1)
 # 提前创建1024字节SPI发送缓冲区(全局初始化,避免重复创建)
 # 包头格式:前4字节为数据长度(uint32_t,小端序),后1020字节为数据
 SPI_TX_BUFFER = bytearray(1024)
-PACKET_HEADER_SIZE = 4  # 包头大小:4字节(数据长度)
-PACKET_DATA_SIZE = 1020  # 数据部分大小:1020字节(1024 - 4)
+PACKET_HEADER_SIZE = 8   # 包头大小:8字节(数据长度4 + 包序号4)
+PACKET_DATA_SIZE = 1016  # 数据部分大小:1024 - 8
 
 # 兼容型字节拷贝函数(替代切片赋值,适配所有MicroPython版本)
 def copy_bytes_to_buffer(buffer, data, offset=0):
@@ -475,11 +490,21 @@ class HttpFileDownloadTcpServer:
             remaining_data = b''
             # 累计补0字节数
             total_padded_zeros = 0
+            # 包序号(从0开始)
+            seq_num = 0
             # 全局SPI缓冲区引用
             tx_buffer = SPI_TX_BUFFER
+            # 重置停止传输标志
+            global stop_transmission
+            stop_transmission = False
             
             # response.content 是 bytes 类型生成器
             for chunk in response.content:
+                # 检查是否收到停止传输的中断信号
+                if stop_transmission:
+                    clog.warning("Transmission stopped by interrupt signal")
+                    return False, 0.0, -1
+                
                 if chunk:  # 过滤掉空块
                     try:
                         # 合并剩余数据和当前chunk
@@ -488,15 +513,23 @@ class HttpFileDownloadTcpServer:
                         
                         # 拆分数据为PACKET_DATA_SIZE字节的块(提前填充缓冲区,带包头)
                         while len(combined_data) >= PACKET_DATA_SIZE:
-                            # 1. 填充包头(4字节数据长度,小端序)
+                            # 检查是否收到停止传输的中断信号
+                            if stop_transmission:
+                                clog.warning("Transmission stopped by interrupt signal during packet sending")
+                                return False, 0.0, -1
+                            # 1. 填充包头(4字节数据长度 + 4字节包序号,小端序)
                             data_len = PACKET_DATA_SIZE
-                            header_bytes = pack_uint32_le(data_len)
-                            copy_bytes_to_buffer(tx_buffer, header_bytes, 0)
+                            header_len_bytes = pack_uint32_le(data_len)
+                            header_seq_bytes = pack_uint32_le(seq_num)
+                            copy_bytes_to_buffer(tx_buffer, header_len_bytes, 0)
+                            copy_bytes_to_buffer(tx_buffer, header_seq_bytes, 4)
                             
-                            # 2. 填充数据部分(1020字节)
+                            # 2. 填充数据部分(1016字节)
                             copy_bytes_to_buffer(tx_buffer, combined_data[:PACKET_DATA_SIZE], PACKET_HEADER_SIZE)
                             # 剩余数据保留
                             combined_data = combined_data[PACKET_DATA_SIZE:]
+                            # 增加包序号
+                            seq_num += 1
                             
                             # 3. GPIO电平切换(原有时序)
                             gpioWakeBT.write(0)
@@ -535,6 +568,11 @@ class HttpFileDownloadTcpServer:
             
             # 处理最后不足PACKET_DATA_SIZE字节的剩余数据(提前填充缓冲区补0,兼容版,带包头)
             if len(remaining_data) > 0:
+                # 检查是否收到停止传输的中断信号
+                if stop_transmission:
+                    clog.warning("Transmission stopped by interrupt signal before sending final chunk")
+                    return False, 0.0, -1
+                
                 try:
                     # 1. 计算补0数(提前计算)
                     data_len = len(remaining_data)
@@ -681,7 +719,7 @@ class HttpFileDownloadSpiTest:
     直接从固定URL下载文件并通过SPI发送,用于测试SPI传输速度
     """
     
-    def __init__(self, url=None, timeout=30, sizeof=255, spi_obj=None):
+    def __init__(self, url=None, timeout=30, sizeof=255, spi_obj=None, resume_from_byte=0):
         """
         初始化SPI传输测试
         
@@ -690,10 +728,12 @@ class HttpFileDownloadSpiTest:
             timeout: 下载超时时间(秒),默认30秒
             sizeof: 读取缓冲区的数据块大小,默认255字节
             spi_obj: SPI对象,用于发送数据。如果为None,则自动创建SPI(1, 0, 12)
+            resume_from_byte: 断点重传起始字节位置,默认0(从头开始)。如果大于0,将从该位置继续下载和传输
         """
         self.url = url if url else DEFAULT_TEST_URL
         self.timeout = timeout
         self.sizeof = sizeof
+        self.resume_from_byte = max(0, int(resume_from_byte))  # 确保非负整数
         self.downloader = HttpFileDownloader(timeout=timeout, sizeof=sizeof)
         
         # 初始化SPI对象
@@ -731,34 +771,74 @@ class HttpFileDownloadSpiTest:
     def start_download_and_send(self):
         """
         开始下载文件并通过SPI发送(提前补0优化版,兼容所有MicroPython)
+        支持断点重传:如果resume_from_byte > 0,将从该位置继续下载和传输
         
         Returns:
             tuple: (success: bool, download_time: float, file_size: int)
         """
         try:
+            # 计算断点重传的起始位置(包序号从断点位置对应的包序号开始,用于校验断点位置)
+            resume_from_packet = 0
+            skip_bytes = 0
+            if self.resume_from_byte > 0:
+                resume_from_packet = self.resume_from_byte // PACKET_DATA_SIZE
+                skip_bytes = self.resume_from_byte % PACKET_DATA_SIZE
+                clog.info("Resume from byte {} (equivalent packet {}, skip {} bytes in first packet, seq_num starts from {})".format(
+                    self.resume_from_byte, resume_from_packet, skip_bytes, resume_from_packet))
+            
             clog.info("Starting SPI speed test: {}".format(self.url))
             
             # 记录开始时间
             start_time = utime.time()
             
-            # 发送GET请求
-            response = request.get(
-                self.url,
-                sizeof=self.sizeof,
-                decode=False  # 不进行UTF-8解码,直接获取bytes
-            )
+            # 发送GET请求(如果启用断点重传,使用Range请求)
+            if self.resume_from_byte > 0:
+                # 使用Range请求从指定位置开始下载
+                headers = {'Range': 'bytes={}-'.format(self.resume_from_byte)}
+                response = request.get(
+                    self.url,
+                    headers=headers,
+                    sizeof=self.sizeof,
+                    decode=False  # 不进行UTF-8解码,直接获取bytes
+                )
+                # Range请求成功时状态码是206,而不是200
+                if response.status_code not in (200, 206):
+                    clog.error("Error: HTTP error {}: {}".format(response.status_code, self.url))
+                    self.close()  # 关闭SPI
+                    return False, 0.0, -1
+            else:
+                # 正常请求
+                response = request.get(
+                    self.url,
+                    sizeof=self.sizeof,
+                    decode=False  # 不进行UTF-8解码,直接获取bytes
+                )
             
-            # 检查响应状态码
-            if response.status_code != 200:
+            # 检查响应状态码(Range请求返回206,正常请求返回200)
+            if response.status_code not in (200, 206):
                 clog.error("Error: HTTP error {}: {}".format(response.status_code, self.url))
                 self.close()  # 关闭SPI
                 return False, 0.0, -1
             
-            # 获取文件总大小(如果服务器提供了Content-Length)
+            # 获取文件总大小(如果服务器提供了Content-Length或Content-Range)
             total_size = 0
             if 'Content-Length' in response.headers:
                 try:
-                    total_size = int(response.headers['Content-Length'])
+                    # Range请求时,Content-Length是剩余部分的大小
+                    remaining_size = int(response.headers['Content-Length'])
+                    if self.resume_from_byte > 0:
+                        # 如果有Content-Range,可以获取总大小
+                        if 'Content-Range' in response.headers:
+                            # Content-Range格式: "bytes start-end/total" 或 "bytes */total"
+                            content_range = response.headers['Content-Range']
+                            if '/' in content_range:
+                                total_size = int(content_range.split('/')[-1])
+                            else:
+                                total_size = self.resume_from_byte + remaining_size
+                        else:
+                            total_size = self.resume_from_byte + remaining_size
+                    else:
+                        total_size = remaining_size
                     clog.info("File size: {} bytes ({:.2f} MB)".format(
                         total_size, total_size / 1024.0 / 1024.0))
                 except (ValueError, TypeError):
@@ -770,50 +850,103 @@ class HttpFileDownloadSpiTest:
             remaining_data = b''
             # 累计补0字节数
             total_padded_zeros = 0
+            # 包序号(从断点位置对应的包序号开始,用于校验断点位置)
+            seq_num = resume_from_packet  # 从断点位置对应的包序号开始
             # 全局SPI缓冲区引用
             tx_buffer = SPI_TX_BUFFER
+            # 重置停止传输标志
+            global stop_transmission
+            stop_transmission = False
+            # 第一个包需要跳过的字节数(断点重传时)
+            first_packet_skip = skip_bytes
             
             # response.content是bytes类型生成器
             for chunk in response.content:
+                # 检查是否收到停止传输的中断信号
+                if stop_transmission:
+                    clog.warning("Transmission stopped by interrupt signal")
+                    self.close()  # 关闭SPI
+                    return False, 0.0, -1
+                
                 if chunk:  # 过滤掉空块
                     try:
                         # 合并剩余数据和当前chunk
                         combined_data = remaining_data + chunk
                         remaining_data = b''
                         
+                        # 处理第一个包的跳过逻辑(断点重传时)
+                        if first_packet_skip > 0 and len(combined_data) > 0:
+                            # 跳过前first_packet_skip字节
+                            if len(combined_data) > first_packet_skip:
+                                combined_data = combined_data[first_packet_skip:]
+                                skipped = first_packet_skip
+                                first_packet_skip = 0  # 标记已处理
+                                clog.info("Skipped {} bytes in first packet for resume".format(skipped))
+                            else:
+                                # 如果剩余数据不足跳过,全部跳过
+                                first_packet_skip -= len(combined_data)
+                                combined_data = b''
+                                continue
+                        
                         # 拆分数据为PACKET_DATA_SIZE字节的块(提前填充缓冲区,带包头)
                         while len(combined_data) >= PACKET_DATA_SIZE:
-                            # 1. 填充包头(4字节数据长度,小端序)
+                            # 检查是否收到停止传输的中断信号
+                            if stop_transmission:
+                                clog.warning("Transmission stopped by interrupt signal during packet sending")
+                                self.close()  # 关闭SPI
+                                return False, 0.0, -1
+                            # 1. 填充包头(4字节数据长度 + 4字节包序号,小端序)
                             data_len = PACKET_DATA_SIZE
-                            header_bytes = pack_uint32_le(data_len)
-                            copy_bytes_to_buffer(tx_buffer, header_bytes, 0)
+                            header_len_bytes = pack_uint32_le(data_len)
+                            header_seq_bytes = pack_uint32_le(seq_num)
+                            copy_bytes_to_buffer(tx_buffer, header_len_bytes, 0)
+                            copy_bytes_to_buffer(tx_buffer, header_seq_bytes, 4)
                             
-                            # 2. 填充数据部分(1020字节)
+                            # 2. 填充数据部分(1016字节)
                             copy_bytes_to_buffer(tx_buffer, combined_data[:PACKET_DATA_SIZE], PACKET_HEADER_SIZE)
                             # 剩余数据保留
                             combined_data = combined_data[PACKET_DATA_SIZE:]
                             
-                            # 3. GPIO电平切换(原有时序)
-                            gpioWakeBT.write(0)
-                            utime.sleep_ms(2)
-                            gpioWakeBT.write(1)
-                            utime.sleep_ms(2)
+                            # 记录是否为包序号0(需要发送2次,仅当从头开始下载时)
+                            # 断点重传时,即使包序号不是0,第一个包也可能需要特殊处理
+                            is_seq_zero = (seq_num == resume_from_packet)
+                            # 如果是断点重传的第一个包(seq_num == resume_from_packet 且 resume_from_packet > 0),不需要发送2次
+                            # 只有从头开始下载(seq_num == 0)时才发送2次
+                            send_count = 2 if is_seq_zero else 1
                             
-                            # 4. 发送缓冲区数据(包头+数据,共1024字节)
-                            chunk_len = 1024
-                            if hasattr(self.spi_obj, 'write'):
-                                self.spi_obj.write(tx_buffer, chunk_len)
-                            elif hasattr(self.spi_obj, 'write_read'):
-                                read_buf = bytearray(chunk_len)
-                                try:
-                                    self.spi_obj.write_read(read_buf, tx_buffer, chunk_len, 5)
-                                except TypeError:
-                                    self.spi_obj.write_read(read_buf, tx_buffer, chunk_len)
-                            else:
-                                clog.error("SPI object does not have write or write_read method")
-                                self.close()  # 关闭SPI
-                                return False, 0.0, -1
+                            # 发送包(包序号为0时发送2次)
+                            for send_idx in range(send_count):
+                                clog.info("spi_obj seq_num {} (send {}/{})".format(seq_num, send_idx + 1, send_count))
+                                
+                                # 3. GPIO电平切换(原有时序)
+                                gpioWakeBT.write(0)
+                                utime.sleep_ms(2)
+                                gpioWakeBT.write(1)
+                                utime.sleep_ms(2)
+                                
+                                # 4. 发送缓冲区数据(包头+数据,共1024字节)
+                                chunk_len = 1024
+                                if hasattr(self.spi_obj, 'write'):
+                                    try:
+                                        self.spi_obj.write(tx_buffer, chunk_len)
+                                        utime.sleep_ms(10)
+                                    except Exception as e:
+                                        clog.error("SPI write error: {}".format(e))
+                                        self.close()
+                                        return False, 0.0, -1
+                                elif hasattr(self.spi_obj, 'write_read'):
+                                    read_buf = bytearray(chunk_len)
+                                    try:
+                                        self.spi_obj.write_read(read_buf, tx_buffer, chunk_len, 5)
+                                    except TypeError:
+                                        self.spi_obj.write_read(read_buf, tx_buffer, chunk_len)
+                                else:
+                                    clog.error("SPI object does not have write or write_read method")
+                                    self.close()  # 关闭SPI
+                                    return False, 0.0, -1
                             
+                            # 增加包序号(发送完成后)
+                            seq_num += 1
                             downloaded_size += PACKET_DATA_SIZE
                             
                             # 显示进度(每发送64KB打印一次,减少打印数量)
@@ -834,15 +967,23 @@ class HttpFileDownloadSpiTest:
             
             # 处理最后不足PACKET_DATA_SIZE字节的剩余数据(提前填充缓冲区补0,兼容版,带包头)
             if len(remaining_data) > 0:
+                # 检查是否收到停止传输的中断信号
+                if stop_transmission:
+                    clog.warning("Transmission stopped by interrupt signal before sending final chunk")
+                    self.close()  # 关闭SPI
+                    return False, 0.0, -1
+                
                 try:
                     # 1. 提前计算补0数和填充缓冲区
                     data_len = len(remaining_data)
                     pad_len = PACKET_DATA_SIZE - data_len
                     total_padded_zeros += pad_len
                     
-                    # 2. 填充包头(4字节数据长度,小端序)
-                    header_bytes = pack_uint32_le(data_len)
-                    copy_bytes_to_buffer(tx_buffer, header_bytes, 0)
+                    # 2. 填充包头(4字节数据长度 + 4字节包序号,小端序)
+                    header_len_bytes = pack_uint32_le(data_len)
+                    header_seq_bytes = pack_uint32_le(seq_num)
+                    copy_bytes_to_buffer(tx_buffer, header_len_bytes, 0)
+                    copy_bytes_to_buffer(tx_buffer, header_seq_bytes, 4)
                     
                     # 3. 提前填充缓冲区(逐字节拷贝,无切片赋值)
                     # 清空数据部分(显式清空,确保兼容性)
@@ -850,6 +991,8 @@ class HttpFileDownloadSpiTest:
                         tx_buffer[i] = 0
                     # 填充有效数据(从包头后开始,逐字节拷贝)
                     copy_bytes_to_buffer(tx_buffer, remaining_data, PACKET_HEADER_SIZE)
+                    # 增加包序号
+                    seq_num += 1
                     
                     clog.info("=== Pre-filled SPI Buffer Info ===")
                     clog.info("Buffer filled with {} bytes valid data (pre-send)".format(data_len))
@@ -866,7 +1009,12 @@ class HttpFileDownloadSpiTest:
                     
                     # 4. 发送已补0的缓冲区(无任何拼接/计算)
                     if hasattr(self.spi_obj, 'write'):
-                        self.spi_obj.write(tx_buffer, 1024)
+                        try:
+                            self.spi_obj.write(tx_buffer, 1024)
+                        except Exception as e:
+                            clog.error("SPI write error on final chunk: {}".format(e))
+                            self.close()
+                            return False, 0.0, -1
                     elif hasattr(self.spi_obj, 'write_read'):
                         read_buf = bytearray(1024)
                         try:
@@ -877,7 +1025,7 @@ class HttpFileDownloadSpiTest:
                     downloaded_size += data_len  # 只统计实际数据长度
                     # 打印最后一块补0详情
                     clog.info("=== Final SPI Chunk Detail ===")
-                    clog.info("Last chunk header: {} bytes (data length: {})".format(PACKET_HEADER_SIZE, data_len))
+                    clog.info("Last chunk header: {} bytes (data length: {}, seq={})".format(PACKET_HEADER_SIZE, data_len, seq_num-1))
                     clog.info("Last chunk actual data: {} bytes".format(data_len))
                     clog.info("Last chunk padded zeros: {} bytes (all pre-send)".format(pad_len))
                     clog.info("Total padded zeros in transmission: {} bytes".format(total_padded_zeros))
@@ -1009,7 +1157,7 @@ def start_spi_speed_test():
 # ==================== 外部接口函数 ====================
 # 提供简单的函数接口,可以从外部传入URL进行下载和SPI传输
 
-def download_file_spi(url, timeout=30, sizeof=255, spi_obj=None):
+def download_file_spi(url, timeout=30, sizeof=255, spi_obj=None, resume_from_byte=0):
     """
     下载文件并通过SPI传输的简单接口函数
     
@@ -1018,6 +1166,7 @@ def download_file_spi(url, timeout=30, sizeof=255, spi_obj=None):
         timeout: 下载超时时间(秒),默认30秒
         sizeof: 读取缓冲区的数据块大小,默认255字节
         spi_obj: SPI对象,用于发送数据。如果为None,则自动创建SPI(2, 0, 12)
+        resume_from_byte: 断点重传起始字节位置,默认0(从头开始)。如果大于0,将从该位置继续下载和传输
     
     Returns:
         tuple: (success: bool, download_time: float, file_size: int)
@@ -1027,33 +1176,43 @@ def download_file_spi(url, timeout=30, sizeof=255, spi_obj=None):
     
     Example:
         # 基本用法
-        success, time, size = download_file_via_spi("https://example.com/file.zip")
+        success, time, size = download_file_spi("https://example.com/file.zip")
         
         # 自定义参数
-        success, time, size = download_file_via_spi(
+        success, time, size = download_file_spi(
             url="https://example.com/file.zip",
             timeout=60,
             sizeof=512
         )
+        
+        # 断点重传:从第1024字节位置继续下载和传输
+        success, time, size = download_file_spi(
+            url="https://example.com/file.zip",
+            resume_from_byte=1024
+        )
     """
     if not url or not isinstance(url, str):
-        clog.error("download_file_via_spi: Invalid URL parameter")
+        clog.error("download_file_spi: Invalid URL parameter")
         return False, 0.0, -1
     
-    test = HttpFileDownloadSpiTest(url=url, timeout=timeout, sizeof=sizeof, spi_obj=spi_obj)
+    test = HttpFileDownloadSpiTest(url=url, timeout=timeout, sizeof=sizeof, spi_obj=spi_obj, resume_from_byte=resume_from_byte)
     try:
         success, download_time, file_size = test.start_download_and_send()
         if success:
-            clog.info("download_file_via_spi: Download completed successfully")
+            clog.info("download_file_spi: Download completed successfully")
             clog.info("  URL: {}".format(url))
+            if resume_from_byte > 0:
+                clog.info("  Resumed from byte: {}".format(resume_from_byte))
             clog.info("  Size: {} bytes".format(file_size))
             clog.info("  Time: {:.3f} seconds".format(download_time))
         else:
-            clog.error("download_file_via_spi: Download failed")
+            clog.error("download_file_spi: Download failed")
             clog.error("  URL: {}".format(url))
+            if resume_from_byte > 0:
+                clog.error("  Resume from byte: {}".format(resume_from_byte))
         return success, download_time, file_size
     except Exception as e:
-        clog.error("download_file_via_spi: Exception occurred: {}".format(e))
+        clog.error("download_file_spi: Exception occurred: {}".format(e))
         return False, 0.0, -1
     finally:
         # 确保SPI对象被关闭
@@ -1080,7 +1239,7 @@ class HttpFileDownloadSpiClient:
         self.spi_obj = spi_obj
         self.test_instance = None
     
-    def download(self, url):
+    def download(self, url, resume_from_byte=0):
         """
         下载指定URL的文件并通过SPI传输
         
@@ -1097,6 +1256,9 @@ class HttpFileDownloadSpiClient:
             client = HttpFileDownloadSpiClient()
             success, time, size = client.download("https://example.com/file.zip")
             client.close()
+            
+            # 断点重传
+            success, time, size = client.download("https://example.com/file.zip", resume_from_byte=1024)
         """
         if not url or not isinstance(url, str):
             clog.error("HttpFileDownloadSpiClient.download: Invalid URL parameter")
@@ -1107,7 +1269,8 @@ class HttpFileDownloadSpiClient:
             url=url, 
             timeout=self.timeout, 
             sizeof=self.sizeof, 
-            spi_obj=self.spi_obj
+            spi_obj=self.spi_obj,
+            resume_from_byte=resume_from_byte
         )
         
         try:
index 0064923..9639b36 100644 (file)
@@ -78,18 +78,164 @@ static http_download_state_t g_download_state = HTTP_DOWNLOAD_STATE_IDLE;
 static osSemaphoreId_t g_download_complete_sem = NULL;  /* 下载完成信号量 */
 static int g_download_file_fd = -1;  /* 下载文件句柄 */
 static char g_download_file_path[512] = {0};  /* 下载文件路径 */
+static char g_download_url[512] = {0};  /* 当前下载的URL */
 static uint32_t g_total_received_bytes = 0;  /* 总接收字节数 */
 static uint32_t g_total_received_packets = 0;  /* 总接收包数 */
 static http_file_download_data_callback_t g_external_data_callback = NULL;  /* 外部数据回调函数 */
+static osal_timer g_download_timeout_timer = {0};  /* 下载超时定时器 */
+static uint32_t g_download_timeout_ms = 10000;  /* 下载超时时间(毫秒),默认30秒 */
+static int http_download_spi_data_callback(const uint8_t *data, uint32_t data_len, uint32_t packet_num);
+static void http_download_spi_error_callback(const custom_spi_error_info_t *error_info);
+static void http_download_timeout_callback(unsigned long arg);
+
 
 /* 前向声明 */
 extern void udp_send_msg_toCat1(uint8_t cmd, char *content);
+extern uint8_t g_py_ver;  /* Python服务版本号 */
+
+/**
+ * @brief 下载超时定时器回调函数
+ * @param arg 定时器参数(未使用)
+ */
+static void http_download_timeout_callback(unsigned long arg)
+{
+    (void)arg;
+    
+    // 检查是否仍在下载状态
+    if (g_download_state != HTTP_DOWNLOAD_STATE_DOWNLOADING) {
+        cat1_print_warning("HTTP download: Timeout callback called but not in downloading state\n");
+        return;
+    }
+    
+    cat1_print_err("HTTP download: Timeout! No data received for %u ms\n", g_download_timeout_ms);
+    
+    // 停止定时器
+    osal_timer_stop(&g_download_timeout_timer);
+    
+    // 触发下载失败
+    http_file_download_notify_complete(0);
+}
+
+/**
+ * @brief SPI传输错误回调函数(用于下载服务)
+ * @param error_info 错误信息结构体指针
+ */
+static void http_download_spi_error_callback(const custom_spi_error_info_t *error_info)
+{
+    if (error_info == NULL) {
+        cat1_print_err("HTTP download: Invalid error info in error callback\n");
+        return;
+    }
+    
+    // 根据错误类型进行相应处理
+    switch (error_info->error_type) {
+        case CUSTOM_SPI_ERROR_READ_FAILED:
+            cat1_print_err("HTTP download: SPI read failed, error_code=0x%x, expected_seq=%u, received_packets=%u\n",
+                          error_info->error_code, error_info->expected_seq, error_info->received_packets);
+            break;
+            
+        case CUSTOM_SPI_ERROR_INVALID_DATA_LEN:
+            cat1_print_err("HTTP download: Invalid data length %u (max: %u), packet_seq=%u, expected_seq=%u\n",
+                          error_info->data_len, SPI_PACKET_DATA_SIZE, error_info->packet_seq, error_info->expected_seq);
+            break;
+            
+        case CUSTOM_SPI_ERROR_SEQUENCE_GAP:
+            cat1_print_err("HTTP download: Sequence gap detected, lost %u packets, packet_seq=%u, expected_seq=%u, received_packets=%u\n",
+                          error_info->error_code, error_info->packet_seq, error_info->expected_seq, error_info->received_packets);
+            // 丢包错误:可以选择继续或终止下载
+            // 这里选择继续,但记录错误
+            break;
+            
+        case CUSTOM_SPI_ERROR_DUPLICATE_PACKET:
+            cat1_print_warning("HTTP download: Duplicate packet detected, packet_seq=%u, expected_seq=%u, received_packets=%u\n",
+                              error_info->packet_seq, error_info->expected_seq, error_info->received_packets);
+            // 重复包:通常可以忽略,但记录警告
+            break;
+            
+        case CUSTOM_SPI_ERROR_CALLBACK_FAILED:
+            cat1_print_err("HTTP download: Data callback failed, error_code=%d, packet_seq=%u, expected_seq=%u\n",
+                          error_info->error_code, error_info->packet_seq, error_info->expected_seq);
+            // 数据回调失败:可能是文件写入失败等,需要终止下载
+            // 注意:这个错误会在下面的严重错误检查中统一处理
+            break;
+            
+        default:
+            cat1_print_err("HTTP download: Unknown error type %d, packet_seq=%u, expected_seq=%u\n",
+                          error_info->error_type, error_info->packet_seq, error_info->expected_seq);
+            break;
+    }
+    
+    // 对于严重错误(读取失败、数据长度无效、回调失败),重新初始化SPI并终止下载
+    // 注意:版本28不进行SPI重新初始化,因为版本28不支持中断触发和包序号检查
+    if (error_info->error_type == CUSTOM_SPI_ERROR_READ_FAILED ||
+        error_info->error_type == CUSTOM_SPI_ERROR_INVALID_DATA_LEN ||
+        error_info->error_type == CUSTOM_SPI_ERROR_CALLBACK_FAILED) {
+        // 版本28跳过SPI重新初始化逻辑
+        if (g_py_ver == 28) {
+            cat1_print_info("HTTP download: Critical error detected (v28), skipping SPI reinitialization\n");
+            http_file_download_notify_complete(0);  // 通知下载失败
+
+            // 版本28只记录错误,不进行SPI重新初始化
+            return;
+        }
+        
+        if (g_download_state == HTTP_DOWNLOAD_STATE_DOWNLOADING) {
+            cat1_print_err("HTTP download: Critical error detected, reinitializing SPI...\n");
+            
+            // 取消注册回调函数
+            custom_spi_register_data_callback(NULL);
+            custom_spi_register_error_callback(NULL);
+            
+            // 反初始化SPI
+            custom_spi_hw_deinit();
+            cat1_print_info("HTTP download: SPI deinitialized\n");
+            
+            // 重新初始化SPI
+            custom_spi_hw_init();
+            cat1_print_info("HTTP download: SPI reinitialized\n");
+            
+            // 重新注册回调函数
+            custom_spi_register_data_callback(http_download_spi_data_callback);
+            custom_spi_register_error_callback(http_download_spi_error_callback);
+            cat1_print_info("HTTP download: SPI callbacks re-registered\n");
+            //等待重启稳定
+            LOS_Msleep(1000);
+            
+            // SPI重新初始化后,自动进行断点重传
+            if (strlen(g_download_url) > 0 ) {
+                cat1_print_info("HTTP download: Auto-resuming download from byte %u\n", g_total_received_bytes);
+                
+                // 临时重置状态,以便调用重传函数
+                http_download_state_t old_state = g_download_state;
+                g_download_state = HTTP_DOWNLOAD_STATE_IDLE;
+                
+                // 获取文件路径(如果存在)
+                const char *file_path = (strlen(g_download_file_path) > 0) ? g_download_file_path : NULL;
+                
+                // 调用断点重传函数
+                int resume_ret = http_file_download_resume(g_download_url, file_path, g_total_received_bytes, 0);
+                
+                if (resume_ret != 0) {
+                    cat1_print_err("HTTP download: Failed to resume download, error: %d\n", resume_ret);
+                    g_download_state = HTTP_DOWNLOAD_STATE_ERROR;
+                    http_file_download_notify_complete(0);  // 通知下载失败
+                } else {
+                    cat1_print_info("HTTP download: Resume function called successfully\n");
+                }
+            } else {
+                cat1_print_err("HTTP download: Cannot resume, URL or received bytes invalid (URL: %s, bytes: %u)\n", 
+                              g_download_url, g_total_received_bytes);
+                http_file_download_notify_complete(0);  // 通知下载失败
+            }
+        }
+    }
+}
 
 /**
  * @brief SPI数据接收回调函数(用于下载服务)
  * @param data 接收到的数据指针(不包含包头,仅数据部分)
  * @param data_len 数据长度(从包头解析得到)
- * @param packet_num 包序号(从1开始递增)
+ * @param packet_num 包序号(从0开始,从包头中解析得到)
  * @return 0表示成功,非0表示失败
  */
 static int http_download_spi_data_callback(const uint8_t *data, uint32_t data_len, uint32_t packet_num)
@@ -99,9 +245,15 @@ static int http_download_spi_data_callback(const uint8_t *data, uint32_t data_le
         return -1;
     }
     
+    // 重置超时定时器(每次收到数据时重置)
+    if (g_download_state == HTTP_DOWNLOAD_STATE_DOWNLOADING && g_download_timeout_timer.timer != NULL) {
+        osal_timer_stop(&g_download_timeout_timer);
+        osal_timer_start(&g_download_timeout_timer);
+    }
+    
     // 更新统计信息
     g_total_received_bytes += data_len;
-    g_total_received_packets = packet_num;
+    g_total_received_packets ++;
     
     // 写入文件(如果文件已打开)
     if (g_download_file_fd >= 0) {
@@ -184,6 +336,10 @@ int http_file_download_init(http_file_download_data_callback_t callback)
     // 注册SPI数据接收回调函数
     custom_spi_register_data_callback(http_download_spi_data_callback);
     
+    // 注册SPI错误回调函数
+    custom_spi_register_error_callback(http_download_spi_error_callback);
+    cat1_print_info("HTTP file download service: SPI error callback registered\n");
+    
     // 重置状态和统计信息
     g_download_state = HTTP_DOWNLOAD_STATE_IDLE;
     g_total_received_bytes = 0;
@@ -203,10 +359,21 @@ void http_file_download_deinit(void)
     // 取消注册SPI数据回调
     custom_spi_register_data_callback(NULL);
     
+    // 取消注册SPI错误回调
+    custom_spi_register_error_callback(NULL);
+    
     // 反初始化SPI服务
     custom_spi_deinit();
     cat1_print_info("HTTP file download service: SPI deinitialized\n");
     
+    // 停止并销毁超时定时器
+    if (g_download_timeout_timer.timer != NULL) {
+        osal_timer_stop(&g_download_timeout_timer);
+        osal_timer_destroy(&g_download_timeout_timer);
+        g_download_timeout_timer.timer = NULL;
+        cat1_print_info("HTTP file download service: Timeout timer destroyed\n");
+    }
+    
     // 删除信号量
     if (g_download_complete_sem != NULL) {
         osSemaphoreDelete(g_download_complete_sem);
@@ -219,6 +386,7 @@ void http_file_download_deinit(void)
         g_download_file_fd = -1;
     }
     memset(g_download_file_path, 0, sizeof(g_download_file_path));
+    memset(g_download_url, 0, sizeof(g_download_url));
     
     // 重置状态和统计信息
     g_download_state = HTTP_DOWNLOAD_STATE_IDLE;
@@ -261,6 +429,10 @@ int http_file_download_start(const char *url, const char *file_path, uint32_t ti
     
     cat1_print_info("HTTP file download: Starting download from URL: %s\n", url);
     
+    // 保存当前下载的URL(用于断点重传)
+    strncpy(g_download_url, url, sizeof(g_download_url) - 1);
+    g_download_url[sizeof(g_download_url) - 1] = '\0';
+    
     // 关闭之前的文件(如果已打开)
     if (g_download_file_fd >= 0) {
         close(g_download_file_fd);
@@ -273,6 +445,9 @@ int http_file_download_start(const char *url, const char *file_path, uint32_t ti
         // 保存文件路径
         strncpy(g_download_file_path, file_path, sizeof(g_download_file_path) - 1);
         g_download_file_path[sizeof(g_download_file_path) - 1] = '\0';
+
+        cat1_print_info("HTTP file download: No file path provided, data will not be saved\n");
+
         
         // 打开文件(创建或截断)
         g_download_file_fd = open(file_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
@@ -291,6 +466,32 @@ int http_file_download_start(const char *url, const char *file_path, uint32_t ti
     g_total_received_bytes = 0;
     g_total_received_packets = 0;
     
+    // 初始化并启动超时定时器
+    if (g_download_timeout_timer.timer == NULL) {
+        g_download_timeout_timer.handler = http_download_timeout_callback;
+        g_download_timeout_timer.data = 0;
+        g_download_timeout_timer.interval = g_download_timeout_ms;
+        
+        int timer_ret = osal_timer_init(&g_download_timeout_timer);
+        if (timer_ret != OSAL_SUCCESS) {
+            cat1_print_err("HTTP file download: Failed to init timeout timer: 0x%x\n", timer_ret);
+            // 定时器初始化失败不影响下载,继续执行
+        } else {
+            timer_ret = osal_timer_start(&g_download_timeout_timer);
+            if (timer_ret != OSAL_SUCCESS) {
+                cat1_print_err("HTTP file download: Failed to start timeout timer: 0x%x\n", timer_ret);
+                osal_timer_destroy(&g_download_timeout_timer);
+            } else {
+                cat1_print_info("HTTP file download: Timeout timer started (%u ms)\n", g_download_timeout_ms);
+            }
+        }
+    } else {
+        // 定时器已存在,重新启动
+        osal_timer_stop(&g_download_timeout_timer);
+        osal_timer_start(&g_download_timeout_timer);
+        cat1_print_info("HTTP file download: Timeout timer restarted (%u ms)\n", g_download_timeout_ms);
+    }
+    
     // 通过UDP发送下载指令到CAT1(携带URL地址)
     udp_send_msg_toCat1(UDP_CMD_HTTP_FILE_DOWNLOAD, (char *)url);
     
@@ -302,6 +503,12 @@ int http_file_download_start(const char *url, const char *file_path, uint32_t ti
     
     osStatus_t sem_ret = osSemaphoreAcquire(g_download_complete_sem, wait_timeout);
     
+    // 停止超时定时器
+    if (g_download_timeout_timer.timer != NULL) {
+        osal_timer_stop(&g_download_timeout_timer);
+        cat1_print_info("HTTP file download: Timeout timer stopped\n");
+    }
+    
     // 关闭文件(无论成功或失败)
     if (g_download_file_fd >= 0) {
         close(g_download_file_fd);
@@ -342,6 +549,316 @@ int http_file_download_start(const char *url, const char *file_path, uint32_t ti
     }
 }
 
+/**
+ * @brief 开始HTTP文件下载(阻塞接口,支持断点重传)
+ * @param url 要下载的文件URL
+ * @param file_path 保存文件的路径,NULL或空字符串表示不保存文件(仅接收数据)
+ * @param resume_from_byte 断点重传起始字节位置(0表示从头开始下载)
+ * @param timeout_ms 超时时间(毫秒),0表示使用默认值(300秒)
+ * @return 0成功,-1失败
+ * 
+ * 此函数会:
+ * 1. 打开文件(如果提供了文件路径,resume_from_byte>0时以追加模式打开,否则以截断模式打开)
+ * 2. 通过UDP发送下载指令到CAT1(如果resume_from_byte>0,发送URL|resume_from_byte格式,否则只发送URL)
+ * 3. 使用信号量阻塞等待下载完成
+ * 4. 下载完成后关闭文件并返回
+ */
+int http_file_download_start_with_resume(const char *url, const char *file_path, uint32_t resume_from_byte, uint32_t timeout_ms)
+{
+    if (url == NULL || strlen(url) == 0) {
+        cat1_print_err("HTTP file download: Invalid URL\n");
+        return -1;
+    }
+    
+    if (g_download_complete_sem == NULL) {
+        cat1_print_err("HTTP file download: Service not initialized, call http_file_download_init() first\n");
+        return -1;
+    }
+    
+    if (g_download_state == HTTP_DOWNLOAD_STATE_DOWNLOADING) {
+        cat1_print_warning("HTTP file download: Download already in progress\n");
+        return -1;
+    }
+    
+    if (resume_from_byte > 0) {
+        cat1_print_info("HTTP file download: Starting download with resume from URL: %s, byte position: %u\n", url, resume_from_byte);
+    } else {
+        cat1_print_info("HTTP file download: Starting download from URL: %s\n", url);
+    }
+    
+    // 保存当前下载的URL(用于断点重传)
+    strncpy(g_download_url, url, sizeof(g_download_url) - 1);
+    g_download_url[sizeof(g_download_url) - 1] = '\0';
+    
+    // 关闭之前的文件(如果已打开)
+    if (g_download_file_fd >= 0) {
+        close(g_download_file_fd);
+        g_download_file_fd = -1;
+    }
+    memset(g_download_file_path, 0, sizeof(g_download_file_path));
+    
+    // 打开文件(如果提供了文件路径)
+    if (file_path != NULL && strlen(file_path) > 0) {
+        // 保存文件路径
+        strncpy(g_download_file_path, file_path, sizeof(g_download_file_path) - 1);
+        g_download_file_path[sizeof(g_download_file_path) - 1] = '\0';
+        
+        // 根据resume_from_byte决定打开模式
+        if (resume_from_byte > 0) {
+            // 断点重传:以追加模式打开
+            g_download_file_fd = open(file_path, O_WRONLY | O_CREAT | O_APPEND, 0644);
+            if (g_download_file_fd < 0) {
+                cat1_print_err("HTTP file download: Failed to open file '%s' in append mode, errno=%d\n", file_path, errno);
+                return -1;
+            }
+            cat1_print_info("HTTP file download: File opened in append mode: %s\n", file_path);
+        } else {
+            // 从头下载:以截断模式打开
+            g_download_file_fd = open(file_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+            if (g_download_file_fd < 0) {
+                cat1_print_err("HTTP file download: Failed to open file '%s', errno=%d\n", file_path, errno);
+                return -1;
+            }
+            cat1_print_info("HTTP file download: File opened: %s\n", file_path);
+        }
+    } else {
+        cat1_print_info("HTTP file download: No file path provided, data will not be saved\n");
+        g_download_file_fd = -1;
+    }
+    
+
+    
+    // 重置状态和统计信息
+    g_download_state = HTTP_DOWNLOAD_STATE_DOWNLOADING;
+    if (resume_from_byte == 0) {
+        // 从头下载:重置统计信息
+        g_total_received_bytes = 0;
+        g_total_received_packets = 0;
+    } else {
+        // 断点重传:保持现有统计信息(或根据需求重置)
+        // 这里保持现有统计,因为断点重传是继续之前的下载
+    }
+    
+    // 初始化并启动超时定时器
+    if (g_download_timeout_timer.timer == NULL) {
+        g_download_timeout_timer.handler = http_download_timeout_callback;
+        g_download_timeout_timer.data = 0;
+        g_download_timeout_timer.interval = g_download_timeout_ms;
+        
+        int timer_ret = osal_timer_init(&g_download_timeout_timer);
+        if (timer_ret != OSAL_SUCCESS) {
+            cat1_print_err("HTTP file download: Failed to init timeout timer: 0x%x\n", timer_ret);
+            // 定时器初始化失败不影响下载,继续执行
+        } else {
+            timer_ret = osal_timer_start(&g_download_timeout_timer);
+            if (timer_ret != OSAL_SUCCESS) {
+                cat1_print_err("HTTP file download: Failed to start timeout timer: 0x%x\n", timer_ret);
+                osal_timer_destroy(&g_download_timeout_timer);
+            } else {
+                cat1_print_info("HTTP file download: Timeout timer started (%u ms)\n", g_download_timeout_ms);
+            }
+        }
+    } else {
+        // 定时器已存在,重新启动
+        osal_timer_stop(&g_download_timeout_timer);
+        osal_timer_start(&g_download_timeout_timer);
+        cat1_print_info("HTTP file download: Timeout timer restarted (%u ms)\n", g_download_timeout_ms);
+    }
+    
+    // 通过UDP发送下载指令到CAT1
+    if (resume_from_byte > 0) {
+        // 断点重传:发送URL|resume_from_byte格式
+        char udp_msg[1024] = {0};
+        int msg_len = snprintf(udp_msg, sizeof(udp_msg), "%s|%u", url, resume_from_byte);
+        if (msg_len < 0 || msg_len >= (int)sizeof(udp_msg)) {
+            cat1_print_err("HTTP file download: Failed to format UDP message\n");
+            if (g_download_file_fd >= 0) {
+                close(g_download_file_fd);
+                g_download_file_fd = -1;
+            }
+            return -1;
+        }
+        udp_send_msg_toCat1(UDP_CMD_HTTP_FILE_DOWNLOAD, udp_msg);
+        cat1_print_info("HTTP file download: Resume command sent (URL: %s, resume from byte: %u)\n", url, resume_from_byte);
+    } else {
+        // 从头下载:只发送URL
+        udp_send_msg_toCat1(UDP_CMD_HTTP_FILE_DOWNLOAD, (char *)url);
+        cat1_print_info("HTTP file download: Download command sent\n");
+    }
+    
+    cat1_print_info("HTTP file download: Waiting for completion...\n");
+    
+    // 使用信号量阻塞等待下载完成
+    // 默认超时时间:300秒(如果timeout_ms为0)
+    uint32_t wait_timeout = (timeout_ms == 0) ? (300 * 1000) : timeout_ms;
+    
+    osStatus_t sem_ret = osSemaphoreAcquire(g_download_complete_sem, wait_timeout);
+    
+    // 停止超时定时器
+    if (g_download_timeout_timer.timer != NULL) {
+        osal_timer_stop(&g_download_timeout_timer);
+        cat1_print_info("HTTP file download: Timeout timer stopped\n");
+    }
+    
+    // 关闭文件(无论成功或失败)
+    if (g_download_file_fd >= 0) {
+        close(g_download_file_fd);
+        g_download_file_fd = -1;
+        cat1_print_info("HTTP file download: File closed: %s\n", g_download_file_path);
+    }
+    
+    if (sem_ret == osOK) {
+        // 下载完成
+        if (g_download_state == HTTP_DOWNLOAD_STATE_COMPLETED) {
+            cat1_print_info("HTTP file download: Completed successfully\n");
+            cat1_print_info("HTTP file download: Total received: %u bytes, %u packets\n", 
+                           g_total_received_bytes, g_total_received_packets);
+            if (strlen(g_download_file_path) > 0) {
+                cat1_print_info("HTTP file download: File saved to: %s\n", g_download_file_path);
+            }
+            g_download_state = HTTP_DOWNLOAD_STATE_IDLE;
+            return 0;
+        } else if (g_download_state == HTTP_DOWNLOAD_STATE_ERROR) {
+            cat1_print_err("HTTP file download: Failed with error\n");
+            g_download_state = HTTP_DOWNLOAD_STATE_IDLE;
+            return -1;
+        } else {
+            cat1_print_warning("HTTP file download: Unexpected state: %d\n", g_download_state);
+            g_download_state = HTTP_DOWNLOAD_STATE_IDLE;
+            return -1;
+        }
+    } else if (sem_ret == osErrorTimeout) {
+        // 超时
+        cat1_print_err("HTTP file download: Timeout after %u ms\n", wait_timeout);
+        g_download_state = HTTP_DOWNLOAD_STATE_ERROR;
+        return -1;
+    } else {
+        // 信号量错误
+        cat1_print_err("HTTP file download: Semaphore error: %d\n", sem_ret);
+        g_download_state = HTTP_DOWNLOAD_STATE_ERROR;
+        return -1;
+    }
+}
+
+/**
+ * @brief 开始HTTP文件断点重传(非阻塞接口)
+ * @param url 要下载的文件URL
+ * @param file_path 保存文件的路径,NULL或空字符串表示不保存文件(仅接收数据)
+ * @param resume_from_byte 断点重传起始字节位置(从该位置继续下载)
+ * @param timeout_ms 超时时间(毫秒),此参数已废弃,保留用于兼容性
+ * @return 0成功,-1失败
+ * 
+ * 此函数会:
+ * 1. 打开文件(如果提供了文件路径,以追加模式打开)
+ * 2. 设置期望的包序号(基于重传位置)
+ * 3. 通过UDP发送断点重传指令到CAT1(携带URL地址和重传位置)
+ * 4. 立即返回(非阻塞模式)
+ * 
+ * 注意:
+ * - 此函数不会阻塞等待下载完成
+ * - 文件会在下载完成或出错时由回调函数自动关闭
+ * - 可以通过 http_file_download_get_state() 查询下载状态
+ */
+int http_file_download_resume(const char *url, const char *file_path, uint32_t resume_from_byte, uint32_t timeout_ms)
+{
+    if (url == NULL || strlen(url) == 0) {
+        cat1_print_err("HTTP file download resume: Invalid URL\n");
+        return -1;
+    }
+    
+    if (g_download_complete_sem == NULL) {
+        cat1_print_err("HTTP file download resume: Service not initialized, call http_file_download_init() first\n");
+        return -1;
+    }
+    
+    if (g_download_state == HTTP_DOWNLOAD_STATE_DOWNLOADING) {
+        cat1_print_warning("HTTP file download resume: Download already in progress\n");
+        return -1;
+    }
+    
+    cat1_print_info("HTTP file download resume: Resuming download from URL: %s, byte position: %u file_path %s\n", url, resume_from_byte,file_path);
+    
+    // 保存当前下载的URL(用于断点重传)
+    strncpy(g_download_url, url, sizeof(g_download_url) - 1);
+    g_download_url[sizeof(g_download_url) - 1] = '\0';
+    
+    // 关闭之前的文件(如果已打开)
+    if (g_download_file_fd >= 0) {
+        close(g_download_file_fd);
+        g_download_file_fd = -1;
+    }
+    // 打开文件(如果提供了文件路径,以追加模式打开)
+    if (file_path != NULL && strlen(file_path) > 0) {
+        // 保存文件路径
+        // 打开文件(追加模式,用于断点重传)
+        g_download_file_fd = open(file_path, O_WRONLY | O_CREAT | O_APPEND, 0644);
+        if (g_download_file_fd < 0) {
+            cat1_print_err("HTTP file download resume: Failed to open file '%s', errno=%d\n", file_path, errno);
+            return -1;
+        }
+        cat1_print_info("HTTP file download resume: File opened in append mode: %s\n", file_path);
+    } else {
+        cat1_print_info("HTTP file download resume: No file path provided, data will not be saved\n");
+        g_download_file_fd = -1;
+    }
+    
+    // 计算期望的起始包序号(基于重传位置)
+    uint32_t resume_from_packet = resume_from_byte / SPI_PACKET_DATA_SIZE;
+    
+    // 重置状态和统计信息(新的下载会话)
+    g_download_state = HTTP_DOWNLOAD_STATE_DOWNLOADING;
+    // g_total_received_bytes = 0;  // 重置为0,本次会话的接收字节数
+    // g_total_received_packets = 0;  // 重置为0,本次会话的接收包数
+    
+    // 初始化并启动超时定时器
+    if (g_download_timeout_timer.timer == NULL) {
+        g_download_timeout_timer.handler = http_download_timeout_callback;
+        g_download_timeout_timer.data = 0;
+        g_download_timeout_timer.interval = g_download_timeout_ms;
+        
+        int timer_ret = osal_timer_init(&g_download_timeout_timer);
+        if (timer_ret != OSAL_SUCCESS) {
+            cat1_print_err("HTTP file download resume: Failed to init timeout timer: 0x%x\n", timer_ret);
+            // 定时器初始化失败不影响下载,继续执行
+        } else {
+            timer_ret = osal_timer_start(&g_download_timeout_timer);
+            if (timer_ret != OSAL_SUCCESS) {
+                cat1_print_err("HTTP file download resume: Failed to start timeout timer: 0x%x\n", timer_ret);
+                osal_timer_destroy(&g_download_timeout_timer);
+            } else {
+                cat1_print_info("HTTP file download resume: Timeout timer started (%u ms)\n", g_download_timeout_ms);
+            }
+        }
+    } else {
+        // 定时器已存在,重新启动
+        osal_timer_stop(&g_download_timeout_timer);
+        osal_timer_start(&g_download_timeout_timer);
+        cat1_print_info("HTTP file download resume: Timeout timer restarted (%u ms)\n", g_download_timeout_ms);
+    }
+    
+    // 格式化UDP消息:URL|resume_from_byte
+    char udp_msg[1024] = {0};
+    int msg_len = snprintf(udp_msg, sizeof(udp_msg), "%s|%u", url, resume_from_byte);
+    if (msg_len < 0 || msg_len >= (int)sizeof(udp_msg)) {
+        cat1_print_err("HTTP file download resume: Failed to format UDP message\n");
+        if (g_download_file_fd >= 0) {
+            close(g_download_file_fd);
+            g_download_file_fd = -1;
+        }
+        return -1;
+    }
+    
+    // 通过UDP发送断点重传指令到CAT1(携带URL地址和重传位置)
+    udp_send_msg_toCat1(UDP_CMD_HTTP_FILE_DOWNLOAD, udp_msg);
+    
+    cat1_print_info("HTTP file download resume: Resume command sent (URL: %s, resume from byte: %u), non-blocking mode\n", 
+                   url, resume_from_byte);
+    
+    // 非阻塞模式:发送命令后立即返回,不等待下载完成
+    // 文件保持打开状态,直到下载完成或出错时由回调函数关闭
+    return 0;
+}
+
 /**
  * @brief 通知下载完成(由外部调用,例如从CAT1收到完成消息时)
  * @param success 是否成功(1=成功,0=失败)
@@ -359,6 +876,11 @@ void http_file_download_notify_complete(int success)
         return;
     }
     
+    // 停止超时定时器
+    if (g_download_timeout_timer.timer != NULL) {
+        osal_timer_stop(&g_download_timeout_timer);
+    }
+    
     // 设置完成状态
     if (success) {
         g_download_state = HTTP_DOWNLOAD_STATE_COMPLETED;
@@ -369,8 +891,19 @@ void http_file_download_notify_complete(int success)
         cat1_print_err("HTTP file download: Download failed\n");
     }
     
-    // 释放信号量,唤醒等待的线程
-    osSemaphoreRelease(g_download_complete_sem);
+    // 关闭文件(如果已打开)
+    if (g_download_file_fd >= 0) {
+        close(g_download_file_fd);
+        g_download_file_fd = -1;
+        if (strlen(g_download_file_path) > 0) {
+            cat1_print_info("HTTP file download: File closed: %s\n", g_download_file_path);
+        }
+    }
+    
+    // 释放信号量,唤醒等待的线程(如果有线程在等待)
+    if (g_download_complete_sem != NULL) {
+        osSemaphoreRelease(g_download_complete_sem);
+    }
 }
 
 /**
index 0ee4201..b1aa713 100644 (file)
@@ -28,7 +28,7 @@ typedef enum {
  * @brief HTTP下载数据接收回调函数类型
  * @param data 接收到的数据指针(不包含包头,仅数据部分)
  * @param data_len 数据长度(从包头解析得到)
- * @param packet_num 包序号(从1开始递增)
+ * @param packet_num 包序号(从0开始,从包头中解析得到)
  * @return 0表示成功,非0表示失败
  */
 typedef int (*http_file_download_data_callback_t)(const uint8_t *data, uint32_t data_len, uint32_t packet_num);
@@ -60,6 +60,43 @@ void http_file_download_deinit(void);
  */
 int http_file_download_start(const char *url, const char *file_path, uint32_t timeout_ms);
 
+/**
+ * @brief 开始HTTP文件下载(阻塞接口,支持断点重传)
+ * @param url 要下载的文件URL
+ * @param file_path 保存文件的路径,NULL或空字符串表示不保存文件(仅接收数据)
+ * @param resume_from_byte 断点重传起始字节位置(0表示从头开始下载)
+ * @param timeout_ms 超时时间(毫秒),0表示使用默认值(300秒)
+ * @return 0成功,-1失败
+ * 
+ * 此函数会:
+ * 1. 打开文件(如果提供了文件路径,resume_from_byte>0时以追加模式打开,否则以截断模式打开)
+ * 2. 通过UDP发送下载指令到CAT1(如果resume_from_byte>0,发送URL|resume_from_byte格式,否则只发送URL)
+ * 3. 使用信号量阻塞等待下载完成
+ * 4. 下载完成后关闭文件并返回
+ */
+int http_file_download_start_with_resume(const char *url, const char *file_path, uint32_t resume_from_byte, uint32_t timeout_ms);
+
+/**
+ * @brief 开始HTTP文件断点重传(非阻塞接口)
+ * @param url 要下载的文件URL
+ * @param file_path 保存文件的路径,NULL或空字符串表示不保存文件(仅接收数据)
+ * @param resume_from_byte 断点重传起始字节位置(从该位置继续下载)
+ * @param timeout_ms 超时时间(毫秒),此参数已废弃,保留用于兼容性
+ * @return 0成功,-1失败
+ * 
+ * 此函数会:
+ * 1. 打开文件(如果提供了文件路径,以追加模式打开)
+ * 2. 设置期望的包序号(基于重传位置)
+ * 3. 通过UDP发送断点重传指令到CAT1(携带URL地址和重传位置)
+ * 4. 立即返回(非阻塞模式)
+ * 
+ * 注意:
+ * - 此函数不会阻塞等待下载完成
+ * - 文件会在下载完成或出错时由回调函数自动关闭
+ * - 可以通过 http_file_download_get_state() 查询下载状态
+ */
+int http_file_download_resume(const char *url, const char *file_path, uint32_t resume_from_byte, uint32_t timeout_ms);
+
 /**
  * @brief 通知下载完成(由外部调用,例如从CAT1收到完成消息时)
  * @param success 是否成功(1=成功,0=失败)
index 83db0ff..9b26583 100644 (file)
@@ -23,6 +23,8 @@ set(SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_web_ack.c
     ${CMAKE_CURRENT_SOURCE_DIR}/ln_file_recv_task.c
     ${CMAKE_CURRENT_SOURCE_DIR}/ln_bluetooth_api.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/ln_websoket_api_ext.c
+    
 )
 
 # 公共头文件路径(供其他模块引用)
index acf62a9..de67e5d 100644 (file)
@@ -28,6 +28,9 @@ static app_task_definition_t ln_send_taskcfg = {0};
 static unsigned long g_ln_send_queue = 0xFFFFFFFF;
 #define LN_HTTP_BOARD_TASK_SIZE 0x2500
 
+/* HTTP 文件下载阻塞实现(由发送线程调用) */
+extern int ln_http_download_file_do(const char *url, uint32_t timeout_ms);
+
 static char *s_cached_device_id = NULL;
 char *g_device_token = NULL; // 设备token,服务器获取
 static char *s_cached_device_iccid = NULL;
@@ -1074,6 +1077,9 @@ void ln_ws_process_message(const ws_data_msg_t *data_msg, ws_command_t type)
         if (data_msg && data_msg->finished) {
             push_story_response_finished_notification(true);
         }
+
+        ln_http_download_file(data_msg->audio_url, 100*1000);
+
          break;
     }
     case WS_CMD_RANDOM_STORY_RESPONSE:
@@ -1083,6 +1089,9 @@ void ln_ws_process_message(const ws_data_msg_t *data_msg, ws_command_t type)
         if (data_msg && data_msg->finished) {
             push_story_response_finished_notification(true);
         }
+
+        ln_http_download_file(data_msg->audio_url, 100*1000);
+
         break;
     }
     case WS_CMD_AUDIO_PLAYBACK_START:
@@ -1234,6 +1243,20 @@ static void ln_http_server_send_task(void const *arg)
             printf("LN_HTTP_SERVICE_CMD_OTA_DOWNLOAD:下载OTA升级包\n");
             ln_ota_download_flow_http(uapi_ln_get_device_id(), uapi_ln_get_device_token(), uapi_ln_get_device_version());
         }
+        else if (mail.cmd == LN_HTTP_SERVICE_CMD_HTTP_DOWNLOAD)
+        {
+            if (mail.data && mail.len >= sizeof(ln_http_download_mail_t))
+            {
+                ln_http_download_mail_t req = {0};
+                memcpy(&req, mail.data, sizeof(req));
+                req.url[sizeof(req.url) - 1] = '\0';
+                ln_http_download_file_do(req.url, req.timeout_ms);
+            }
+            else
+            {
+                WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[LN_HTTP_SERVICE_CMD_HTTP_DOWNLOAD] invalid mail data");
+            }
+        }
         else if (mail.cmd == LN_HTTP_SERVICE_CMD_DOWNLOAD_EMOJI)
         {
             WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "LN_HTTP_SERVICE_CMD_DOWNLOAD_EMOJI:下载表情任务请求\n");
index 3556a86..d39e8ca 100644 (file)
@@ -144,6 +144,9 @@ typedef struct
 /* 语音对话流相关命令 */
 #define LN_HTTP_SERVICE_CMD_CHAT_SESSION_START 0x1016 /* 语音对话流开始 */
 #define LN_HTTP_SERVICE_CMD_CHAT_SESSION_END 0x1017 /* 语音对话流结束 */
+#define LN_HTTP_SERVICE_CMD_HTTP_DOWNLOAD 0x101A /* HTTP文件下载(队列异步调度) */
+
+
 
 /* -------------------------------------------------------------------------- */
 extern char g_ln_last_package_url_new1[256];
@@ -162,6 +165,13 @@ typedef struct
     char *save_path; /* 保存路径,使用malloc分配,接收方负责free */
 } ln_download_file_req_t;
 
+/* HTTP下载任务邮件体(队列传输用,固定大小避免二次分配) */
+typedef struct
+{
+    uint32_t timeout_ms;   /* 超时时间 */
+    char url[512];         /* 下载URL,保证以'\0'结尾 */
+} ln_http_download_mail_t;
+
 /* 故事-随机故事 请求体(可选主题)*/
 typedef struct
 {
@@ -552,6 +562,65 @@ bool ln_ws_is_vad_server_enabled(void); /* 获取 VAD 服务器启用状态 */
 bool ln_ws_is_vad_server_initialized(void); /* 获取 VAD 服务器初始化状态 */
 bool ln_ws_is_vad_server_auth_success(void); /* 获取 VAD 服务器认证成功状态 */
 
+/* ==================== HTTP文件下载接口(基于SPI传输) ==================== */
+/**
+ * @brief 初始化HTTP文件下载服务
+ * @return 0成功,-1失败
+ * 
+ * 注意:数据通过内部回调函数处理,不写入文件。
+ */
+int ln_http_download_init(void);
+
+/**
+ * @brief 反初始化HTTP文件下载服务
+ */
+void ln_http_download_deinit(void);
+
+/**
+ * @brief 下载指定文件(异步接口,通过发送队列在 ln_http_server_send_task 中执行)
+ * @param url 要下载的文件URL(必需,不能为NULL或空字符串)
+ * @param timeout_ms 超时时间(毫秒),0表示使用默认值(300秒)
+ * @return 0成功投递,-1参数/投递失败
+ * 
+ * 注意:数据不会写入文件,通过内部回调函数处理。
+ */
+int ln_http_download_file(const char *url, uint32_t timeout_ms);
+
+/* 队列消费线程调用的阻塞实现,外部不要直接调用 */
+int ln_http_download_file_do(const char *url, uint32_t timeout_ms);
+
+/**
+ * @brief 下载指定文件(支持断点重传,阻塞接口,不写入文件,通过内部回调处理数据)
+ * @param url 要下载的文件URL(必需,不能为NULL或空字符串)
+ * @param resume_from_byte 断点重传起始字节位置(0表示从头开始下载)
+ * @param timeout_ms 超时时间(毫秒),0表示使用默认值(300秒)
+ * @return 0成功,-1失败
+ * 
+ * 注意:数据不会写入文件,通过内部回调函数处理。
+ */
+int ln_http_download_file_with_resume(const char *url, uint32_t resume_from_byte, uint32_t timeout_ms);
+
+/**
+ * @brief 获取下载服务状态
+ * @return 当前下载状态
+ */
+http_download_state_t ln_http_download_get_state(void);
+
+/**
+ * @brief 获取下载统计信息
+ * @param total_bytes 输出参数:总接收字节数(可以为NULL)
+ * @param total_packets 输出参数:总接收包数(可以为NULL)
+ * @return 0成功,-1失败
+ */
+int ln_http_download_get_stats(uint32_t *total_bytes, uint32_t *total_packets);
+
+/**
+ * @brief 检查下载服务是否已初始化
+ * @return 1已初始化,0未初始化
+ */
+uint8_t ln_http_download_is_initialized(void);
+/* ====================================================================== */
+
 #ifdef __cplusplus
 }
 #endif
index 3619f18..159391a 100644 (file)
@@ -194,9 +194,9 @@ static bool g_chat_id_matched = false;    // 当前对话ID是否匹配
 
 // 音频播放计时相关变量
 static uint32_t g_last_audio_write_tick = 0;  // 最后一次音频写入的tick
-static bool g_audio_playback_active = false;  // 音频播放是否活跃
+bool g_audio_playback_active = false;  // 音频播放是否活跃
 static uint32_t g_audio_timeout_ticks = 1500; // 音频超时tick数(默认1.5秒)
-static uint32_t g_last_binary_data_tick = 0;  // 最后一次接收二进制数据的tick
+uint32_t g_last_binary_data_tick = 0;  // 最后一次接收二进制数据的tick
 
 // 音频数据最近缓存(环形缓冲区,仅保留最近数据)
 #define AUDIO_CACHE_BUFFER_SIZE  (1024*3)  // 8KB缓存
@@ -208,7 +208,7 @@ static uint32_t g_audio_playback_start_tick = 0;  // 音频播放开始时间tic
 
 // VAD冷却时间相关变量
 static uint32_t g_vad_cooldown_start_tick = 0;  // VAD冷却开始时间tick
-static bool g_vad_cooldown_active = false;       // VAD冷却是否激活
+bool g_vad_cooldown_active = false;       // VAD冷却是否激活
 static uint32_t g_vad_cooldown_duration_ms = 10000; // VAD冷却持续时间(毫秒)
 
 // 音频时长记录循环数组
@@ -229,7 +229,7 @@ static void generate_chat_id(char *chat_id, size_t size);
 static void generate_message_id(char *message_id, size_t size, const char *chat_id, uint32_t counter);
 static void reset_chat_id_match(void);
 static void set_chat_id_matched(void);
-static bool is_chat_id_matched(void);
+bool is_chat_id_matched(void);
 static bool is_active_chat_response(const char *chat_id, const char *response_id);
 static void ln_ws_audio_write_callback(uint32_t size);
 static bool ln_ws_is_vad_cooldown_active(void);
@@ -334,7 +334,7 @@ static void ln_ws_event_callback(ws_event_type_t event_type)
 static void ln_ws_url_callback(char *url, size_t url_size, char *header_buf, size_t header_size)
 {
 #if LN_ENV_PRODUCTION
-    const char *base = "ws://ambybox-o.api.leiniao.com/ambybox-device-api/v1/websocket"; 
+const char *base = "ws://ambybox-o.api.leiniao.com/ambybox-device-api/v1/websocket"; 
 #else
     const char *base = "ws://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket"; 
 #endif
@@ -475,7 +475,7 @@ static void set_chat_id_matched(void)
 }
 
 // 检查对话ID是否匹配
-static bool is_chat_id_matched(void)
+bool is_chat_id_matched(void)
 {
     return g_chat_id_matched;
 }
@@ -1317,6 +1317,9 @@ void ln_ws_audio_init(bool is_vad_server_enabled)
     #endif
     uapi_cat1_add_cat1_vote();
 
+    ln_http_download_init();
+
+
     old_Start_AIVoice(AIVoiceDataCallback_ln,NULL,&audioParams_in,&audioParams_out,&audioParams_in_vad,false);
     
     // 设置初始化标志位
@@ -1377,6 +1380,9 @@ void ln_ws_audio_deinit(void)
     cat1_api_keep_wakeup(0);
     cat1_api_wakeup_cat1(0);
     #endif
+
+    ln_http_download_deinit();
+
     uapi_cat1_remove_cat1_vote();
 
     g_audio_playback_active = false;
@@ -1633,6 +1639,12 @@ int ln_ws_send_random_story_request(const char *story_theme)
     
     cJSON_AddStringToObject(data, "chatId", g_audio_chat_ctx.chat_id);
     cJSON_AddStringToObject(data, "storyTheme", story_theme);
+    
+    // 只有当HTTP下载服务正确初始化后才添加returnBytes字段
+    if (ln_http_download_is_initialized()) {
+        cJSON_AddBoolToObject(data, "returnBytes", false);
+    }
+
     cJSON_AddItemToObject(json, "data", data);
     
     // 转换为字符串
diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_websoket_api_ext.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_websoket_api_ext.c
new file mode 100644 (file)
index 0000000..8b782dc
--- /dev/null
@@ -0,0 +1,372 @@
+/**
+ * @file ln_websoket_api_ext.c
+ * @brief WebSocket API扩展接口 - HTTP文件下载功能
+ * 
+ * 提供基于SPI传输的HTTP文件下载接口,使用http_file_download_client模块
+ */
+
+#include "ln_api.h"
+#include "http_file_download_client.h"
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include "wearable_log.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* 下载服务初始化状态标志 */
+static uint8_t g_download_service_initialized = 0;
+
+/* Python服务版本号声明 */
+extern uint8_t g_py_ver;  /* Python服务版本号 */
+
+/* 版本检查:需要g_py_ver >= 29才能使用HTTP文件下载功能 */
+#define LN_HTTP_DOWNLOAD_MIN_PY_VER 29
+
+extern uint32_t g_last_binary_data_tick;
+extern bool g_vad_cooldown_active;
+extern bool g_audio_playback_active;
+extern bool is_chat_id_matched(void);
+/**
+ * @brief 内部数据回调函数(static)
+ * @param data 接收到的数据指针
+ * @param data_len 数据长度
+ * @param packet_num 包序号
+ * @return 0成功,-1失败
+ * 
+ * 此函数处理从SPI接收到的下载数据
+ * 目前仅记录日志,可根据需要扩展数据处理逻辑
+ */
+static int ln_http_download_data_callback(const uint8_t *data, uint32_t data_len, uint32_t packet_num)
+{
+    // 可以在这里添加数据处理逻辑
+    // 例如:写入文件、转发到其他模块等
+    
+    // 每100个包记录一次日志
+    if (packet_num % 100 == 0) {
+        WEARABLE_LOGD(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Received packet %u, data_len=%u", packet_num, data_len);
+    }
+    // 记录接收数据的时间
+    g_last_binary_data_tick = osKernelGetTickCount();
+    g_vad_cooldown_active = false;
+    
+    if (!is_chat_id_matched() && g_audio_playback_active) {
+        WEARABLE_LOGD(WEARABLE_LOG_MODULE_APP,"Chat ID not matched\n");
+        return 0;
+    }
+
+    if (!data || data_len == 0) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP,"Chat ID not matched\n");
+        return 0;
+    }
+
+    AI_SendAudioDataToQueue(data, data_len);
+    return 0;
+}
+
+/**
+ * @brief 初始化HTTP文件下载服务
+ * @return 0成功,-1失败
+ * 
+ * 此函数会初始化SPI传输和下载服务,只需要调用一次
+ * 数据通过内部回调函数处理,不写入文件
+ * 
+ * 注意:需要g_py_ver >= 29才能使用此功能
+ */
+int ln_http_download_init(void)
+{
+    // 版本检查:需要g_py_ver >= 29
+    if (g_py_ver < LN_HTTP_DOWNLOAD_MIN_PY_VER) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Python version %u is too low, requires >= %d", 
+                     g_py_ver, LN_HTTP_DOWNLOAD_MIN_PY_VER);
+        return -1;
+    }
+    
+    if (g_download_service_initialized) {
+        WEARABLE_LOGW(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Download service already initialized");
+        return 0;
+    }
+    
+    int ret = http_file_download_init(ln_http_download_data_callback);
+    if (ret == 0) {
+        g_download_service_initialized = 1;
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Download service initialized successfully");
+    } else {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Failed to initialize download service");
+    }
+    
+    return ret;
+}
+
+/**
+ * @brief 反初始化HTTP文件下载服务
+ * 
+ * 释放下载服务资源,包括SPI传输资源
+ * 
+ * 注意:需要g_py_ver >= 29才能使用此功能
+ */
+void ln_http_download_deinit(void)
+{
+    // 版本检查:需要g_py_ver >= 29
+    if (g_py_ver < LN_HTTP_DOWNLOAD_MIN_PY_VER) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Python version %u is too low, requires >= %d", 
+                     g_py_ver, LN_HTTP_DOWNLOAD_MIN_PY_VER);
+        return;
+    }
+    
+    if (!g_download_service_initialized) {
+        WEARABLE_LOGW(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Download service not initialized");
+        return;
+    }
+    
+    http_file_download_deinit();
+    g_download_service_initialized = 0;
+    WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Download service deinitialized");
+}
+
+/**
+ * @brief 下载指定文件(阻塞接口,不写入文件,通过内部回调处理数据)
+ * @param url 要下载的文件URL(必需,不能为NULL或空字符串)
+ * @param timeout_ms 超时时间(毫秒),0表示使用默认值(300秒)
+ * @return 0成功,-1失败
+ * 
+ * 此函数会:
+ * 1. 检查下载服务是否已初始化,如果未初始化则自动初始化
+ * 2. 调用http_file_download_start下载文件(file_path为NULL,不写入文件)
+ * 3. 数据通过内部回调函数处理
+ * 4. 阻塞等待下载完成
+ * 
+ * 注意:
+ * - 数据不会写入文件,通过内部回调函数处理
+ * 
+ * 示例:
+ *   // 先初始化
+ *   ln_http_download_init();
+ *   // 然后下载
+ *   int ret = ln_http_download_file("https://example.com/file.zip", 300000);
+ */
+/* 阻塞实现:在发送线程中调用 */
+int ln_http_download_file_do(const char *url, uint32_t timeout_ms)
+{
+    // 版本检查:需要g_py_ver >= 29
+    if (g_py_ver < LN_HTTP_DOWNLOAD_MIN_PY_VER) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Python version %u is too low, requires >= %d", 
+                     g_py_ver, LN_HTTP_DOWNLOAD_MIN_PY_VER);
+        return -1;
+    }
+    
+    if (url == NULL || strlen(url) == 0) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Invalid URL parameter");
+        return -1;
+    }
+    
+    // 如果未初始化,自动初始化
+    if (!g_download_service_initialized) {
+        WEARABLE_LOGW(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Service not initialized, auto-initializing...");
+        if (ln_http_download_init() != 0) {
+            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Auto-initialization failed");
+            return -1;
+        }
+    }
+    
+    WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Starting download: URL=%s, timeout=%u ms (data via internal callback, no file write)", 
+                  url, timeout_ms);
+    
+    // file_path传入NULL,不写入文件,数据通过内部回调处理
+    int ret = http_file_download_start(url, NULL, timeout_ms);
+    
+    if (ret == 0) {
+        // 获取下载统计信息
+        uint32_t total_bytes = 0;
+        uint32_t total_packets = 0;
+        if (http_file_download_get_stats(&total_bytes, &total_packets) == 0) {
+            WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Download completed: %u bytes, %u packets", 
+                         total_bytes, total_packets);
+        } else {
+            WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Download completed");
+        }
+    } else {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Download failed: URL=%s", url);
+    }
+    
+    return ret;
+}
+
+/* 对外接口:异步投递到 ln_http_server_send_task,避免阻塞调用线程 */
+int ln_http_download_file(const char *url, uint32_t timeout_ms)
+{
+    // 版本检查:需要g_py_ver >= 29
+    if (g_py_ver < LN_HTTP_DOWNLOAD_MIN_PY_VER) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Python version %u is too low, requires >= %d", 
+                     g_py_ver, LN_HTTP_DOWNLOAD_MIN_PY_VER);
+        return -1;
+    }
+    
+    if (url == NULL || strlen(url) == 0) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Invalid URL parameter");
+        return -1;
+    }
+
+    ln_http_download_mail_t mail = {0};
+    mail.timeout_ms = timeout_ms;
+
+    int n = snprintf(mail.url, sizeof(mail.url), "%s", url);
+    if (n < 0 || n >= (int)sizeof(mail.url)) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] URL too long, len=%d", n);
+        return -1;
+    }
+
+    do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_HTTP_DOWNLOAD,
+                                (char *)&mail, sizeof(mail));
+    WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Enqueued download task: URL=%s, timeout=%u ms",
+                  mail.url, timeout_ms);
+    return 0;
+}
+
+/**
+ * @brief 下载指定文件(支持断点重传,阻塞接口,不写入文件,通过内部回调处理数据)
+ * @param url 要下载的文件URL(必需,不能为NULL或空字符串)
+ * @param resume_from_byte 断点重传起始字节位置(0表示从头开始下载)
+ * @param timeout_ms 超时时间(毫秒),0表示使用默认值(300秒)
+ * @return 0成功,-1失败
+ * 
+ * 此函数会:
+ * 1. 检查下载服务是否已初始化,如果未初始化则自动初始化
+ * 2. 调用http_file_download_start_with_resume下载文件(file_path为NULL,不写入文件)
+ * 3. 数据通过内部回调函数处理
+ * 4. 阻塞等待下载完成
+ * 
+ * 注意:
+ * - 数据不会写入文件,通过内部回调函数处理
+ * - 断点重传时,内部回调函数会从resume_from_byte位置开始接收数据
+ * 
+ * 示例:
+ *   // 先初始化
+ *   ln_http_download_init();
+ *   // 从头开始下载
+ *   int ret = ln_http_download_file_with_resume("https://example.com/file.zip", 0, 300000);
+ *   // 从第1024字节开始断点重传
+ *   int ret = ln_http_download_file_with_resume("https://example.com/file.zip", 1024, 300000);
+ */
+int ln_http_download_file_with_resume(const char *url, uint32_t resume_from_byte, uint32_t timeout_ms)
+{
+    // 版本检查:需要g_py_ver >= 29
+    if (g_py_ver < LN_HTTP_DOWNLOAD_MIN_PY_VER) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Python version %u is too low, requires >= %d", 
+                     g_py_ver, LN_HTTP_DOWNLOAD_MIN_PY_VER);
+        return -1;
+    }
+    
+    if (url == NULL || strlen(url) == 0) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Invalid URL parameter");
+        return -1;
+    }
+    
+    // 如果未初始化,自动初始化
+    if (!g_download_service_initialized) {
+        WEARABLE_LOGW(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Service not initialized, auto-initializing...");
+        if (ln_http_download_init() != 0) {
+            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Auto-initialization failed");
+            return -1;
+        }
+    }
+    
+    if (resume_from_byte > 0) {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Starting download with resume: URL=%s, resume_from_byte=%u, timeout=%u ms (data via internal callback, no file write)", 
+                      url, resume_from_byte, timeout_ms);
+    } else {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Starting download: URL=%s, timeout=%u ms (data via internal callback, no file write)", 
+                      url, timeout_ms);
+    }
+    
+    // file_path传入NULL,不写入文件,数据通过内部回调处理
+    int ret = http_file_download_start_with_resume(url, NULL, resume_from_byte, timeout_ms);
+    
+    if (ret == 0) {
+        // 获取下载统计信息
+        uint32_t total_bytes = 0;
+        uint32_t total_packets = 0;
+        if (http_file_download_get_stats(&total_bytes, &total_packets) == 0) {
+            WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Download completed: %u bytes, %u packets", 
+                         total_bytes, total_packets);
+        } else {
+            WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Download completed");
+        }
+    } else {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Download failed: URL=%s", url);
+    }
+    
+    return ret;
+}
+
+/**
+ * @brief 获取下载服务状态
+ * @return 当前下载状态
+ * 
+ * 返回值:
+ * - HTTP_DOWNLOAD_STATE_IDLE: 空闲状态
+ * - HTTP_DOWNLOAD_STATE_DOWNLOADING: 下载中
+ * - HTTP_DOWNLOAD_STATE_COMPLETED: 下载完成
+ * - HTTP_DOWNLOAD_STATE_ERROR: 下载错误
+ */
+http_download_state_t ln_http_download_get_state(void)
+{
+    // 版本检查:需要g_py_ver >= 29
+    if (g_py_ver < LN_HTTP_DOWNLOAD_MIN_PY_VER) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Python version %u is too low, requires >= %d", 
+                     g_py_ver, LN_HTTP_DOWNLOAD_MIN_PY_VER);
+        return HTTP_DOWNLOAD_STATE_IDLE;
+    }
+    
+    if (!g_download_service_initialized) {
+        return HTTP_DOWNLOAD_STATE_IDLE;
+    }
+    return http_file_download_get_state();
+}
+
+/**
+ * @brief 获取下载统计信息
+ * @param total_bytes 输出参数:总接收字节数(可以为NULL)
+ * @param total_packets 输出参数:总接收包数(可以为NULL)
+ * @return 0成功,-1失败
+ */
+int ln_http_download_get_stats(uint32_t *total_bytes, uint32_t *total_packets)
+{
+    // 版本检查:需要g_py_ver >= 29
+    if (g_py_ver < LN_HTTP_DOWNLOAD_MIN_PY_VER) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Python version %u is too low, requires >= %d", 
+                     g_py_ver, LN_HTTP_DOWNLOAD_MIN_PY_VER);
+        return -1;
+    }
+    
+    if (!g_download_service_initialized) {
+        WEARABLE_LOGW(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Service not initialized");
+        return -1;
+    }
+    return http_file_download_get_stats(total_bytes, total_packets);
+}
+
+/**
+ * @brief 检查下载服务是否已初始化
+ * @return 1已初始化,0未初始化
+ * 
+ * 注意:需要g_py_ver >= 29才能使用此功能
+ */
+uint8_t ln_http_download_is_initialized(void)
+{
+    // 版本检查:需要g_py_ver >= 29
+    if (g_py_ver < LN_HTTP_DOWNLOAD_MIN_PY_VER) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "[HTTP_DL] Python version %u is too low, requires >= %d", 
+                     g_py_ver, LN_HTTP_DOWNLOAD_MIN_PY_VER);
+        return 0;
+    }
+    
+    return g_download_service_initialized;
+}
+
+#ifdef __cplusplus
+}
+#endif
+
index 962c09b..acdf069 100644 (file)
 #include "custom_utils.h"
 #include "pm.h"
 
+void custom_spi_start(void);
 
+/* 外部变量:Python服务版本号 */
+extern uint8_t g_py_ver;
 
 static osSemaphoreId_t g_semaphore_spi_id = NULL;
 static app_task_definition_t taskcfg={0};
@@ -31,10 +34,18 @@ static uint8_t g_rx_buffer[1024] = {0};
 
 static uint32_t pkg_recv_cnt = 0;  // 新增:接收包计数
 static uint32_t total_byte_error_cnt = 0;
+static uint32_t g_expected_seq = 0;  // 期望接收的包序号(从0开始)
+static uint8_t g_error_triggered = 0;  // 错误中断触发标志:1表示已触发,0表示未触发
+static uint32_t g_error_count = 0;     // 错误计数(用于异常次数判断)
+
+#define SPI_ERROR_RESET_LIMIT 20  // 当错误计数超过此阈值且g_error_triggered仍为0时,强制触发中断并重置计数
 
 /* SPI数据接收回调函数指针 */
 static custom_spi_data_callback_t g_spi_data_callback = NULL;
 
+/* SPI错误回调函数指针 */
+static custom_spi_error_callback_t g_spi_error_callback = NULL;
+
 /**
  * @brief 从4字节小端序数据中解析32位无符号整数
  * @param data 4字节数据指针
@@ -51,8 +62,39 @@ static uint32_t unpack_uint32_le(const uint8_t *data)
            ((uint32_t)data[3] << 24);
 }
 
+/**
+ * @brief 触发错误回调函数
+ * @param error_type 错误类型
+ * @param packet_seq 当前包序号(如果不可解析,使用0xFFFFFFFF)
+ * @param expected_seq 期望的包序号
+ * @param error_code 错误码(如SPI读取返回的错误码)
+ * @param data_len 数据长度(如果可解析)
+ */
+static void trigger_error_callback(custom_spi_error_type_t error_type, 
+                                   uint32_t packet_seq, 
+                                   uint32_t expected_seq,
+                                   uint32_t error_code,
+                                   uint32_t data_len)
+{
+    if (g_spi_error_callback != NULL) {
+        custom_spi_error_info_t error_info = {0};
+        error_info.error_type = error_type;
+        error_info.packet_seq = packet_seq;
+        error_info.expected_seq = expected_seq;
+        error_info.received_packets = pkg_recv_cnt;
+        error_info.error_code = error_code;
+        error_info.data_len = data_len;
+        g_spi_error_callback(&error_info);
+    }
+}
 
-void custom_spi_read(void)
+
+/**
+ * @brief SPI数据读取(版本28格式:4字节包头,无包序号,不触发中断)
+ * @note 包头格式:4字节数据长度(小端序)
+ *       总包大小:1020字节(4字节包头 + 1016字节数据)
+ */
+static void custom_spi_read_v28(void)
 {
     spi_xfer_data_t spi_data = {0};
     errcode_t ret = ERRCODE_FAIL;
@@ -63,16 +105,25 @@ void custom_spi_read(void)
 
     ret = uapi_spi_slave_read(SPI_BUS_1, &spi_data, 50);
 
-    if(ret == 0 && spi_data.rx_bytes == SPI_PACKET_TOTAL_SIZE)
+    uint32_t expected_packet_size = 4 + SPI_PACKET_DATA_SIZE;  // 1020字节
+
+    if(ret == 0 && spi_data.rx_bytes == expected_packet_size)
     {
         // 解析包头:前4字节为数据长度(小端序)
         uint32_t data_len = unpack_uint32_le(g_rx_buffer);
+        const uint8_t *data_ptr = g_rx_buffer + 4;  // 数据部分从第4字节开始
         
         // 验证数据长度合理性(应该在0到SPI_PACKET_DATA_SIZE之间)
         if (data_len > SPI_PACKET_DATA_SIZE) {
-            printf("SPI packet error: invalid data length %u (max: %u)\n", 
+            printf("SPI packet error (v28): invalid data length %u (max: %u)\n", 
                    data_len, SPI_PACKET_DATA_SIZE);
             total_byte_error_cnt++;
+            // 版本28:只触发错误回调,不触发中断
+            trigger_error_callback(CUSTOM_SPI_ERROR_INVALID_DATA_LEN, 
+                                   0xFFFFFFFF,  // 版本28没有包序号
+                                   g_expected_seq,
+                                   0,
+                                   data_len);
             return;
         }
         
@@ -82,15 +133,219 @@ void custom_spi_read(void)
         // 统计总接收字节数(仅统计实际数据长度,不包括包头)
         total_cnt += data_len;
         
-        // printf("SPI packet #%u: data_len=%u, total_cnt=%u, rx_bytes=%u\n", 
-        //        pkg_recv_cnt, data_len, total_cnt, spi_data.rx_bytes);
+        // 调用注册的回调函数处理数据(传递数据部分,不包含包头)
+        // 注意:版本28没有包序号,packet_num参数使用0
+        if (g_spi_data_callback != NULL) {
+            int callback_ret = g_spi_data_callback(data_ptr, data_len, 0);
+            if (callback_ret != 0) {
+                printf("SPI data callback returned error (v28): %d\n", callback_ret);
+                // 版本28:只触发错误回调,不触发中断
+                trigger_error_callback(CUSTOM_SPI_ERROR_CALLBACK_FAILED, 
+                                      0xFFFFFFFF,  // 版本28没有包序号
+                                      g_expected_seq,
+                                      callback_ret,
+                                      data_len);
+            }
+        } else {
+            // 如果没有注册回调函数,使用默认处理(打印调试信息)
+            printf("No SPI data callback registered, using default handling\n");
+        }
+    }
+    else
+    {
+        printf("SPI read error (v28): ret=0x%x, rx_bytes=%u (expected: %u)\n", 
+               ret, spi_data.rx_bytes, expected_packet_size);
+        total_byte_error_cnt++;
+        // 版本28:只触发错误回调,不触发中断
+        trigger_error_callback(CUSTOM_SPI_ERROR_READ_FAILED, 
+                              0xFFFFFFFF,  // 包序号不可解析
+                              g_expected_seq,
+                              ret,  // 错误码
+                              0);   // 数据长度不可解析
+    }
+}
+
+/**
+ * @brief SPI数据读取(标准格式:8字节包头,包含包序号,支持中断触发)
+ * @note 包头格式:4字节数据长度(小端序)+ 4字节包序号(小端序)
+ *       总包大小:1024字节(8字节包头 + 1016字节数据)
+ */
+static void custom_spi_read_normal(void)
+{
+    spi_xfer_data_t spi_data = {0};
+    errcode_t ret = ERRCODE_FAIL;
+    
+    memset(g_rx_buffer, 0, sizeof(g_rx_buffer));
+    spi_data.rx_bytes = sizeof(g_rx_buffer);
+    spi_data.rx_buff = g_rx_buffer;
+
+    ret = uapi_spi_slave_read(SPI_BUS_1, &spi_data, 50);
+
+    if(ret == 0 && spi_data.rx_bytes == SPI_PACKET_TOTAL_SIZE)
+    {
+        // 解析包头:前4字节为数据长度(小端序),后4字节为包序号(小端序)
+        uint32_t data_len = unpack_uint32_le(g_rx_buffer);
+        uint32_t packet_seq = unpack_uint32_le(g_rx_buffer + SPI_PACKET_SEQ_OFFSET);
+        const uint8_t *data_ptr = g_rx_buffer + SPI_PACKET_HEADER_SIZE;
+        
+        // 检查到正确位置的包:清除错误触发标志并重置错误计数
+        // 第一个包(pkg_recv_cnt == 0)或包序号匹配期望序号时,表示收到正确位置的包
+        if (packet_seq == g_expected_seq) {
+            g_error_triggered = 0;
+            g_error_count = 0;  // 重置错误计数
+            if (pkg_recv_cnt == 0) {
+                printf("SPI packet: first packet seq=%u detected, reset error trigger flag\n", packet_seq);
+            } else {
+                printf("SPI packet: correct sequence seq=%u (expected=%u) detected, reset error trigger flag\n", 
+                       packet_seq, g_expected_seq);
+            }
+        }
+        
+        // 验证数据长度合理性(应该在0到SPI_PACKET_DATA_SIZE之间)
+        if (data_len > SPI_PACKET_DATA_SIZE || data_len==0) {
+            printf("SPI packet error: invalid data length %u (max: %u)\n", 
+                   data_len, SPI_PACKET_DATA_SIZE);
+            total_byte_error_cnt++;
+           
+            // 触发中断通知模组传输出错和错误回调(仅在未触发过或收到正确位置的包时触发)
+            if (g_error_triggered == 0 || packet_seq == g_expected_seq) {
+                g_error_count++;
+                custom_spi_start();
+                
+                g_error_triggered = 1;  // 记录已触发
+                // 触发错误回调
+                trigger_error_callback(CUSTOM_SPI_ERROR_INVALID_DATA_LEN, 
+                                       packet_seq, 
+                                       g_expected_seq,
+                                       0,
+                                       data_len);
+            } else {
+                // 错误已经触发,增加错误计数
+                g_error_count++;
+                // 如果错误计数超过上限,重置标志与计数,允许后续错误继续触发
+                if (g_error_count >= SPI_ERROR_RESET_LIMIT) {
+                    printf("SPI packet: error count (%u) >= reset limit (%u), force reset error flag\n",
+                           g_error_count, SPI_ERROR_RESET_LIMIT);
+                    g_error_triggered = 0;
+                    g_error_count = 0;
+                } else {
+                    printf("SPI packet: error already triggered, skipping duplicate trigger and callback (seq=%u)\n", packet_seq);
+                }
+            }
+
+            return;
+        }
+        
+        // 验证包序号连续性(从第二个包开始检查,且期望包序号不为0时才检查)
+        // 当期望包序号为0时,跳过检查,方便断点重传
+        // if (pkg_recv_cnt > 0)
+        
+        if (packet_seq != g_expected_seq) {
+            if (packet_seq < g_expected_seq) {
+                // 收到重复包,忽略
+                printf("SPI packet: duplicate packet seq=%u (expected=%u), ignoring\n", 
+                        packet_seq, g_expected_seq);
+                // 触发错误回调(重复包,仅在未触发过或收到正确位置的包时触发)
+                if (g_error_triggered == 0 || packet_seq == g_expected_seq) {
+                    g_error_count++;
+                    trigger_error_callback(CUSTOM_SPI_ERROR_DUPLICATE_PACKET, 
+                                            packet_seq, 
+                                            g_expected_seq,
+                                            0,
+                                            data_len);
+                } else {
+                    // 错误已经触发,增加错误计数
+                    g_error_count++;
+                    // 如果错误计数超过上限,重置标志与计数,允许后续错误继续触发
+                    if (g_error_count >= SPI_ERROR_RESET_LIMIT) {
+                        printf("SPI packet: error count (%u) >= reset limit (%u), force reset error flag\n",
+                               g_error_count, SPI_ERROR_RESET_LIMIT);
+                        g_error_triggered = 0;
+                        g_error_count = 0;
+                    }
+                }
+                return;
+            } else {
+                // 包序号跳跃,可能有丢包
+                uint32_t lost_packets = packet_seq - g_expected_seq;
+                printf("SPI packet: sequence gap detected, lost %u packets (expected=%u, received=%u)\n", 
+                        lost_packets, g_expected_seq, packet_seq);
+                
+                // 触发中断通知模组传输出错和错误回调(丢包,仅在未触发过或收到正确位置的包时触发)
+                if (g_error_triggered == 0 || packet_seq == g_expected_seq) {
+                    g_error_count++;
+                    custom_spi_start();
+                    g_error_triggered = 1;  // 记录已触发
+                    // 触发错误回调(丢包)
+                    trigger_error_callback(CUSTOM_SPI_ERROR_SEQUENCE_GAP, 
+                                            packet_seq, 
+                                            g_expected_seq,
+                                            lost_packets,
+                                            data_len);
+                   
+                } else {
+                    // 错误已经触发,增加错误计数
+                    g_error_count++;
+                    // 如果错误计数超过上限,重置标志与计数,允许后续错误继续触发
+                    if (g_error_count >= SPI_ERROR_RESET_LIMIT) {
+                        printf("SPI packet: error count (%u) >= reset limit (%u), force reset error flag\n",
+                               g_error_count, SPI_ERROR_RESET_LIMIT);
+                        g_error_triggered = 0;
+                        g_error_count = 0;
+                    } else {
+                        printf("SPI packet: error already triggered, skipping duplicate trigger and callback (seq=%u)\n", packet_seq);
+                    }
+                }
+                return;
+                
+                // 注意:这里可以发送重传请求,但需要SPI发送功能支持
+            }
+        }
+        
+        // 更新期望的包序号(下一个应该是当前序号+1)
+        g_expected_seq = packet_seq + 1;
+        
+        // 包计数自增
+        pkg_recv_cnt++;
+        
+        // 统计总接收字节数(仅统计实际数据长度,不包括包头)
+        total_cnt += data_len;
+
+        if(data_len != SPI_PACKET_DATA_SIZE)
+        {
+            g_expected_seq = 0;
+            pkg_recv_cnt=0;
+        }
         
         // 调用注册的回调函数处理数据(传递数据部分,不包含包头)
+        // 注意:回调函数中的packet_num参数使用接收到的包序号(packet_seq)
         if (g_spi_data_callback != NULL) {
-            const uint8_t *data_ptr = g_rx_buffer + SPI_PACKET_HEADER_SIZE;
-            int callback_ret = g_spi_data_callback(data_ptr, data_len, pkg_recv_cnt);
+
+            int callback_ret = g_spi_data_callback(data_ptr, data_len, packet_seq);
             if (callback_ret != 0) {
                 printf("SPI data callback returned error: %d\n", callback_ret);
+                // 触发错误回调(数据回调返回错误,仅在未触发过或收到正确位置的包时触发)
+                if (g_error_triggered == 0 || packet_seq == g_expected_seq) {
+                    g_error_count++;
+                    trigger_error_callback(CUSTOM_SPI_ERROR_CALLBACK_FAILED, 
+                                          packet_seq, 
+                                          g_expected_seq,
+                                          callback_ret,
+                                          data_len);
+                    g_error_triggered = 1;  // 记录已触发
+                } else {
+                    // 错误已经触发,增加错误计数
+                    g_error_count++;
+                    // 如果错误计数超过上限,重置标志与计数,允许后续错误继续触发
+                    if (g_error_count >= SPI_ERROR_RESET_LIMIT) {
+                        printf("SPI packet: error count (%u) >= reset limit (%u), force reset error flag\n",
+                               g_error_count, SPI_ERROR_RESET_LIMIT);
+                        g_error_triggered = 0;
+                        g_error_count = 0;
+                    } else {
+                        printf("SPI callback: error already triggered, skipping duplicate callback (seq=%u)\n", packet_seq);
+                    }
+                }
             }
         } else {
             // 如果没有注册回调函数,使用默认处理(打印调试信息)
@@ -102,6 +357,43 @@ void custom_spi_read(void)
         printf("SPI read error: ret=0x%x, rx_bytes=%u (expected: %u)\n", 
                ret, spi_data.rx_bytes, SPI_PACKET_TOTAL_SIZE);
         total_byte_error_cnt++;
+        
+        // 触发中断通知模组传输出错和错误回调(仅在未触发过时触发,因为无法解析包序号)
+        if (g_error_triggered == 0) {
+            g_error_count++;
+            custom_spi_start();
+            g_error_triggered = 1;  // 记录已触发
+            // 触发错误回调(SPI读取失败)
+            trigger_error_callback(CUSTOM_SPI_ERROR_READ_FAILED, 
+                                  0xFFFFFFFF,  // 包序号不可解析
+                                  g_expected_seq,
+                                  ret,  // 错误码
+                                  0);   // 数据长度不可解析
+        } else {
+            // 错误已经触发,增加错误计数
+            g_error_count++;
+            // 如果错误计数超过上限,重置标志与计数,允许后续错误继续触发
+            if (g_error_count >= SPI_ERROR_RESET_LIMIT) {
+                printf("SPI read: error count (%u) >= reset limit (%u), force reset error flag\n",
+                       g_error_count, SPI_ERROR_RESET_LIMIT);
+                g_error_triggered = 0;
+                g_error_count = 0;
+            } else {
+                printf("SPI read: error already triggered, skipping duplicate trigger and callback\n");
+            }
+        }
+    }
+}
+
+/**
+ * @brief SPI数据读取(统一入口,根据版本调用对应函数)
+ */
+void custom_spi_read(void)
+{
+    if (g_py_ver == 28) {
+        custom_spi_read_v28();
+    } else {
+        custom_spi_read_normal();
     }
 }
 
@@ -115,10 +407,14 @@ void custom_spi_service_task(void *arg)
         errcode_t ret = osSemaphoreAcquire(g_semaphore_spi_id, 60 * 1000);
         
         if (ret == 0) {
-            // 4. 信号量获取成功:处理SPI数据
+            // 4. 信号量获取成功:根据版本调用对应的SPI读取函数
             // printf("获取信号量成功,开始读取SPI数据...\r\n");
             
-            custom_spi_read();
+            if (g_py_ver == 28) {
+                custom_spi_read_v28();
+            } else {
+                custom_spi_read_normal();
+            }
             
             // 5. 数据处理完成后,信号量自动回到「锁住」状态(无需手动操作)
             //    下一次循环会再次调用osSemaphoreAcquire,等待下一次中断释放
@@ -135,6 +431,69 @@ void custom_spi_service_task(void *arg)
     }
 }
 
+/**
+ * @brief 仅初始化SPI硬件(不操作线程、信号量、GPIO中断、PM等)
+ * @return ERRCODE_SUCC成功,其他值失败
+ * @note 此函数只初始化SPI硬件,不创建线程和信号量,不注册GPIO中断,不进行PM操作
+ */
+errcode_t custom_spi_hw_init(void)
+{
+    spi_bus_t bus = SPI_BUS_1;
+    
+    // Initialize SPI1 as Slave (Client) mode
+    spi_attr_t attr = {0};
+    spi_extra_attr_t extra_attr = {0};
+    
+    attr.is_slave = true;              // Slave模式(Client)
+    attr.slave_num = 0;                 // Slave编号
+    attr.bus_clk = 32000000;           // 总线时钟32MHz
+    attr.freq_mhz = 0;                  // Slave模式下由Master控制(26MHz由Master设置)
+    attr.clk_polarity = SPI_CFG_CLK_CPOL_0;  // CPOL=0
+    attr.clk_phase = SPI_CFG_CLK_CPHA_0;    // CPHA=0 (Mode 0)
+    attr.frame_format = SPI_CFG_FRAME_FORMAT_MOTOROLA_SPI;
+    attr.spi_frame_format = (uint32_t)HAL_SPI_FRAME_FORMAT_STANDARD;  // 标准SPI格式(非QUAD)
+    attr.frame_size = (uint32_t)HAL_SPI_FRAME_SIZE_8;  // 8位数据(与Python端匹配)
+    attr.tmod = HAL_SPI_TRANS_MODE_RX;      // 只接收模式
+    attr.sste = SPI_CFG_SSTE_DISABLE;
+    attr.ndf = 1;
+    
+    // DMA模式配置
+    // 注意:在自动切换模式下,extra_attr.rx_use_dma设置为true只是告诉驱动支持DMA
+    // 实际是否使用DMA由uapi_spi_slave_read根据数据大小自动决定
+    extra_attr.tx_use_dma = false;
+    extra_attr.rx_use_dma = true;  // 启用DMA接收模式支持(自动切换模式下会根据数据大小决定)
+    
+    memset(&extra_attr.qspi_param, 0, sizeof(extra_attr.qspi_param));
+    memset(&extra_attr.sspi_param, 0, sizeof(extra_attr.sspi_param));
+    
+    // Deinitialize SPI first (if already initialized)
+    uapi_spi_deinit(bus);
+    
+    // Initialize SPI
+    errcode_t ret = uapi_spi_init(bus, &attr, &extra_attr);
+    if (ret != ERRCODE_SUCC) {
+        printf("SPI1 hardware init failed: 0x%x\n", ret);
+        return ret;
+    }
+    
+    printf("SPI hardware initialized successfully\n");
+    return ERRCODE_SUCC;
+}
+
+/**
+ * @brief 仅反初始化SPI硬件(不操作线程、信号量、GPIO中断、PM等)
+ * @note 此函数只反初始化SPI硬件,不销毁线程和信号量,不注销GPIO中断,不进行PM操作
+ */
+void custom_spi_hw_deinit(void)
+{
+    errcode_t ret = uapi_spi_deinit(SPI_BUS_1);
+    if (ret != ERRCODE_SUCC) {
+        printf("uapi_spi_deinit failed: 0x%x\n", ret);
+    }
+    
+    printf("SPI hardware deinitialized\n");
+}
+
 void custom_spi_init(void)
 {
     spi_bus_t bus = SPI_BUS_1;
@@ -240,6 +599,9 @@ void custom_spi_deinit(void)
     pkg_recv_cnt = 0;
     total_byte_error_cnt = 0;
     total_cnt = 0;
+    g_expected_seq = 0;  // 重置期望包序号
+    g_error_triggered = 0;  // 重置错误触发标志
+    g_error_count = 0;  // 重置错误计数
     errcode_t ret = 0;
 
     if(taskcfg.task_handle)
@@ -277,6 +639,7 @@ void custom_spi_deinit(void)
     {
         uapi_pm_remove_sleep_veto(PM_ID_SPI);
     }
+    printf("custom_spi_deinit: succ\n");
 }
 
 
@@ -314,4 +677,19 @@ void custom_spi_register_data_callback(custom_spi_data_callback_t callback)
     } else {
         printf("SPI data callback unregistered\n");
     }
-}
\ No newline at end of file
+}
+
+/**
+ * @brief 注册SPI传输错误回调函数
+ * @param callback 回调函数指针,NULL表示取消注册
+ */
+void custom_spi_register_error_callback(custom_spi_error_callback_t callback)
+{
+    g_spi_error_callback = callback;
+    if (callback != NULL) {
+        printf("SPI error callback registered successfully\n");
+    } else {
+        printf("SPI error callback unregistered\n");
+    }
+}
+
index ff83e45..dd1437e 100644 (file)
@@ -26,22 +26,64 @@ extern "C" {
 #include "platform_core.h"
 
 /* SPI数据包格式定义 */
-#define SPI_PACKET_HEADER_SIZE 4  /* 包头大小:4字节(数据长度,小端序) */
+#define SPI_PACKET_HEADER_SIZE 8  /* 包头大小:8字节(数据长度4字节 + 包序号4字节,均为小端序) */
 #define SPI_PACKET_TOTAL_SIZE 1024  /* 总包大小:1024字节 */
-#define SPI_PACKET_DATA_SIZE (SPI_PACKET_TOTAL_SIZE - SPI_PACKET_HEADER_SIZE)  /* 数据部分大小:1020字节 */
+#define SPI_PACKET_DATA_SIZE (SPI_PACKET_TOTAL_SIZE - SPI_PACKET_HEADER_SIZE)  /* 数据部分大小:1016字节 */
+#define SPI_PACKET_SEQ_OFFSET 4  /* 包序号在包头中的偏移(数据长度后4字节) */
+
+/* SPI传输错误类型 */
+typedef enum {
+    CUSTOM_SPI_ERROR_NONE = 0,              /* 无错误 */
+    CUSTOM_SPI_ERROR_READ_FAILED,          /* SPI读取失败 */
+    CUSTOM_SPI_ERROR_INVALID_DATA_LEN,      /* 数据长度无效 */
+    CUSTOM_SPI_ERROR_SEQUENCE_GAP,          /* 包序号跳跃(丢包) */
+    CUSTOM_SPI_ERROR_DUPLICATE_PACKET,      /* 重复包 */
+    CUSTOM_SPI_ERROR_CALLBACK_FAILED         /* 数据回调函数返回错误 */
+} custom_spi_error_type_t;
+
+/* SPI错误信息结构体 */
+typedef struct {
+    custom_spi_error_type_t error_type;     /* 错误类型 */
+    uint32_t packet_seq;                    /* 当前包序号(如果可解析,否则为0xFFFFFFFF) */
+    uint32_t expected_seq;                  /* 期望的包序号 */
+    uint32_t received_packets;              /* 已接收的包计数 */
+    uint32_t error_code;                   /* 错误码(如SPI读取返回的错误码) */
+    uint32_t data_len;                      /* 数据长度(如果可解析) */
+} custom_spi_error_info_t;
 
 /* SPI数据接收回调函数类型 */
 /**
  * @brief SPI数据接收回调函数
  * @param data 接收到的数据指针(不包含包头,仅数据部分)
  * @param data_len 数据长度(从包头解析得到)
- * @param packet_num 包序号(从1开始递增)
+ * @param packet_num 包序号(从0开始,从包头中解析得到)
  * @return 0表示成功,非0表示失败
  */
 typedef int (*custom_spi_data_callback_t)(const uint8_t *data, uint32_t data_len, uint32_t packet_num);
 
+/* SPI错误回调函数类型 */
+/**
+ * @brief SPI传输错误回调函数
+ * @param error_info 错误信息结构体指针,包含错误类型、错误位置等信息
+ */
+typedef void (*custom_spi_error_callback_t)(const custom_spi_error_info_t *error_info);
+
 void custom_spi_init(void);
 void custom_spi_deinit(void);
+
+/**
+ * @brief 仅初始化SPI硬件(不操作线程、信号量、GPIO中断、PM等)
+ * @return ERRCODE_SUCC成功,其他值失败
+ * @note 此函数只初始化SPI硬件,不创建线程和信号量,不注册GPIO中断,不进行PM操作
+ */
+errcode_t custom_spi_hw_init(void);
+
+/**
+ * @brief 仅反初始化SPI硬件(不操作线程、信号量、GPIO中断、PM等)
+ * @note 此函数只反初始化SPI硬件,不销毁线程和信号量,不注销GPIO中断,不进行PM操作
+ */
+void custom_spi_hw_deinit(void);
+
 void custom_spi_read(void);
 void custom_spi_rx_callback(pin_t pin, uintptr_t param);
 
@@ -51,6 +93,13 @@ void custom_spi_rx_callback(pin_t pin, uintptr_t param);
  */
 void custom_spi_register_data_callback(custom_spi_data_callback_t callback);
 
+/**
+ * @brief 注册SPI传输错误回调函数
+ * @param callback 回调函数指针,NULL表示取消注册
+ */
+void custom_spi_register_error_callback(custom_spi_error_callback_t callback);
+
+
 #ifdef __cplusplus
 }
 #endif /* __cplusplus */
index 1634cf3..52dd746 100644 (file)
@@ -49,7 +49,7 @@ static const char *g_log_level_str[] = {
  */
 
 /* 消息队列相关定义 */
-#define WS_MSG_QUEUE_COUNT 16
+#define WS_MSG_QUEUE_COUNT 64
 #define WS_MSG_TYPE_SEND_TEXT 0x01
 #define WS_MSG_TYPE_FORCE_RECONNECT 0x02
 #define WS_MSG_TYPE_STOP 0x03