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"
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
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类型,说明如下:
# 提前创建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):
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
# 拆分数据为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)
# 处理最后不足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)
直接从固定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传输测试
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对象
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):
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打印一次,减少打印数量)
# 处理最后不足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. 提前填充缓冲区(逐字节拷贝,无切片赋值)
# 清空数据部分(显式清空,确保兼容性)
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))
# 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:
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))
# ==================== 外部接口函数 ====================
# 提供简单的函数接口,可以从外部传入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传输的简单接口函数
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)
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对象被关闭
self.spi_obj = spi_obj
self.test_instance = None
- def download(self, url):
+ def download(self, url, resume_from_byte=0):
"""
下载指定URL的文件并通过SPI传输
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")
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:
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)
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) {
// 注册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;
// 取消注册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);
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;
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);
// 保存文件路径
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);
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);
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);
}
}
+/**
+ * @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=失败)
return;
}
+ // 停止超时定时器
+ if (g_download_timeout_timer.timer != NULL) {
+ osal_timer_stop(&g_download_timeout_timer);
+ }
+
// 设置完成状态
if (success) {
g_download_state = HTTP_DOWNLOAD_STATE_COMPLETED;
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);
+ }
}
/**
* @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);
*/
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=失败)
${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
+
)
# 公共头文件路径(供其他模块引用)
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;
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:
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:
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");
/* 语音对话流相关命令 */
#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];
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
{
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
// 音频播放计时相关变量
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缓存
// 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冷却持续时间(毫秒)
// 音频时长记录循环数组
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);
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
}
// 检查对话ID是否匹配
-static bool is_chat_id_matched(void)
+bool is_chat_id_matched(void)
{
return g_chat_id_matched;
}
#endif
uapi_cat1_add_cat1_vote();
+ ln_http_download_init();
+
+
old_Start_AIVoice(AIVoiceDataCallback_ln,NULL,&audioParams_in,&audioParams_out,&audioParams_in_vad,false);
// 设置初始化标志位
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;
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);
// 转换为字符串
--- /dev/null
+/**
+ * @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
+
#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};
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字节数据指针
((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;
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;
}
// 统计总接收字节数(仅统计实际数据长度,不包括包头)
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 {
// 如果没有注册回调函数,使用默认处理(打印调试信息)
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();
}
}
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,等待下一次中断释放
}
}
+/**
+ * @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;
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)
{
uapi_pm_remove_sleep_veto(PM_ID_SPI);
}
+ printf("custom_spi_deinit: succ\n");
}
} 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");
+ }
+}
+
#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);
*/
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 */
*/
/* 消息队列相关定义 */
-#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