소스 검색

Merge branch 'master' of https://gitee.com/openLuat/LuatOS

陈取德 3 달 전
부모
커밋
3aefa7eb5c
80개의 변경된 파일14050개의 추가작업 그리고 1211개의 파일을 삭제
  1. 3 48
      components/airlink/src/task/luat_airlink_spi_master_task.c
  2. 4 0
      components/airlink/src/task/luat_airlink_task.c
  3. 134 54
      components/ethernet/common/dhcp_client.c
  4. 7 2
      components/hzfont/binding/luat_lib_hzfont.c
  5. 1 1
      components/hzfont/inc/luat_hzfont.h
  6. 90 11
      components/hzfont/src/luat_hzfont.c
  7. 9 1
      components/network/iperf/binding/luat_lib_iperf.c
  8. 14 7
      components/network/iperf/include/luat_lwiperf.h
  9. 27 36
      components/network/iperf/src/luat_lwiperf.c
  10. 2 1
      components/network/libhttp/luat_http_client.c
  11. 12 0
      components/network/libsntp/luat_sntp.c
  12. 10 0
      components/network/netdrv/include/luat_netdrv_ch390h.h
  13. 35 0
      components/network/netdrv/include/luat_netdrv_napt.h
  14. 43 29
      components/network/netdrv/src/ch390h_api.c
  15. 66 28
      components/network/netdrv/src/ch390h_task.c
  16. 41 13
      components/network/netdrv/src/luat_netdrv_ch390h.c
  17. 377 49
      components/network/netdrv/src/luat_netdrv_napt.c
  18. 23 9
      components/network/netdrv/src/luat_netdrv_napt_tcp.c
  19. 24 10
      components/network/netdrv/src/luat_netdrv_napt_udp.c
  20. 88 2
      components/rtmp/include/luat_rtmp_push.h
  21. 179 6
      components/rtmp/src/luat_rtmp_push.c
  22. 531 0
      components/rtsp/binding/luat_lib_rtsp.c
  23. 362 0
      components/rtsp/include/luat_rtsp_push.h
  24. 1854 0
      components/rtsp/src/luat_rtsp_push.c
  25. 5 0
      luat/include/luat_libs.h
  26. 2 0
      luat/include/luat_mem.h
  27. 2 0
      luat/include/luat_uart.h
  28. 43 0
      luat/include/luat_usb.h
  29. 12 0
      luat/modules/luat_lib_fota.c
  30. 12 0
      luat/modules/luat_lib_rtos.c
  31. 5 1
      luat/modules/luat_lib_uart.c
  32. 346 0
      luat/modules/luat_lib_usb.c
  33. 65 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/main.lua
  34. 0 9
      module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/onewire_multi_18b20_swich_read/main.lua
  35. 0 125
      module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/onewire_multi_18b20_swich_read/switch_read.lua
  36. 367 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/onewire_multi_app.lua
  37. 0 108
      module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/onewire_single_18b20/main.lua
  38. 203 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/onewire_single_app.lua
  39. 212 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/readme.md
  40. 366 291
      module/Air780EHM_Air780EHV_Air780EGH/demo/tf_card/tfcard_app.lua
  41. 65 0
      module/Air780EPM/demo/onewire/main.lua
  42. 0 9
      module/Air780EPM/demo/onewire/onewire_multi_18b20_swich_read/main.lua
  43. 0 125
      module/Air780EPM/demo/onewire/onewire_multi_18b20_swich_read/switch_read.lua
  44. 367 0
      module/Air780EPM/demo/onewire/onewire_multi_app.lua
  45. 0 107
      module/Air780EPM/demo/onewire/onewire_single_18b20/main.lua
  46. 203 0
      module/Air780EPM/demo/onewire/onewire_single_app.lua
  47. 212 0
      module/Air780EPM/demo/onewire/readme.md
  48. 275 0
      module/Air8000/demo/fota/ble_fota/ble_file_fota.lua
  49. BIN
      module/Air8000/demo/fota/ble_fota/ble_fota.bin
  50. 246 0
      module/Air8000/demo/fota/ble_fota/ble_fota_tool.py
  51. 236 0
      module/Air8000/demo/fota/ble_fota/ble_main.lua
  52. 270 0
      module/Air8000/demo/fota/ble_fota/ble_packet_fota.lua
  53. 92 0
      module/Air8000/demo/fota/ble_fota/main.lua
  54. 1297 0
      module/Air8000/demo/fota/ble_fota/readme.md
  55. 149 0
      module/Air8000/demo/modbus/rtu_master/main.lua
  56. 159 0
      module/Air8000/demo/modbus/rtu_master/param_field.lua
  57. 257 0
      module/Air8000/demo/modbus/rtu_master/raw_frame.lua
  58. 444 0
      module/Air8000/demo/modbus/rtu_master/readme.md
  59. 129 0
      module/Air8000/demo/modbus/rtu_master/temp_hum_sensor.lua
  60. 79 0
      module/Air8000/demo/modbus/rtu_slave/main.lua
  61. 128 0
      module/Air8000/demo/modbus/rtu_slave/readme.md
  62. 147 0
      module/Air8000/demo/modbus/rtu_slave/rtu_slave_manage.lua
  63. 81 0
      module/Air8000/demo/modbus/tcp_slave/main.lua
  64. 81 0
      module/Air8000/demo/modbus/tcp_slave/netdrv_eth_spi.lua
  65. 130 0
      module/Air8000/demo/modbus/tcp_slave/readme.md
  66. 139 0
      module/Air8000/demo/modbus/tcp_slave/tcp_slave_manage.lua
  67. 56 105
      module/Air8000/demo/onewire/main.lua
  68. 367 0
      module/Air8000/demo/onewire/onewire_multi_app.lua
  69. 203 0
      module/Air8000/demo/onewire/onewire_single_app.lua
  70. 213 0
      module/Air8000/demo/onewire/readme.md
  71. 71 0
      module/Air8000/project/Air8000A_CH390_1/main.lua
  72. 66 0
      module/Air8000/project/Air8000A_CH390_1/netif_app.lua
  73. 250 0
      module/Air8000/project/Air8000A_CH390_1/pins_air8000.json
  74. 58 0
      module/Air8000/project/Air8000A_CH390_1/readme.md
  75. 66 0
      olddemo/camera/rtsp_usb/main.lua
  76. 1 23
      olddemo/iperf/air8101/main.lua
  77. 386 0
      script/libs/exmodbus.lua
  78. 1071 0
      script/libs/exmodbus_rtu_ascii.lua
  79. 479 0
      script/libs/exmodbus_tcp.lua
  80. 1 1
      script/libs/libfota.lua

+ 3 - 48
components/airlink/src/task/luat_airlink_spi_master_task.c

@@ -13,9 +13,6 @@
 #include "luat_mem.h"
 #include "luat_mcu.h"
 #include "luat_fs.h"
-#ifdef __BK72XX__
-#include <os/os.h>
-#endif
 #define LUAT_LOG_TAG "airlink"
 #include "luat_log.h"
 
@@ -43,10 +40,6 @@ static uint8_t thread_rdy;
 static uint8_t spi_rdy;
 static luat_rtos_task_handle spi_task_handle;
 static luat_rtos_task_handle spi_irq_task_handle;
-#if defined(__BK72XX__) && defined(CONFIG_FREERTOS_SMP)
-static beken_thread_t bk_spi_task_handle = NULL;
-static beken_thread_t bk_spi_irq_task_handle = NULL;
-#endif
 
 #if defined(LUAT_USE_AIRLINK_EXEC_MOBILE)
 extern luat_airlink_dev_info_t g_airlink_self_dev_info;
@@ -77,13 +70,8 @@ __USER_FUNC_IN_RAM__ static int rdy_pin_irq_handler(void* param)
     }
     rdy_ready_flag = 1;
     // 发送通知事件,告知任务RDY已就绪
-#ifdef __BK72XX__
-    luat_event_t evt = {.id = 2};
-    luat_rtos_queue_send(rdy_task_evt_queue, &evt, sizeof(evt), 0);
-#else
     luat_event_t evt = {.id = 6};
     luat_rtos_queue_send(rdy_evt_queue, &evt, sizeof(evt), 0);
-#endif
     // LLOGD("RDY中断触发,设置就绪标志");
     return 0;
 }
@@ -156,12 +144,11 @@ void luat_airlink_spi_master_pin_setup(void)
         .mode = 1, // mode设置为1,全双工
 #ifdef __BK72XX__
         .bandrate = g_airlink_spi_conf.speed > 0 ? g_airlink_spi_conf.speed : 13000000,
-        .cs = AIRLINK_SPI_CS_PIN};
 #else
         .bandrate = g_airlink_spi_conf.speed > 0 ? g_airlink_spi_conf.speed : 31000000,
-        .cs = 255};
-    // luat_pm_iovolt_ctrl(0, 3300);
+        // luat_pm_iovolt_ctrl(0, 3300);
 #endif
+        .cs = 255};
     luat_spi_setup(&spi_conf);
     luat_gpio_cfg_t gpio_cfg = {0};
 
@@ -181,15 +168,13 @@ void luat_airlink_spi_master_pin_setup(void)
     gpio_cfg.pull = LUAT_GPIO_PULLUP;
     gpio_cfg.irq_cb = rdy_pin_irq_handler;
     luat_gpio_open(&gpio_cfg);
-#ifndef __BK72XX__
-    // CS片选脚
+//     // CS片选脚
     luat_gpio_set_default_cfg(&gpio_cfg);
     gpio_cfg.pin = AIRLINK_SPI_CS_PIN;
     gpio_cfg.mode = LUAT_GPIO_OUTPUT;
     gpio_cfg.pull = LUAT_GPIO_PULLUP;
     gpio_cfg.output_level = 1;
     luat_gpio_open(&gpio_cfg);
-#endif
 
     if (g_airlink_spi_conf.irq_pin != 255)
     {
@@ -237,9 +222,7 @@ __USER_FUNC_IN_RAM__ void airlink_transfer_and_exec(uint8_t *txbuff, uint8_t *rx
 
     g_airlink_statistic.tx_pkg.total++;
     // 拉低片选, 准备发送数据
-#ifndef __BK72XX__
     luat_gpio_set(AIRLINK_SPI_CS_PIN, 0);
-#endif
     
     // luat_spi_lock(MASTER_SPI_ID);
     // 发送数据
@@ -256,9 +239,7 @@ __USER_FUNC_IN_RAM__ void airlink_transfer_and_exec(uint8_t *txbuff, uint8_t *rx
     }
     rdy_ready_flag = 0;
     // 拉高片选, 数据发送完毕
-#ifndef __BK72XX__
     luat_gpio_set(AIRLINK_SPI_CS_PIN, 1);
-#endif
     // luat_airlink_print_buff("RX", rxbuff, 64);
     // 对接收到的数据进行解析
     link = luat_airlink_data_unpack(rxbuff, TEST_BUFF_SIZE);
@@ -451,24 +432,6 @@ __USER_FUNC_IN_RAM__ static void spi_master_task(void *param)
     }
 }
 
-#ifdef __BK72XX__
-__USER_FUNC_IN_RAM__ static void spi_irq_task(void *param)
-{
-    while (1)
-    {
-        uint64_t timeout = 0;
-        luat_event_t event = {0};
-        luat_rtos_queue_recv(rdy_task_evt_queue, &event, sizeof(luat_event_t), 10);
-        if (event.id == 2)
-        {
-            // LLOGD("从机通知IRQ中断");
-            // 发送通知事件,告知任务RDY已就绪
-            luat_event_t evt = {.id = 6};
-            luat_rtos_queue_send(rdy_evt_queue, &evt, sizeof(evt), 0);
-        }
-    }
-}
-#endif
 
 void luat_airlink_start_master(void)
 {
@@ -485,15 +448,7 @@ void luat_airlink_start_master(void)
     luat_rtos_queue_create(&evt_queue, 4 * 1024, sizeof(luat_event_t));
     // 创建专门的RDY事件队列 (id=6)
     luat_rtos_queue_create(&rdy_evt_queue, 10, sizeof(luat_event_t));
-#if defined(__BK72XX__) && defined(CONFIG_FREERTOS_SMP)
-// luat_rtos_task_create(&spi_irq_task_handle,  4 * 1024, 95, "spi_irq", spi_irq_task, NULL, 0);
-    rtos_core0_create_thread((beken_thread_t *)bk_spi_task_handle, BEKEN_APPLICATION_PRIORITY, "spi", (beken_thread_function_t)spi_master_task, 8 * 1024, 0);
-    rtos_core0_create_thread((beken_thread_t *)bk_spi_irq_task_handle, 10, "spi_irq", (beken_thread_function_t)spi_irq_task, 4 * 1024, 0);
-    // 创建专门用于处理从机通知中断的任务
-    luat_rtos_queue_create(&rdy_task_evt_queue, 1, sizeof(luat_event_t));
-#else
     luat_rtos_task_create(&spi_task_handle, 8 * 1024, 50, "spi", spi_master_task, NULL, 0);
-#endif
 }
 
 int luat_airlink_irqmode(luat_airlink_irq_ctx_t *ctx) {

+ 4 - 0
components/airlink/src/task/luat_airlink_task.c

@@ -112,6 +112,10 @@ __AIRLINK_CODE_IN_RAM__ static int luat_airlink_task(void *param) {
                 if (g_airlink_ext_dev_info.tp == 0x01) {
                     memcpy(&tmpv, g_airlink_ext_dev_info.wifi.version, 4);
                     LLOGI("AIRLINK_READY %ld version %ld t %ld", (uint32_t)g_airlink_last_cmd_timestamp, tmpv, (uint32_t)t_used);
+                    #ifdef LUAT_MODEL_AIR8000
+                    extern void luat_airlink_wifi_ready_event();
+                    luat_airlink_wifi_ready_event();
+                    #endif
                 }
                 else if (g_airlink_ext_dev_info.tp == 0x02) {
                     memcpy(&tmpv, g_airlink_ext_dev_info.cat1.version, 4);

+ 134 - 54
components/ethernet/common/dhcp_client.c

@@ -8,6 +8,15 @@
 #include "luat_log.h"
 #define DHCP_OPTION_138 138
 
+// DHCP超时和重传配置
+#define DHCP_MIN_LEASE_SEC 60
+#define DHCP_RETRY_BASE_MS 1000
+#define DHCP_RETRY_MAX_MS 8000
+#define DHCP_RENEW_TIMEOUT_MS 2500
+#define DHCP_REBIND_TIMEOUT_MS 2500
+#define DHCP_SELECT_ACK_TIMEOUT_MS 1900
+#define DHCP_MAX_SELECT_RETRIES 3
+
 
 //extern void DBG_Printf(const char* format, ...);
 //extern void DBG_HexPrintf(void *Data, unsigned int len);
@@ -20,6 +29,11 @@ void make_ip4_dhcp_msg_base(dhcp_client_info_t *dhcp, uint16_t flag, Buffer_Stru
 	{
 		escape_time = (luat_mcu_tick64_ms() - dhcp->last_tx_time) / 1000;
 	}
+	// 确保缓冲区至少能容纳DHCP基本消息(236) + magic(4) + 选项空间
+	if (out->MaxLen < DHCP_MSG_LEN + 4 + 64) {
+		LLOGE("buffer too small for DHCP message");
+		return;
+	}
 	BytesPut8ToBuf(out, DHCP_BOOTREQUEST);
 	BytesPut8ToBuf(out, DHCP_HTYPE_ETH);
 	BytesPut8ToBuf(out, 6);
@@ -32,8 +46,9 @@ void make_ip4_dhcp_msg_base(dhcp_client_info_t *dhcp, uint16_t flag, Buffer_Stru
 	BytesPutLe32ToBuf(out, 0);
 	BytesPutLe32ToBuf(out, 0);
 	OS_BufferWrite(out, dhcp->mac, 6);
-	memset(out->Data + out->Pos, 0, 10 + 64 + 128);
-	out->Pos += 10 + 64 + 128;
+	// 使用OS_BufferWrite确保安全,填充chaddr剩余10字节 + sname(64) + file(128)
+	uint8_t zeros[202] = {0}; // 10 + 64 + 128
+	OS_BufferWrite(out, zeros, sizeof(zeros));
 	BytesPutBe32ToBuf(out, DHCP_MAGIC_COOKIE);
 }
 
@@ -56,6 +71,8 @@ void ip4_dhcp_msg_add_ip_option(uint8_t id, uint32_t ip, Buffer_Struct *out)
 {
 	BytesPut8ToBuf(out, id);
 	BytesPut8ToBuf(out, 4);
+	// 保持与系统内部IP存储格式一致(小端序)
+	// 该系统所有IP读取都用BytesGetLe32,写入也需要用Le32保持一致
 	BytesPutLe32ToBuf(out, ip);
 }
 
@@ -95,14 +112,7 @@ void make_ip4_dhcp_discover_msg(dhcp_client_info_t *dhcp, Buffer_Struct *out)
 	ip4_dhcp_msg_add_bytes_option(DHCP_OPTION_HOSTNAME, (uint8_t*)dhcp->name, strlen(dhcp->name), out);
 	ip4_dhcp_msg_add_client_id_option(DHCP_OPTION_CLIENT_ID, (uint8_t*)dhcp->mac, 6, out);
 	BytesPut8ToBuf(out, 0xff);
-	if (out->Pos < (DHCP_MSG_LEN + 72))
-	{
-		out->Pos = (DHCP_MSG_LEN + 72);
-	}
-	else
-	{
-		out->Pos = (out->Pos + (4 - 1)) & (~(4 - 1));
-	}
+	// DHCP选项无需强制4字节对齐,避免产生冗余填充
 }
 
 void make_ip4_dhcp_select_msg(dhcp_client_info_t *dhcp, uint16_t flag, Buffer_Struct *out)
@@ -119,7 +129,8 @@ void make_ip4_dhcp_select_msg(dhcp_client_info_t *dhcp, uint16_t flag, Buffer_St
 			  //DHCP_OPTION_138,
 	  };
 	out->Pos = 0;
-	uint8_t full_name[35] = {0};
+	// 构造FQDN(Option 81): Flags(1) + RCode1(1) + RCode2(1) + Hostname
+	uint8_t full_name[96] = {0};
 	make_ip4_dhcp_msg_base(dhcp, flag, out);
 	ip4_dhcp_msg_add_integer_option(DHCP_OPTION_MESSAGE_TYPE, DHCP_OPTION_MESSAGE_TYPE_LEN, DHCP_REQUEST, out);
 	ip4_dhcp_msg_add_client_id_option(DHCP_OPTION_CLIENT_ID, (uint8_t*)dhcp->mac, 6, out);
@@ -128,10 +139,20 @@ void make_ip4_dhcp_select_msg(dhcp_client_info_t *dhcp, uint16_t flag, Buffer_St
 	{
 		ip4_dhcp_msg_add_ip_option(DHCP_OPTION_SERVER_ID, dhcp->server_ip, out);
 	}
-	ip4_dhcp_msg_add_bytes_option(DHCP_OPTION_HOSTNAME, (uint8_t*)dhcp->name, strlen(dhcp->name), out);
-	memcpy(full_name + 3, (uint8_t*)dhcp->name, strlen(dhcp->name));
-	ip4_dhcp_msg_add_bytes_option(81, full_name, strlen(dhcp->name) + 3, out);
-	ip4_dhcp_msg_add_bytes_option(60, (uint8_t *)"MSFT 5.0", 8, out);
+	// HOSTNAME(长度限制避免超过255)
+	{
+		size_t name_len = strlen(dhcp->name);
+		if (name_len > 63) name_len = 63; // 常见实现对主机名长度做限制
+		ip4_dhcp_msg_add_bytes_option(DHCP_OPTION_HOSTNAME, (uint8_t*)dhcp->name, (uint8_t)name_len, out);
+		// FQDN: Flags=0x01(服务器进行更新),RCode1=0,RCode2=0
+		full_name[0] = 0x01;
+		full_name[1] = 0x00;
+		full_name[2] = 0x00;
+		memcpy(full_name + 3, (uint8_t*)dhcp->name, name_len);
+		ip4_dhcp_msg_add_bytes_option(81, full_name, (uint8_t)(name_len + 3), out);
+	}
+	// Vendor Class已移除(原"MSFT 5.0"),避免服务器施加Windows特定策略
+	// 如有需要,可根据实际平台配置添加
 	ip4_dhcp_msg_add_bytes_option(DHCP_OPTION_PARAMETER_REQUEST_LIST, dhcp_discover_request_options, sizeof(dhcp_discover_request_options), out);
 	BytesPut8ToBuf(out, 0xff);
 }
@@ -186,6 +207,12 @@ int analyze_ip4_dhcp(dhcp_client_info_t *dhcp, Buffer_Struct *in)
 		LLOGD("cookie error");
 		return -2;
 	}
+	// 基本类型校验:HTYPE=ETH(1),HLEN=6
+	if (in->Data[1] != DHCP_HTYPE_ETH || in->Data[2] != 6)
+	{
+		LLOGD("htype/hlen error %d/%d", in->Data[1], in->Data[2]);
+		return -1;
+	}
 
 	if (BytesGetBe32(&in->Data[4]) != dhcp->xid)
 	{
@@ -211,7 +238,26 @@ int analyze_ip4_dhcp(dhcp_client_info_t *dhcp, Buffer_Struct *in)
 	while (in->Pos < in->MaxLen)
 	{
 __CHECK:
-		switch(in->Data[in->Pos])
+		// 边界检查,确保能读取type/len
+		if (in->Pos + 1 >= in->MaxLen)
+			break;
+		uint8_t opt = in->Data[in->Pos];
+		uint8_t len = in->Data[in->Pos + 1];
+		if (opt == DHCP_OPTION_PAD)
+		{
+			in->Pos++;
+			goto __CHECK;
+		}
+		if (opt == DHCP_OPTION_END)
+		{
+			return ack;
+		}
+		if (in->Pos + 2 + len > in->MaxLen)
+		{
+			LLOGW("option overflow opt=%d len=%d pos=%d", opt, len, in->Pos);
+			break;
+		}
+		switch(opt)
 		{
 		case DHCP_OPTION_MESSAGE_TYPE:
 			ack = in->Data[in->Pos + 2];
@@ -223,14 +269,15 @@ __CHECK:
 			if (DHCP_ACK == ack)
 			{
 				dhcp->lease_time = BytesGetBe32(&in->Data[in->Pos + 2]);
-				if (dhcp->lease_time < 60)
+				if (dhcp->lease_time < DHCP_MIN_LEASE_SEC)
 				{
-					LLOGW("lease time too short %d, set to 60", dhcp->lease_time);
-					dhcp->lease_time = 60; // 最小租约时间为60秒
+					LLOGW("lease time too short %d, set to %d", dhcp->lease_time, DHCP_MIN_LEASE_SEC);
+					dhcp->lease_time = DHCP_MIN_LEASE_SEC;
 				}
 				lease_time = dhcp->lease_time;
 				lease_time *= 1000;
 				dhcp->lease_end_time = luat_mcu_tick64_ms() + lease_time;
+				// 默认按比例,若后续解析到T1/T2会覆盖
 				dhcp->lease_p1_time = dhcp->lease_end_time - (lease_time >> 1);
 				dhcp->lease_p2_time = dhcp->lease_end_time - (lease_time >> 3);
 			}
@@ -243,20 +290,40 @@ __CHECK:
 			dhcp->gateway = BytesGetLe32(&in->Data[in->Pos + 2]);
 			break;
 		case DHCP_OPTION_DNS_SERVER:
-			dhcp->dns_server[0] = BytesGetLe32(&in->Data[in->Pos + 2]);
-			dhcp->dns_server[1] = (in->Data[in->Pos + 1] >= 8)?BytesGetLe32(&in->Data[in->Pos + 6]):0;
+			{
+				// 解析所有DNS,每4字节一个地址
+				uint8_t count = len / 4;
+				for (uint8_t i = 0; i < count && i < 2; i++)
+				{
+					uint32_t addr = BytesGetLe32(&in->Data[in->Pos + 2 + i * 4]);
+					dhcp->dns_server[i] = addr;
+				}
+				for (uint8_t i = count; i < 2; i++) dhcp->dns_server[i] = 0;
+			}
 			break;
-		case DHCP_OPTION_PAD:
-			in->Pos++;
-			goto __CHECK;
+		case 58: // Renewal Time (T1)
+			if (DHCP_ACK == ack && len == 4)
+			{
+				uint64_t t1 = BytesGetBe32(&in->Data[in->Pos + 2]);
+				if (t1 < DHCP_MIN_LEASE_SEC) t1 = DHCP_MIN_LEASE_SEC;
+				dhcp->lease_p1_time = luat_mcu_tick64_ms() + t1 * 1000;
+				LLOGD("T1(Renew)=%llu sec", t1);
+			}
+			break;
+		case 59: // Rebinding Time (T2)
+			if (DHCP_ACK == ack && len == 4)
+			{
+				uint64_t t2 = BytesGetBe32(&in->Data[in->Pos + 2]);
+				if (t2 < DHCP_MIN_LEASE_SEC) t2 = DHCP_MIN_LEASE_SEC;
+				dhcp->lease_p2_time = luat_mcu_tick64_ms() + t2 * 1000;
+				LLOGD("T2(Rebind)=%llu sec", t2);
+			}
 			break;
-		case DHCP_OPTION_END:
-			return ack;
 		default:
 			//LLOGD("jump %d,%d", in->Data[in->Pos], in->Data[in->Pos+1]);
 			break;
 		}
-		in->Pos += 2 + in->Data[in->Pos+1];
+		in->Pos += 2 + len;
 	}
 	return ack;
 }
@@ -309,41 +376,51 @@ int ip4_dhcp_run(dhcp_client_info_t *dhcp, Buffer_Struct *in, Buffer_Struct *out
 	case DHCP_STATE_WAIT_LEASE_P1_ACK:
 		if (in && (result == DHCP_ACK))
 		{
-			LLOGD("lease p1 require ip ok");
+			LLOGD("lease p1 renew ip ok");
 			dhcp->state = DHCP_STATE_WAIT_LEASE_P1;
 			break;
 		}
-		if (luat_mcu_tick64_ms() >= (dhcp->last_tx_time + 2500))
+		if (tnow >= (dhcp->last_tx_time + DHCP_RENEW_TIMEOUT_MS))
 		{
-			LLOGD("lease p1 require ip long time no ack");
+			LLOGD("lease p1 renew timeout, enter rebind phase");
 			dhcp->state = DHCP_STATE_WAIT_LEASE_P2;
 		}
 		break;
 	case DHCP_STATE_WAIT_LEASE_P2:
 		if (tnow >= dhcp->lease_p2_time)
 		{
-			dhcp->state = DHCP_STATE_WAIT_SELECT_ACK;
+			LLOGD("lease p2 rebind time reached, broadcast request");
+			// Rebind阶段使用广播,不指定server_ip
+			flag = 0x8000;
+			*remote_ip = 0xffffffff;
+			dhcp->state = DHCP_STATE_WAIT_LEASE_P2_ACK;
 			goto DHCP_NEED_REQUIRE;
 		}
 		break;
 	case DHCP_STATE_WAIT_LEASE_P2_ACK:
 		if (in && (result == DHCP_ACK))
 		{
-			LLOGD("lease p2 require ip ok");
+			LLOGD("lease p2 rebind ip ok");
 			dhcp->state = DHCP_STATE_WAIT_LEASE_P1;
 			break;
 		}
-		if (tnow >= (dhcp->last_tx_time + 2500))
+		if (tnow >= (dhcp->last_tx_time + DHCP_REBIND_TIMEOUT_MS))
 		{
-			LLOGD("lease p2 require ip long time no ack");
+			LLOGD("lease p2 rebind timeout, wait for lease expiry");
 			dhcp->state = DHCP_STATE_WAIT_LEASE_END;
 		}
 		break;
 	case DHCP_STATE_WAIT_LEASE_END:
 		if (tnow >= dhcp->lease_end_time)
 		{
-			dhcp->state = DHCP_STATE_WAIT_SELECT_ACK;
-			goto DHCP_NEED_REQUIRE;
+			LLOGD("lease expired, restart from discover");
+			dhcp->state = DHCP_STATE_DISCOVER;
+			dhcp->temp_ip = 0;
+			dhcp->server_ip = 0;
+			dhcp->ip = 0;
+			dhcp->last_tx_time = 0;
+			dhcp->discover_cnt = 0;
+			// 下一轮循环会触发DISCOVER
 		}
 		break;
 //	case DHCP_STATE_WAIT_REQUIRE_ACK:
@@ -373,42 +450,45 @@ int ip4_dhcp_run(dhcp_client_info_t *dhcp, Buffer_Struct *in, Buffer_Struct *out
 	case DHCP_STATE_WAIT_OFFER:
 		if (in && (result == DHCP_OFFER))
 		{
-			LLOGD("select offer, wait ack");
+			LLOGD("got offer, send request");
 			dhcp->state = DHCP_STATE_WAIT_SELECT_ACK;
 			dhcp->wait_selec_ack_cnt = 0;
 			goto DHCP_NEED_REQUIRE;
 		}
-		if (tnow >= (dhcp->last_tx_time + (dhcp->discover_cnt * 500) + 900))
+		// 指数退避:1s, 2s, 4s, 8s...
 		{
-			LLOGD("long time no offer, resend");
-			dhcp->discover_cnt++;
-			OS_ReInitBuffer(out, 512);
-			make_ip4_dhcp_discover_msg(dhcp, out);
-			dhcp->last_tx_time = luat_mcu_tick64_ms();
+			uint32_t backoff = DHCP_RETRY_BASE_MS << dhcp->discover_cnt;
+			if (backoff > DHCP_RETRY_MAX_MS) backoff = DHCP_RETRY_MAX_MS;
+			if (tnow >= (dhcp->last_tx_time + backoff))
+			{
+				LLOGD("no offer after %ums, resend discover (retry %d)", backoff, dhcp->discover_cnt);
+				dhcp->discover_cnt++;
+				OS_ReInitBuffer(out, 512);
+				make_ip4_dhcp_discover_msg(dhcp, out);
+				dhcp->last_tx_time = luat_mcu_tick64_ms();
+			}
 		}
 		break;
 	case DHCP_STATE_WAIT_SELECT_ACK:
 		if (in && (result == DHCP_ACK))
 		{
-//			LLOGD("need check ip %x,%x,%x,%x", dhcp->temp_ip, dhcp->submask, dhcp->gateway, dhcp->server_ip);
 			dhcp->ip = dhcp->temp_ip;
 			dhcp->state = DHCP_STATE_CHECK;
-
 			dhcp->weak_temp_ip = 0;
 			dhcp->weak_server_ip = 0;
-
-			LLOGD("DHCP get ip ready");
+			LLOGD("DHCP acquired IP %d.%d.%d.%d", 
+				(dhcp->ip) & 0xFF, (dhcp->ip >> 8) & 0xFF, 
+				(dhcp->ip >> 16) & 0xFF, (dhcp->ip >> 24) & 0xFF);
 			break;
 		}
-		if (dhcp->wait_selec_ack_cnt > 3)
+		if (dhcp->wait_selec_ack_cnt >= DHCP_MAX_SELECT_RETRIES)
 		{
-			LLOGD("select ip long time no ack");
+			LLOGD("select request timeout after %d retries", dhcp->wait_selec_ack_cnt);
 			if ((dhcp->weak_temp_ip == dhcp->temp_ip) && (dhcp->weak_server_ip == dhcp->server_ip))
 			{
-				LLOGD("get same ip and server, maybe dhcp server error, use old ip");
+				LLOGW("same offer repeated, assume server issue, accept IP");
 				dhcp->ip = dhcp->temp_ip;
 				dhcp->state = DHCP_STATE_CHECK;
-
 				dhcp->weak_temp_ip = 0;
 				dhcp->weak_server_ip = 0;
 				break;
@@ -418,6 +498,7 @@ int ip4_dhcp_run(dhcp_client_info_t *dhcp, Buffer_Struct *in, Buffer_Struct *out
 				dhcp->weak_temp_ip = dhcp->temp_ip;
 				dhcp->weak_server_ip = dhcp->server_ip;
 			}
+			// 重新DISCOVER
 			OS_ReInitBuffer(out, 512);
 			make_ip4_dhcp_discover_msg(dhcp, out);
 			dhcp->last_tx_time = luat_mcu_tick64_ms();
@@ -426,14 +507,13 @@ int ip4_dhcp_run(dhcp_client_info_t *dhcp, Buffer_Struct *in, Buffer_Struct *out
 		}
 		else
 		{
-			if (luat_mcu_tick64_ms() >= (dhcp->last_tx_time + 1900 * (dhcp->wait_selec_ack_cnt + 1)))
+			if (tnow >= (dhcp->last_tx_time + DHCP_SELECT_ACK_TIMEOUT_MS * (dhcp->wait_selec_ack_cnt + 1)))
 			{
 				dhcp->wait_selec_ack_cnt++;
-				LLOGD("select ip no ack,resend %d", dhcp->wait_selec_ack_cnt);
+				LLOGD("select request no ack, retry %d", dhcp->wait_selec_ack_cnt);
 				goto DHCP_NEED_REQUIRE;
 			}
 		}
-
 		break;
 	case DHCP_STATE_REQUIRE:
 	case DHCP_STATE_SELECT:

+ 7 - 2
components/hzfont/binding/luat_lib_hzfont.c

@@ -25,9 +25,10 @@ hzfont.init("/sd/font.ttf")
 
 /**
 初始化HzFont字体库
-@api hzfont.init([ttf_path][, cache_size])
+@api hzfont.init([ttf_path][, cache_size][, load_to_psram])
 @string ttf_path TTF字体文件路径,可选;留空则回退到内置字库(若启用)
 @int cache_size 可选,位图与码点缓存容量(支持常量 HZFONT_CACHE_128/256/512/1024/2048),默认 HZFONT_CACHE_256
+@bool load_to_psram 可选,true 时将字库整包拷贝到 PSRAM 后再解析,减少后续 IO
 @return boolean 成功返回true,失败返回false
 @usage
 -- 从文件加载,使用默认缓存 256
@@ -52,7 +53,11 @@ static int l_hzfont_init(lua_State* L) {
     if (!lua_isnoneornil(L, 2)) {
         cache_size = (uint32_t)luaL_checkinteger(L, 2);
     }
-    int result = luat_hzfont_init(ttf_path, cache_size);
+    int load_to_psram = 0;
+    if (!lua_isnoneornil(L, 3)) {
+        load_to_psram = lua_toboolean(L, 3);
+    }
+    int result = luat_hzfont_init(ttf_path, cache_size, load_to_psram);
     lua_pushboolean(L, result);
     return 1;
 }

+ 1 - 1
components/hzfont/inc/luat_hzfont.h

@@ -14,7 +14,7 @@ typedef enum {
     LUAT_HZFONT_STATE_ERROR  = 2,
 } luat_hzfont_state_t;
 
-int luat_hzfont_init(const char *ttf_path, uint32_t cache_size);
+int luat_hzfont_init(const char *ttf_path, uint32_t cache_size, int load_to_psram);
 void luat_hzfont_deinit(void);
 luat_hzfont_state_t luat_hzfont_get_state(void);
 uint32_t luat_hzfont_get_str_width(const char *utf8, unsigned char font_size);

+ 90 - 11
components/hzfont/src/luat_hzfont.c

@@ -5,8 +5,10 @@
 #include "luat_lcd.h"
 #include "luat_mem.h"
 #include "luat_mcu.h"
+#include "luat_fs.h"
 
 #include <math.h>
+#include <stdio.h>
 #include <stddef.h>
 #include <stdint.h>
 #include <string.h>
@@ -172,6 +174,48 @@ static int hzfont_is_allowed_capacity(uint32_t cap) {
     return 0;
 }
 
+/* 将字体文件完整读入内存(PSRAM),用于后续内存解析 */
+static int hzfont_load_file_to_ram(const char *path, uint8_t **out_data, size_t *out_size) {
+    if (!path || !out_data || !out_size) {
+        return TTF_ERR_RANGE;
+    }
+    *out_data = NULL;
+    *out_size = 0;
+
+    FILE *fp = luat_fs_fopen(path, "rb");
+    if (!fp) {
+        return TTF_ERR_IO;
+    }
+
+    if (luat_fs_fseek(fp, 0, SEEK_END) != 0) {
+        luat_fs_fclose(fp);
+        return TTF_ERR_IO;
+    }
+    long vsize = luat_fs_ftell(fp);
+    if (vsize <= 0) {
+        luat_fs_fclose(fp);
+        return TTF_ERR_IO;
+    }
+    if (luat_fs_fseek(fp, 0, SEEK_SET) != 0) {
+        luat_fs_fclose(fp);
+        return TTF_ERR_IO;
+    }
+    uint8_t *buf = (uint8_t *)luat_heap_malloc((size_t)vsize);
+    if (!buf) {
+        luat_fs_fclose(fp);
+        return TTF_ERR_OOM;
+    }
+    size_t n = luat_fs_fread(buf, 1, (size_t)vsize, fp);
+    luat_fs_fclose(fp);
+    if (n != (size_t)vsize) {
+        luat_heap_free(buf);
+        return TTF_ERR_IO;
+    }
+    *out_data = buf;
+    *out_size = (size_t)vsize;
+    return TTF_OK;
+}
+
 static void hzfont_cache_destroy(void) {
     if (g_hzfont_cache && g_hzfont_cache_capacity) {
         for (size_t i = 0; i < g_hzfont_cache_capacity; ++i) {
@@ -535,12 +579,11 @@ static luat_color_t hzfont_coverage_to_color(uint8_t coverage, const luat_lcd_co
 }
 
 /* 初始化字体库
- * What: 加载 TTF(外部/内置),建立码点与位图缓存。
- * Pre: 需要有效的 ttf_path 或启用内置字库宏;cache_size 仅允许 128/256/512/1024/2048。
+ * What: 加载 TTF(外部/内置),可选整包读入 PSRAM,建立码点与位图缓存。
+ * Pre: 需要有效的 ttf_path 或启用内置字库宏;cache_size 仅允许 128/256/512/1024/2048;load_to_psram=1 时需有足够 RAM
  * Post: 状态置为 READY,后续方可测宽/绘制;失败时状态为 ERROR。
- * TODO: 增加 init 参数,支持选择将字库整包拷贝到 PSRAM,避免后续 IO(外部/内置均可)。
  */
-int luat_hzfont_init(const char *ttf_path, uint32_t cache_size) {
+int luat_hzfont_init(const char *ttf_path, uint32_t cache_size, int load_to_psram) {
     if (g_ft_ctx.state == LUAT_HZFONT_STATE_READY) {
         LLOGE("font already initialized");
         return 0;
@@ -555,17 +598,53 @@ int luat_hzfont_init(const char *ttf_path, uint32_t cache_size) {
     memset(&g_ft_ctx.font, 0, sizeof(g_ft_ctx.font));
 
     int rc = TTF_ERR_RANGE;
+    uint8_t *ram_buf = NULL;
+    size_t ram_size = 0;
     if (ttf_path && ttf_path[0]) {
-        rc = ttf_load_from_file(ttf_path, &g_ft_ctx.font);
-        if (rc == TTF_OK) {
-            strncpy(g_ft_ctx.font_path, ttf_path, sizeof(g_ft_ctx.font_path) - 1);
-            g_ft_ctx.font_path[sizeof(g_ft_ctx.font_path) - 1] = 0;
+        if (load_to_psram) {
+            rc = hzfont_load_file_to_ram(ttf_path, &ram_buf, &ram_size);
+            if (rc == TTF_OK) {
+                rc = ttf_load_from_memory(ram_buf, ram_size, &g_ft_ctx.font);
+                if (rc == TTF_OK) {
+                    g_ft_ctx.font.ownsData = 1; /* 允许 ttf_unload 释放 */
+                    g_ft_ctx.font_path[0] = 0;
+                    strncpy(g_ft_ctx.font_path, ttf_path, sizeof(g_ft_ctx.font_path) - 1);
+                    g_ft_ctx.font_path[sizeof(g_ft_ctx.font_path) - 1] = 0;
+                } else {
+                    luat_heap_free(ram_buf);
+                    ram_buf = NULL;
+                }
+            }
+        } else {
+            rc = ttf_load_from_file(ttf_path, &g_ft_ctx.font);
+            if (rc == TTF_OK) {
+                strncpy(g_ft_ctx.font_path, ttf_path, sizeof(g_ft_ctx.font_path) - 1);
+                g_ft_ctx.font_path[sizeof(g_ft_ctx.font_path) - 1] = 0;
+            }
         }
     } else {
 #ifdef LUAT_CONF_USE_HZFONT_BUILTIN_TTF
-        rc = ttf_load_from_memory(hzfont_builtin_ttf, (size_t)hzfont_builtin_ttf_len, &g_ft_ctx.font);
-        if (rc == TTF_OK) {
-            g_ft_ctx.font_path[0] = '\0';
+        if (load_to_psram) {
+            ram_buf = (uint8_t *)luat_heap_malloc((size_t)hzfont_builtin_ttf_len);
+            if (!ram_buf) {
+                rc = TTF_ERR_OOM;
+            } else {
+                memcpy(ram_buf, hzfont_builtin_ttf, (size_t)hzfont_builtin_ttf_len);
+                ram_size = (size_t)hzfont_builtin_ttf_len;
+                rc = ttf_load_from_memory(ram_buf, ram_size, &g_ft_ctx.font);
+                if (rc == TTF_OK) {
+                    g_ft_ctx.font.ownsData = 1;
+                    g_ft_ctx.font_path[0] = '\0';
+                } else {
+                    luat_heap_free(ram_buf);
+                    ram_buf = NULL;
+                }
+            }
+        } else {
+            rc = ttf_load_from_memory(hzfont_builtin_ttf, (size_t)hzfont_builtin_ttf_len, &g_ft_ctx.font);
+            if (rc == TTF_OK) {
+                g_ft_ctx.font_path[0] = '\0';
+            }
         }
 #else
         LLOGE("empty ttf path and no builtin ttf");

+ 9 - 1
components/network/iperf/binding/luat_lib_iperf.c

@@ -79,7 +79,15 @@ static void iperf_start_cb(void* args) {
         LLOGD("server listen %s:%d", buff, ctx->port);
     }
     else {
-        luat_lwiperf_start_tcp_client(remote_ip, ctx->port, LWIPERF_CLIENT, iperf_report_cb, NULL, &drv->netif->ip_addr);
+        lwiperf_client_conf_t conf = {0};
+        conf.remote_addr = remote_ip;
+        conf.remote_port = ctx->port;
+        conf.type = LWIPERF_CLIENT;
+        conf.report_fn = iperf_report_cb;
+        conf.report_arg = NULL;
+        conf.local_addr = &drv->netif->ip_addr;
+        conf.amount = htonl((u32_t)-6000); // 默认测试60秒
+        luat_lwiperf_start_tcp_client(&conf);
         ipaddr_ntoa_r(remote_ip, buff2, sizeof(buff2));
         LLOGD("client connect %s --> %s:%d", buff, buff2, ctx->port);
     }

+ 14 - 7
components/network/iperf/include/luat_lwiperf.h

@@ -74,6 +74,7 @@ enum lwiperf_client_type
   LWIPERF_TRADEOFF
 };
 
+
 /** Prototype of a report function that is called when a session is finished.
     This report function can show the test results.
     @param report_type contains the test result */
@@ -81,15 +82,21 @@ typedef void (*lwiperf_report_fn)(void *arg, enum lwiperf_report_type report_typ
   const ip_addr_t* local_addr, u16_t local_port, const ip_addr_t* remote_addr, u16_t remote_port,
   u32_t bytes_transferred, u32_t ms_duration, u32_t bandwidth_kbitpsec);
 
+
+typedef struct lwiperf_client_conf
+{
+  const ip_addr_t* remote_addr;
+  u16_t remote_port;
+  enum lwiperf_client_type type;
+  lwiperf_report_fn report_fn;
+  void* report_arg;
+  const ip_addr_t* local_addr;
+  u32_t amount; /* in bytes, 0 means infinite */
+}lwiperf_client_conf_t;
+
 void* luat_lwiperf_start_tcp_server(const ip_addr_t* local_addr, u16_t local_port,
                                lwiperf_report_fn report_fn, void* report_arg);
-void* luat_lwiperf_start_tcp_server_default(lwiperf_report_fn report_fn, void* report_arg);
-void* luat_lwiperf_start_tcp_client(const ip_addr_t* remote_addr, u16_t remote_port,
-                               enum lwiperf_client_type type,
-                               lwiperf_report_fn report_fn, void* report_arg,
-                               const ip_addr_t* local_addr);
-void* luat_lwiperf_start_tcp_client_default(const ip_addr_t* remote_addr,
-                               lwiperf_report_fn report_fn, void* report_arg);
+void* luat_lwiperf_start_tcp_client(lwiperf_client_conf_t* client_conf);
 
 void  luat_lwiperf_abort(void* lwiperf_session);
 

+ 27 - 36
components/network/iperf/src/luat_lwiperf.c

@@ -292,9 +292,17 @@ static void iperf_free(size_t line, void* ptr);
    u16_t txlen_max;
    void *txptr;
    u8_t apiflags;
+   u16_t available_space;
  
    LWIP_ASSERT("conn invalid", (conn != NULL) && conn->base.tcp && (conn->base.server == 0));
  
+   /* 检查发送缓冲区是否有可用空间 */
+   available_space = tcp_sndbuf(conn->conn_pcb);
+   if (available_space == 0) {
+     /* 没有可用空间,等待下次调用 */
+     return ERR_OK;
+   }
+ 
    do {
      send_more = 0;
      if (conn->settings.amount & PP_HTONL(0x80000000)) {
@@ -341,6 +349,12 @@ static void iperf_free(size_t line, void* ptr);
        apiflags = 0; /* no copying needed */
        send_more = 1;
      }
+     
+     /* 限制发送长度不超过可用缓冲区空间 */
+     if (txlen_max > available_space) {
+       txlen_max = available_space;
+     }
+     
      txlen = txlen_max;
      do {
       #if ENABLE_PSIF
@@ -360,6 +374,11 @@ static void iperf_free(size_t line, void* ptr);
  
      if (err == ERR_OK) {
        conn->bytes_transferred += txlen;
+       /* 更新剩余可用空间 */
+       available_space -= txlen;
+       if (available_space == 0) {
+         send_more = 0; /* 缓冲区已满,停止发送 */
+       }
      } else {
        send_more = 0;
      }
@@ -673,21 +692,7 @@ static void iperf_free(size_t line, void* ptr);
    lwiperf_list_add(&conn->base);
    return ERR_OK;
  }
- 
- /**
-  * @ingroup iperf
-  * Start a TCP iperf server on the default TCP port (5001) and listen for
-  * incoming connections from iperf clients.
-  *
-  * @returns a connection handle that can be used to abort the server
-  *          by calling @ref lwiperf_abort()
-  */
- void *
- luat_lwiperf_start_tcp_server_default(lwiperf_report_fn report_fn, void *report_arg)
- {
-   return luat_lwiperf_start_tcp_server(IP_ADDR_ANY, LWIPERF_TCP_PORT_DEFAULT,
-                                   report_fn, report_arg);
- }
+
  
  /**
   * @ingroup iperf
@@ -765,19 +770,6 @@ static void iperf_free(size_t line, void* ptr);
    return ERR_OK;
  }
  
- /**
-  * @ingroup iperf
-  * Start a TCP iperf client to the default TCP port (5001).
-  *
-  * @returns a connection handle that can be used to abort the client
-  *          by calling @ref lwiperf_abort()
-  */
- void*  luat_lwiperf_start_tcp_client_default(const ip_addr_t* remote_addr,
-                                lwiperf_report_fn report_fn, void* report_arg)
- {
-   return  luat_lwiperf_start_tcp_client(remote_addr, LWIPERF_TCP_PORT_DEFAULT, LWIPERF_CLIENT,
-                                   report_fn, report_arg, NULL);
- }
  
  /**
   * @ingroup iperf
@@ -786,15 +778,14 @@ static void iperf_free(size_t line, void* ptr);
   * @returns a connection handle that can be used to abort the client
   *          by calling @ref lwiperf_abort()
   */
- void*  luat_lwiperf_start_tcp_client(const ip_addr_t* remote_addr, u16_t remote_port,
-   enum lwiperf_client_type type, lwiperf_report_fn report_fn, void* report_arg, const ip_addr_t* local_addr)
+ void*  luat_lwiperf_start_tcp_client(lwiperf_client_conf_t* client_conf)
  {
    err_t ret;
    lwiperf_settings_t settings;
    lwiperf_state_tcp_t *state = NULL;
  
    memset(&settings, 0, sizeof(settings));
-   switch (type) {
+   switch (client_conf->type) {
    case LWIPERF_CLIENT:
      /* Unidirectional tx only test */
      settings.flags = 0;
@@ -814,16 +805,16 @@ static void iperf_free(size_t line, void* ptr);
    settings.num_threads = htonl(1);
    settings.remote_port = htonl(LWIPERF_TCP_PORT_DEFAULT);
    /* TODO: implement passing duration/amount of bytes to transfer */
-   settings.amount = htonl((u32_t)-1000);
+   settings.amount = client_conf->amount;
    LLOGD("准备启动iperf客户端");
-   ret = lwiperf_tx_start_impl(remote_addr, remote_port, &settings, report_fn, report_arg, NULL, &state, local_addr);
+   ret = lwiperf_tx_start_impl(client_conf->remote_addr, client_conf->remote_port, &settings, client_conf->report_fn, client_conf->report_arg, NULL, &state, client_conf->local_addr);
    if (ret == ERR_OK) {
      LWIP_ASSERT("state != NULL", state != NULL);
-     if (type != LWIPERF_CLIENT) {
+     if (client_conf->type != LWIPERF_CLIENT) {
        /* start corresponding server now */
        lwiperf_state_tcp_t *server = NULL;
        ret = lwiperf_start_tcp_server_impl(&state->conn_pcb->local_ip, LWIPERF_TCP_PORT_DEFAULT,
-         report_fn, report_arg, (lwiperf_state_base_t *)state, &server);
+         client_conf->report_fn, client_conf->report_arg, (lwiperf_state_base_t *)state, &server);
        if (ret != ERR_OK) {
          /* starting server failed, abort client */
          luat_lwiperf_abort(state);
@@ -832,7 +823,7 @@ static void iperf_free(size_t line, void* ptr);
        /* make this server accept one connection only */
        server->specific_remote = 1;
        server->remote_addr = state->conn_pcb->remote_ip;
-       if (type == LWIPERF_TRADEOFF) {
+       if (client_conf->type == LWIPERF_TRADEOFF) {
          /* tradeoff means that the remote host connects only after the client is done,
             so keep the listen pcb open until the client is done */
          server->client_tradeoff_mode = 1;

+ 2 - 1
components/network/libhttp/luat_http_client.c

@@ -551,6 +551,7 @@ static void http_send_message(luat_http_ctrl_t *http_ctrl){
 	// 发送请求行, 主要,这里都借用了resp_buff,但这并不会与resp冲突
 	int result;
 	http_send(http_ctrl, (uint8_t *)http_ctrl->request_line, strlen((char*)http_ctrl->request_line));
+	http_send(http_ctrl, (uint8_t*)"Connection: Close\r\n", strlen("Connection: Close\r\n"));
 	// 判断自定义headers是否有host	
 	if (http_ctrl->custom_host == 0) {
 		result = snprintf_(http_ctrl->resp_buff, HTTP_RESP_BUFF_SIZE,  "Host: %s:%d\r\n", http_ctrl->host, http_ctrl->remote_port);
@@ -646,7 +647,7 @@ LUAT_RT_RET_TYPE luat_http_timer_callback(LUAT_RT_CB_PARAM){
 }
 
 static void on_tcp_closed(luat_http_ctrl_t *http_ctrl) {
-	LLOGI("on_tcp_closed %p", http_ctrl);
+	LLOGI("on_tcp_closed %p body is done %d header is complete %d", http_ctrl, http_ctrl->http_body_is_finally, http_ctrl->headers_complete);
 	int ret = 0;
 	http_ctrl->tcp_closed = 1;
 	if (http_ctrl->http_body_is_finally == 0) { // 当没有解析完成

+ 12 - 0
components/network/libsntp/luat_sntp.c

@@ -358,3 +358,15 @@ int ntp_get(int adapter_index){
     }
     return ret;
 }
+
+uint64_t luat_sntp_time64_ms() {
+    uint64_t tick64 = luat_mcu_tick64();
+    uint32_t us_period = luat_mcu_us_period();
+    uint64_t ll_sec = tick64 /us_period/ 1000 / 1000;
+    uint64_t ll_ms  = (tick64 /us_period/ 1000) % 1000;
+    uint64_t tmp = ll_sec + g_sntp_ctx.sysboot_diff_sec;
+    //LLOGD("ntp sec: sec=%llu, ms=%u", tmp, ll_ms);
+    tmp *= 1000;
+    tmp += ll_ms + g_sntp_ctx.sysboot_diff_ms;
+    return tmp;
+}

+ 10 - 0
components/network/netdrv/include/luat_netdrv_ch390h.h

@@ -27,6 +27,16 @@ typedef struct ch390h
     uint8_t rxbuff[1600];
     uint8_t txbuff[1600];
     luat_ch390h_cstring_t* txqueue[CH390H_MAX_TX_NUM];
+    char* txtmp;  // TX临时缓冲区,避免多设备冲突
+    int pkg_mem_type;  // 数据包内存类型,每个设备独立配置
+    uint16_t rx_error_count;  // 读包错误计数器
+    uint16_t tx_busy_count;  // TX忙计数器
+    uint16_t vid_pid_error_count;  // VID/PID检查失败计数器
+    uint32_t last_reset_time;  // 最后一次复位时间(毫秒)
+    uint32_t total_reset_count;  // 总复位次数
+    uint32_t total_tx_drop;  // 总丢弃发送包数
+    uint32_t total_rx_drop;  // 总丢弃接收包数
+    uint8_t flow_control;  // 流控状态:0=正常 1=背压中
 }ch390h_t;
 
 

+ 35 - 0
components/network/netdrv/include/luat_netdrv_napt.h

@@ -3,6 +3,28 @@
 
 #include "lwip/pbuf.h"
 
+// 返回值定义
+#define NAPT_RET_OK           0    // 转发成功
+#define NAPT_RET_SKIP         1    // 跳过处理,让LWIP继续
+#define NAPT_RET_NO_MAPPING  -1    // 未找到映射关系
+#define NAPT_RET_NO_MEMORY   -2    // 内存不足
+#define NAPT_RET_LOCK_FAIL   -3    // 加锁失败
+#define NAPT_RET_INVALID_CTX -4    // NAPT上下文无效
+
+// 哈希表大小(用于加速查找)
+// 根据压力测试结果优化:适应高并发场景,负载因子0.5-0.6
+#ifndef NAPT_HASH_TABLE_SIZE
+#if defined(TYPE_EC718HM)
+#define NAPT_HASH_TABLE_SIZE 16384  // 8K映射,负载因子0.5 (~131KB,两表共计)
+#elif defined(TYPE_EC718PM)
+#define NAPT_HASH_TABLE_SIZE 8192   // 4K映射,负载因子0.5 (~65KB)
+#else
+#define NAPT_HASH_TABLE_SIZE 4096   // 2K映射,负载因子0.5 (~33KB)
+#endif
+#endif
+#define NAPT_HASH_INVALID_INDEX 0xFFFF
+#define NAPT_HASH_MAX_PROBE 96      // 提高到96,应对更高的冲突率
+
 // #define IP_NAPT_TIMEOUT_MS_TCP (30*60*1000)
 #define IP_NAPT_TIMEOUT_MS_TCP_DISCON (20*1000)
 #ifndef NAPT_TCP_MAP_ITEM_MAX
@@ -66,6 +88,11 @@ typedef struct luat_netdrv_napt_llist
     luat_netdrv_napt_tcpudp_t item;
 }luat_netdrv_napt_llist_t;
 
+// 哈希表项,用于加速查找(纯线性探测,无链表)
+typedef struct {
+    uint16_t item_index;  // 映射项在items数组中的索引,NAPT_HASH_INVALID_INDEX表示空槽
+} napt_hash_entry_t;
+
 typedef struct luat_netdrv_napt_ctx{
     uint32_t ip_tp;
     size_t clean_tm;
@@ -74,12 +101,19 @@ typedef struct luat_netdrv_napt_ctx{
     luat_netdrv_napt_tcpudp_t items[NAPT_TCP_MAP_ITEM_MAX];
     luat_rtos_mutex_t lock;
     uint32_t *port_used;
+    // 哈希表用于加速查找(WAN->LAN方向)
+    napt_hash_entry_t *hash_table_wan2lan;  // 按(wnet_ip, wnet_port, wnet_local_port)索引
+    // 哈希表用于加速查找(LAN->WAN方向)
+    napt_hash_entry_t *hash_table_lan2wan;  // 按(inet_ip, inet_port, wnet_ip, wnet_port)索引
 }luat_netdrv_napt_ctx_t;
 
 int luat_napt_icmp_handle(napt_ctx_t* ctx);
 int luat_napt_tcp_handle(napt_ctx_t* ctx);
 int luat_napt_udp_handle(napt_ctx_t* ctx);
 
+void luat_netdrv_napt_tcp_cleanup(void);
+void luat_netdrv_napt_udp_cleanup(void);
+
 int luat_netdrv_napt_pkg_input(int id, uint8_t* buff, size_t len);
 
 int luat_netdrv_napt_pkg_input_pbuf(int id, struct pbuf* p);
@@ -89,5 +123,6 @@ int luat_netdrv_napt_tcp_wan2lan(napt_ctx_t* ctx, luat_netdrv_napt_tcpudp_t* map
 int luat_netdrv_napt_tcp_lan2wan(napt_ctx_t* ctx, luat_netdrv_napt_tcpudp_t* mapping, luat_netdrv_napt_ctx_t *napt_ctx);
 
 void luat_netdrv_napt_enable(int adapter_id);
+void luat_netdrv_napt_disable(void);
 
 #endif

+ 43 - 29
components/network/netdrv/src/ch390h_api.c

@@ -25,7 +25,10 @@ int luat_ch390h_read(ch390h_t* ch, uint8_t addr, uint16_t count, uint8_t* buff)
         luat_gpio_set(ch->cspin, 0);
         ret = luat_spi_send(ch->spiid, tmp, 1);
         if (ret != 1) {
-            LLOGE("luat_spi_send 1 but ret %d", ret);
+            LLOGE("luat_spi_send失败 expect=1 ret=%d", ret);
+            luat_gpio_set(ch->cspin, 1);
+            luat_spi_unlock(ch->spiid);
+            return -1;
         }
         char* ptr = (char*)buff;
         while (count > 0) {
@@ -56,30 +59,33 @@ int luat_ch390h_read(ch390h_t* ch, uint8_t addr, uint16_t count, uint8_t* buff)
     return 0;
 }
 
-static char *s_txtmp;
 int luat_ch390h_write(ch390h_t* ch, uint8_t addr, uint16_t count, uint8_t* buff) {
-    if (s_txtmp == NULL) {
-        // LLOGI("分配txtmp缓冲区 3k");
-        s_txtmp = luat_heap_malloc(3 * 1024);
+    if (ch->txtmp == NULL) {
+        ch->txtmp = luat_heap_malloc(3 * 1024);
+        if (ch->txtmp == NULL) {
+            LLOGE("txtmp内存分配失败");
+            return -1;
+        }
     }
     if (count > 1600) {
-        return 0; // 直接不发送
+        LLOGW("数据包过大(%d),丢弃", count);
+        return -2;
     }
     luat_spi_lock(ch->spiid);
     if (addr == 0x78) {
-        s_txtmp[0] = addr | 0x80;
-        memcpy(s_txtmp+1, buff, count);
+        ch->txtmp[0] = addr | 0x80;
+        memcpy(ch->txtmp+1, buff, count);
         luat_gpio_set(ch->cspin, 0);
-        luat_spi_send(ch->spiid, (const char* )s_txtmp, 1 + count);
+        luat_spi_send(ch->spiid, (const char* )ch->txtmp, 1 + count);
         luat_gpio_set(ch->cspin, 1);
     }
     else {
         for (size_t i = 0; i < count; i++)
         {
-            s_txtmp[0] = (addr + i) | 0x80;
-            s_txtmp[1] = buff[i];
+            ch->txtmp[0] = (addr + i) | 0x80;
+            ch->txtmp[1] = buff[i];
             luat_gpio_set(ch->cspin, 0);
-            luat_spi_send(ch->spiid, (const char* )s_txtmp, 2);
+            luat_spi_send(ch->spiid, (const char* )ch->txtmp, 2);
             luat_gpio_set(ch->cspin, 1);
         }
     }
@@ -201,31 +207,39 @@ int luat_ch390h_write_pkg(ch390h_t* ch, uint8_t *buff, uint16_t len) {
     uint8_t NCR = 0;
     uint8_t NSR = 0;
     if (TCR & 0x01) {
-        // busy!!
-        for (size_t i = 0; i < 16; i++)
+        // busy!! 增加重试次数
+        for (size_t i = 0; i < 100; i++)
         {
             luat_timer_us_delay(10);
             luat_ch390h_read(ch, 0x02, 1, tmp);
             TCR = tmp[0];
-            if (TCR & 0x01) {
-                continue;
+            if (!(TCR & 0x01)) {
+                ch->tx_busy_count = 0;  // 成功后清零计数器
+                break;
             }
-            break;
         }
         if (TCR & 0x01) {
-            LLOGW("tx busy, drop pkg len %d and reset ch390!!", len);
-            // 读出NCR 和 NSR
-            luat_ch390h_read(ch, 0x00, 1, tmp);
-            NCR = tmp[0];
-            luat_ch390h_read(ch, 0x01, 1, tmp);
-            NSR = tmp[0];
-            LLOGD("NCR %02X NSR %02X", NCR, NSR);
-            LLOGD("NCR->FDR %02X NSR->SPEED %02X NSR->LINKST %02X", NCR & (1<<3), NSR & (1<<7), NSR & (1<<6));
-            luat_ch390h_software_reset(ch);
-            luat_timer_mdelay(2);
-            return 0;
+            ch->tx_busy_count++;
+            LLOGW("tx busy, drop pkg len %d, busy_count=%d", len, ch->tx_busy_count);
+            // 只有连续多次TX忙且距离上次复位超过5秒才复位
+            extern uint64_t luat_mcu_tick64_ms(void);
+            uint32_t now = (uint32_t)luat_mcu_tick64_ms();
+            if (ch->tx_busy_count >= 10 && (now - ch->last_reset_time > 5000)) {
+                // 读出NCR 和 NSR
+                luat_ch390h_read(ch, 0x00, 1, tmp);
+                NCR = tmp[0];
+                luat_ch390h_read(ch, 0x01, 1, tmp);
+                NSR = tmp[0];
+                LLOGE("连续TX忙超过阈值,执行复位 NCR=%02X NSR=%02X", NCR, NSR);
+                LLOGD("NCR->FDR %02X NSR->SPEED %02X NSR->LINKST %02X", NCR & (1<<3), NSR & (1<<7), NSR & (1<<6));
+                luat_ch390h_software_reset(ch);
+                ch->tx_busy_count = 0;
+                ch->last_reset_time = now;
+                ch->total_reset_count++;
+                luat_timer_mdelay(2);
+            }
+            return -1;
         }
-        // return 1;
     }
     luat_ch390h_write_reg(ch, 0x55, 2);     // 发数据之前重置一下tx的内存指针
     // 写入下一个数据

+ 66 - 28
components/network/netdrv/src/ch390h_task.c

@@ -48,8 +48,6 @@ static uint64_t warn_msg_tm;
 
 static uint32_t s_ch390h_mode; // 0 -- PULL 模式, 1 == IRQ 模式
 
-static int pkg_mem_type = LUAT_HEAP_AUTO;
-
 static int ch390h_irq_cb(void *data, void *args) {
     uint32_t len = 0;
     luat_rtos_queue_get_cnt(qt, &len);
@@ -98,13 +96,13 @@ static int ch390h_bootup(ch390h_t* ch) {
     return 0;
 }
 
-static luat_ch390h_cstring_t* new_cstring(uint16_t len) {
+static luat_ch390h_cstring_t* new_cstring(ch390h_t* ch, uint16_t len) {
     size_t total = 0;
     size_t used = 0;
     size_t max_used = 0;
-    luat_meminfo_opt_sys(pkg_mem_type, &total, &used, &max_used);
+    luat_meminfo_opt_sys(ch->pkg_mem_type, &total, &used, &max_used);
     if (total > 0 && total - used > 32*1024) { // 最少留32k给系统用
-        luat_ch390h_cstring_t* cs = luat_heap_opt_malloc(pkg_mem_type, sizeof(luat_ch390h_cstring_t) + len - 4);
+        luat_ch390h_cstring_t* cs = luat_heap_opt_malloc(ch->pkg_mem_type, sizeof(luat_ch390h_cstring_t) + len - 4);
         if (cs == NULL) {
             LLOGE("有剩余内存不多但分配失败! total %d used %d max_used %d len %d", total, used, max_used, len);
         }
@@ -118,20 +116,29 @@ static void send_msg_cs(ch390h_t* ch, luat_ch390h_cstring_t* cs) {
     uint32_t len = 0;
     luat_rtos_queue_get_cnt(qt, &len);
     uint64_t tm;
+    
+    // 流控背压机制
+    if (len >= 800) {
+        ch->flow_control = 1;  // 进入背压状态
+    } else if (len < 400) {
+        ch->flow_control = 0;  // 解除背压
+    }
+    
     if (len >= 1000) {
         tm = luat_mcu_tick64_ms();
         if (tm - warn_msg_tm > 1000) {
             warn_msg_tm = tm;
-            LLOGW("太多待处理消息了!!! %d", len);
+            LLOGE("队列已满,丢弃数据包 len=%d", len);
         }
-        luat_heap_opt_free(pkg_mem_type, cs);
+        ch->total_tx_drop++;
+        luat_heap_opt_free(ch->pkg_mem_type, cs);
         return;
     }
-    if (len > 512) {
+    if (len > 600) {
         tm = luat_mcu_tick64_ms();
         if (tm - warn_msg_tm > 1000) {
             warn_msg_tm = tm;
-            LLOGD("当前消息数量 %d", len);
+            LLOGW("队列负载较高 len=%d flow_control=%d", len, ch->flow_control);
         }
     }
     
@@ -143,13 +150,13 @@ static void send_msg_cs(ch390h_t* ch, luat_ch390h_cstring_t* cs) {
     int ret = luat_rtos_queue_send(qt, &evt, sizeof(pkg_evt_t), 0);
     if (ret) {
         LLOGE("消息发送失败 %d", ret);
-        luat_heap_opt_free(pkg_mem_type, cs);
+        luat_heap_opt_free(ch->pkg_mem_type, cs);
     }
 }
 
 static void ch390h_dataout(luat_netdrv_t* drv, void* userdata, uint8_t* buff, uint16_t len) {
     ch390h_t* ch = (ch390h_t*)userdata;
-    luat_ch390h_cstring_t* cs = new_cstring(len);
+    luat_ch390h_cstring_t* cs = new_cstring(ch, len);
     if (cs == NULL) {
         return;
     }
@@ -159,7 +166,7 @@ static void ch390h_dataout(luat_netdrv_t* drv, void* userdata, uint8_t* buff, ui
 }
 
 static void ch390h_dataout_pbuf(ch390h_t* ch, struct pbuf* p) {
-    luat_ch390h_cstring_t* cs = new_cstring(p->tot_len);
+    luat_ch390h_cstring_t* cs = new_cstring(ch, p->tot_len);
     if (cs == NULL) {
         return;
     }
@@ -210,19 +217,35 @@ static int check_vid_pid(ch390h_t* ch) {
     uint8_t buff[6] = {0};
     luat_ch390h_read_vid_pid(ch, buff);
     if (0 == memcmp(buff, "\x00\x1C\x51\x91", 4)) {
-        return 0; // 第一次就读取成功, 那就马上返回
+        ch->vid_pid_error_count = 0;  // 成功后清零计数器
+        return 0;
     }
     // 再读一次
     luat_ch390h_read_vid_pid(ch, buff);
     if (0 != memcmp(buff, "\x00\x1C\x51\x91", 4)) {
+        ch->vid_pid_error_count++;
         uint64_t tnow = luat_mcu_tick64_ms();
         if (tnow - warn_vid_pid_tm > 2000) {
-            LLOGE("读取vid/pid失败!请检查接线!! %d %d %02X%02X%02X%02X", ch->spiid, ch->cspin, buff[0], buff[1], buff[2], buff[3]);
+            // 前几次用WARN,多次失败用ERROR
+            if (ch->vid_pid_error_count < 10) {
+                LLOGW("读取vid/pid失败 spi=%d cs=%d %02X%02X%02X%02X error_count=%d", 
+                      ch->spiid, ch->cspin, buff[0], buff[1], buff[2], buff[3], ch->vid_pid_error_count);
+            } else {
+                LLOGE("读取vid/pid持续失败!请检查接线!! spi=%d cs=%d %02X%02X%02X%02X error_count=%d", 
+                      ch->spiid, ch->cspin, buff[0], buff[1], buff[2], buff[3], ch->vid_pid_error_count);
+            }
             warn_vid_pid_tm = tnow;
         }
+        // 连续多次失败后回退到初始状态
+        if (ch->vid_pid_error_count >= 20 && ch->status >= 2) {
+            LLOGE("VID/PID检查连续失败超过阈值,回退到初始状态");
+            ch->status = 0;
+            ch->init_done = 0;
+            ch->vid_pid_error_count = 0;
+        }
         return -1;
     }
-    // LLOGE("读取vid/pid成功!!! %d %d %02X%02X%02X%02X", ch->spiid, ch->cspin, buff[0], buff[1], buff[2], buff[3]);
+    ch->vid_pid_error_count = 0;
     return 0;
 }
 
@@ -324,19 +347,30 @@ static int task_loop_one(ch390h_t* ch, luat_ch390h_cstring_t* cs) {
     if (NSR & 0x01) {
         ret = luat_ch390h_read_pkg(ch, ch->rxbuff, &len);
         if (ret) {
-            LLOGE("读数据包报错,立即复位模组 ret %d spi %d cs %d", ret, ch->spiid, ch->cspin);
-            luat_ch390h_write_reg(ch, 0x05, 0);
-            luat_ch390h_write_reg(ch, 0x55, 1);
-            luat_ch390h_write_reg(ch, 0x75, 0);
-            luat_rtos_task_sleep(1); // 是否真的需要呢??
-            luat_ch390h_basic_config(ch);
-            luat_ch390h_set_phy(ch, 1);
-            luat_ch390h_set_rx(ch, 1);
-            if (ch->intpin != 255) {
-                luat_ch390h_write_reg(ch, 0x7F, 1); // 开启接收中断
+            ch->rx_error_count++;
+            LLOGW("读数据包报错 ret=%d spi=%d cs=%d, error_count=%d", ret, ch->spiid, ch->cspin, ch->rx_error_count);
+            // 只有连续多次错误且距离上次复位超过3秒才执行复位
+            uint32_t now = (uint32_t)luat_mcu_tick64_ms();
+            if (ch->rx_error_count >= 5 && (now - ch->last_reset_time > 3000)) {
+                LLOGE("连续读包错误超过阈值,执行复位");
+                luat_ch390h_write_reg(ch, 0x05, 0);
+                luat_ch390h_write_reg(ch, 0x55, 1);
+                luat_ch390h_write_reg(ch, 0x75, 0);
+                luat_rtos_task_sleep(1);
+                luat_ch390h_basic_config(ch);
+                luat_ch390h_set_phy(ch, 1);
+                luat_ch390h_set_rx(ch, 1);
+                if (ch->intpin != 255) {
+                    luat_ch390h_write_reg(ch, 0x7F, 1);
+                }
+                ch->rx_error_count = 0;
+                ch->last_reset_time = now;
+                ch->total_reset_count++;
             }
             return 0;
         }
+        // 读取成功,清除错误计数
+        ch->rx_error_count = 0;
         if (len > 0) {
             NETDRV_STAT_IN(ch->netdrv, len);
             // 收到数据, 开始后续处理
@@ -410,7 +444,7 @@ static int task_wait_msg(uint32_t timeout) {
         ret = task_loop(ch, cs);
         if (cs) {
             // remain_tx_size -= cs->len;
-            luat_heap_opt_free(pkg_mem_type, cs);
+            luat_heap_opt_free(ch->pkg_mem_type, cs);
             cs = NULL;
         }
         return 1; // 拿到消息, 那队列里可能还有消息, 马上执行下一轮操作
@@ -455,12 +489,16 @@ static void ch390_task_main(void* args) {
 void luat_ch390h_task_start(void) {
     int ret = 0;
     if (ch390h_task_handle == NULL) {
+        // 为所有CH390H设备初始化pkg_mem_type
         size_t total = 0;
         size_t used = 0;
         size_t max_used = 0;
         luat_meminfo_opt_sys(LUAT_HEAP_PSRAM, &total, &used, &max_used);
-        if (total > 1024 * 512) {
-            pkg_mem_type = LUAT_HEAP_PSRAM;
+        int default_mem_type = (total > 1024 * 512) ? LUAT_HEAP_PSRAM : LUAT_HEAP_AUTO;
+        for (size_t i = 0; i < MAX_CH390H_NUM; i++) {
+            if (ch390h_drvs[i] != NULL) {
+                ch390h_drvs[i]->pkg_mem_type = default_mem_type;
+            }
         }
         ret = luat_rtos_queue_create(&qt, 1024, sizeof(pkg_evt_t));
         if (ret) {

+ 41 - 13
components/network/netdrv/src/luat_netdrv_ch390h.c

@@ -28,6 +28,27 @@ extern err_t luat_netdrv_etharp_output(struct netif *netif, struct pbuf *q, cons
 
 extern err_t ch390_netif_output(struct netif *netif, struct pbuf *p);
 
+// 检查设备是否已注册
+static int check_device_duplicate(ch390h_t* ch) {
+    for (size_t i = 0; i < MAX_CH390H_NUM; i++) {
+        if (ch390h_drvs[i] == NULL) continue;
+        
+        if (ch390h_drvs[i]->adapter_id == ch->adapter_id) {
+            LLOGE("已经注册过相同的adapter_id %d", ch->adapter_id);
+            return -1;
+        }
+        if (ch390h_drvs[i]->spiid == ch->spiid && ch390h_drvs[i]->cspin == ch->cspin) {
+            LLOGE("已经注册过相同的spi+cs %d %d", ch->spiid, ch->cspin);
+            return -2;
+        }
+        if (ch390h_drvs[i]->intpin != 255 && ch390h_drvs[i]->intpin == ch->intpin) {
+            LLOGE("已经注册过相同的int脚 %d", ch->intpin);
+            return -3;
+        }
+    }
+    return 0;
+}
+
 static int ch390h_ctrl(luat_netdrv_t* drv, void* userdata, int cmd, void* buff, size_t len) {
     ch390h_t* ch = (ch390h_t*)userdata;
     if (ch == NULL) {
@@ -94,6 +115,16 @@ luat_netdrv_t* luat_netdrv_ch390h_setup(luat_netdrv_conf_t *cfg) {
     memset(drv, 0, sizeof(luat_netdrv_t));
     memset(ulwip, 0, sizeof(ulwip_ctx_t));
 
+    ch->txtmp = NULL;  // 延迟分配
+    ch->pkg_mem_type = LUAT_HEAP_AUTO;  // 默认使用AUTO内存
+    ch->rx_error_count = 0;
+    ch->tx_busy_count = 0;
+    ch->vid_pid_error_count = 0;
+    ch->last_reset_time = 0;
+    ch->total_reset_count = 0;
+    ch->total_tx_drop = 0;
+    ch->total_rx_drop = 0;
+    ch->flow_control = 0;
     ch->adapter_id = cfg->id;
     ch->cspin = cfg->cspin;
     ch->spiid = cfg->spiid;
@@ -105,21 +136,15 @@ luat_netdrv_t* luat_netdrv_ch390h_setup(luat_netdrv_conf_t *cfg) {
 
     drv->ulwip = ulwip;
 
+    // 检查设备是否重复注册
+    if (check_device_duplicate(ch) != 0) {
+        goto clean;
+    }
+
+    // 查找空位并注册设备
     for (size_t i = 0; i < MAX_CH390H_NUM; i++)
     {
         if (ch390h_drvs[i] != NULL) {
-            if (ch390h_drvs[i]->adapter_id == ch->adapter_id) {
-                LLOGE("已经注册过相同的adapter_id %d", ch->adapter_id);
-                goto clean;
-            }
-            if (ch390h_drvs[i]->spiid == ch->spiid  && ch390h_drvs[i]->cspin == ch->cspin) {
-                LLOGE("已经注册过相同的spi+cs %d %d",ch->spiid, ch->cspin);
-                goto clean;
-            }
-            if (ch390h_drvs[i]->intpin != 255 && ch390h_drvs[i]->intpin == ch->intpin) {
-                LLOGE("已经注册过相同的int脚 %d", ch->intpin);
-                goto clean;
-            }
             continue;
         }
         ch390h_drvs[i] = ch;
@@ -142,7 +167,10 @@ luat_netdrv_t* luat_netdrv_ch390h_setup(luat_netdrv_conf_t *cfg) {
     }
     LLOGE("已经没有CH390H空位了!!!");
 clean:
-    if (ch) luat_heap_free(ch);
+    if (ch) {
+        if (ch->txtmp) luat_heap_free(ch->txtmp);
+        luat_heap_free(ch);
+    }
     if (netif) luat_heap_free(netif);
     if (drv) luat_heap_free(drv);
     if (ulwip) luat_heap_free(ulwip);

+ 377 - 49
components/network/netdrv/src/luat_netdrv_napt.c

@@ -23,9 +23,15 @@
 #define LUAT_LOG_TAG "netdrv.napt"
 #include "luat_log.h"
 
+// 超时时间定义
+#define NAPT_TCP_TIMEOUT_MS        (20*60*1000)   // TCP连接超时: 20分钟
+#define NAPT_TCP_DISCON_TIMEOUT_MS (20*1000)      // TCP断开连接超时: 20秒
+#define NAPT_UDP_TIMEOUT_MS        (2*60*1000)    // UDP超时: 2分钟
+#define NAPT_CLEANUP_INTERVAL_SEC  (5)            // 清理周期: 5秒
+
 #define ICMP_MAP_SIZE (32)
-#define UDP_MAP_TIMEOUT (60 * 1000)
 #define NAPT_MAX_PACKET_SIZE (1520)
+#define NAPT_MUTEX_TIMEOUT_MS (2000)  // 统一锁超时时间
 /* napt icmp id range: 3000-65535 */
 #define NAPT_ICMP_ID_RANGE_START     0xBB8
 #define NAPT_ICMP_ID_RANGE_END       0xFFFF
@@ -39,6 +45,8 @@ luat_netdrv_napt_ctx_t *g_napt_udp_ctx;
 // 端口分配
 #define NAPT_PORT_RANGE_START     0x1BBC
 #define NAPT_PORT_RANGE_END       0x6AAA
+#define NAPT_PORT_COUNT           (NAPT_PORT_RANGE_END - NAPT_PORT_RANGE_START + 1)
+#define NAPT_PORT_BITMAP_SIZE     ((NAPT_PORT_COUNT + 31) / 32 * sizeof(uint32_t))
 
 #define u32 uint32_t
 #define u16 uint16_t
@@ -46,12 +54,227 @@ luat_netdrv_napt_ctx_t *g_napt_udp_ctx;
 #define NAPT_ETH_HDR_LEN             sizeof(struct ethhdr)
 #define NAPT_CHKSUM_16BIT_LEN        sizeof(u16)
 
+#ifdef NAPT_DEBUG
 static void check_it(const char* tag, luat_netdrv_napt_llist_t* it, luat_netdrv_napt_llist_t* prev, size_t id) {
     uint32_t tmp = (uint32_t)it;
     if (tmp == 0 || tmp > 0xc300000) {
         LLOGE("why %s cur %p prev %p id %ld", tag, it, prev, id);
     }
 }
+#endif
+
+// ============ 哈希表优化函数 ============
+
+// WAN->LAN方向的哈希函数:基于(wnet_ip, wnet_port, wnet_local_port)
+__NETDRV_CODE_IN_RAM__ static inline uint32_t napt_hash_wan2lan(uint32_t wnet_ip, uint16_t wnet_port, uint16_t wnet_local_port) {
+    // FNV-1a哈希 + 位运算优化(2的幂次方可用位与代替模运算)
+    uint32_t hash = 2166136261U;
+    hash ^= wnet_ip;
+    hash *= 16777619U;
+    hash ^= ((uint32_t)wnet_port << 16) | wnet_local_port;
+    hash *= 16777619U;
+    return hash & (NAPT_HASH_TABLE_SIZE - 1);
+}
+
+// LAN->WAN方向的哈希函数:基于(inet_ip, inet_port, wnet_ip, wnet_port)
+__NETDRV_CODE_IN_RAM__ static inline uint32_t napt_hash_lan2wan(uint32_t inet_ip, uint16_t inet_port, uint32_t wnet_ip, uint16_t wnet_port) {
+    uint32_t hash = 2166136261U;
+    hash ^= inet_ip;
+    hash *= 16777619U;
+    hash ^= ((uint32_t)inet_port << 16) | wnet_port;
+    hash *= 16777619U;
+    hash ^= wnet_ip;
+    hash *= 16777619U;
+    return hash & (NAPT_HASH_TABLE_SIZE - 1);
+}
+
+// 初始化哈希表
+static int napt_hash_init(luat_netdrv_napt_ctx_t *ctx) {
+    if (ctx->hash_table_wan2lan == NULL) {
+        ctx->hash_table_wan2lan = luat_heap_malloc(NAPT_HASH_TABLE_SIZE * sizeof(napt_hash_entry_t));
+        if (ctx->hash_table_wan2lan == NULL) {
+            LLOGE("哈希表wan2lan分配失败,需要 %d 字节", NAPT_HASH_TABLE_SIZE * sizeof(napt_hash_entry_t));
+            return -1;
+        }
+        for (size_t i = 0; i < NAPT_HASH_TABLE_SIZE; i++) {
+            ctx->hash_table_wan2lan[i].item_index = NAPT_HASH_INVALID_INDEX;
+        }
+    }
+    if (ctx->hash_table_lan2wan == NULL) {
+        ctx->hash_table_lan2wan = luat_heap_malloc(NAPT_HASH_TABLE_SIZE * sizeof(napt_hash_entry_t));
+        if (ctx->hash_table_lan2wan == NULL) {
+            LLOGE("哈希表lan2wan分配失败,需要 %d 字节", NAPT_HASH_TABLE_SIZE * sizeof(napt_hash_entry_t));
+            luat_heap_free(ctx->hash_table_wan2lan);
+            ctx->hash_table_wan2lan = NULL;
+            return -1;
+        }
+        for (size_t i = 0; i < NAPT_HASH_TABLE_SIZE; i++) {
+            ctx->hash_table_lan2wan[i].item_index = NAPT_HASH_INVALID_INDEX;
+        }
+    }
+    return 0;
+}
+
+// 向哈希表添加映射(WAN->LAN) - 纯线性探测
+__NETDRV_CODE_IN_RAM__ static int napt_hash_add_wan2lan(luat_netdrv_napt_ctx_t *ctx, uint16_t item_idx, luat_netdrv_napt_tcpudp_t *item) {
+    if (ctx->hash_table_wan2lan == NULL) return -1;
+    
+    uint32_t hash = napt_hash_wan2lan(item->wnet_ip, item->wnet_port, item->wnet_local_port);
+    
+    // 线性探测找空槽,限制最大探测次数防止满表死循环
+    for (size_t i = 0; i < NAPT_HASH_MAX_PROBE; i++) {
+        uint32_t probe = (hash + i) & (NAPT_HASH_TABLE_SIZE - 1);
+        if (ctx->hash_table_wan2lan[probe].item_index == NAPT_HASH_INVALID_INDEX) {
+            ctx->hash_table_wan2lan[probe].item_index = item_idx;
+            return 0;
+        }
+    }
+    LLOGE("哈希表wan2lan已满,无法添加项 %d(冲突深度超过%d)", item_idx, NAPT_HASH_MAX_PROBE);
+    return -1;
+}
+
+// 向哈希表添加映射(LAN->WAN) - 纯线性探测
+__NETDRV_CODE_IN_RAM__ static int napt_hash_add_lan2wan(luat_netdrv_napt_ctx_t *ctx, uint16_t item_idx, luat_netdrv_napt_tcpudp_t *item) {
+    if (ctx->hash_table_lan2wan == NULL) return -1;
+    
+    uint32_t hash = napt_hash_lan2wan(item->inet_ip, item->inet_port, item->wnet_ip, item->wnet_port);
+    
+    // 线性探测找空槽,限制最大探测次数防止满表死循环
+    for (size_t i = 0; i < NAPT_HASH_MAX_PROBE; i++) {
+        uint32_t probe = (hash + i) & (NAPT_HASH_TABLE_SIZE - 1);
+        if (ctx->hash_table_lan2wan[probe].item_index == NAPT_HASH_INVALID_INDEX) {
+            ctx->hash_table_lan2wan[probe].item_index = item_idx;
+            return 0;
+        }
+    }
+    LLOGE("哈希表lan2wan已满,无法添加项 %d(冲突深度超过%d)", item_idx, NAPT_HASH_MAX_PROBE);
+    return -1;
+}
+
+// 从哈希表查找映射(WAN->LAN) - 纯线性探测
+__NETDRV_CODE_IN_RAM__ static int napt_hash_find_wan2lan(luat_netdrv_napt_ctx_t *ctx, uint32_t wnet_ip, uint16_t wnet_port, uint16_t wnet_local_port) {
+    if (ctx->hash_table_wan2lan == NULL) return -1;
+    
+    uint32_t hash = napt_hash_wan2lan(wnet_ip, wnet_port, wnet_local_port);
+    
+    // 线性探测查找,限制最大探测次数
+    for (size_t i = 0; i < NAPT_HASH_MAX_PROBE; i++) {
+        uint32_t probe = (hash + i) & (NAPT_HASH_TABLE_SIZE - 1);
+        uint16_t idx = ctx->hash_table_wan2lan[probe].item_index;
+        
+        if (idx == NAPT_HASH_INVALID_INDEX) {
+            return -1;  // 遇到空槽说明不存在
+        }
+        
+        // 防御性检查:确保索引有效
+        if (idx >= ctx->item_last) {
+            LLOGE("哈希表wan2lan数据损坏,idx %d >= item_last %d", idx, ctx->item_last);
+            return -1;
+        }
+        
+        luat_netdrv_napt_tcpudp_t *item = &ctx->items[idx];
+        if (item->is_vaild == 0) continue;  // 跳过已删除的项
+        
+        if (item->wnet_ip == wnet_ip && item->wnet_port == wnet_port && item->wnet_local_port == wnet_local_port) {
+            return idx;
+        }
+    }
+    return -1;
+}
+
+// 从哈希表查找映射(LAN->WAN) - 纯线性探测
+__NETDRV_CODE_IN_RAM__ static int napt_hash_find_lan2wan(luat_netdrv_napt_ctx_t *ctx, uint32_t inet_ip, uint16_t inet_port, uint32_t wnet_ip, uint16_t wnet_port) {
+    if (ctx->hash_table_lan2wan == NULL) return -1;
+    
+    uint32_t hash = napt_hash_lan2wan(inet_ip, inet_port, wnet_ip, wnet_port);
+    
+    // 线性探测查找,限制最大探测次数
+    for (size_t i = 0; i < NAPT_HASH_MAX_PROBE; i++) {
+        uint32_t probe = (hash + i) & (NAPT_HASH_TABLE_SIZE - 1);
+        uint16_t idx = ctx->hash_table_lan2wan[probe].item_index;
+        
+        if (idx == NAPT_HASH_INVALID_INDEX) {
+            return -1;  // 遇到空槽说明不存在
+        }
+        
+        // 防御性检查:确保索引有效
+        if (idx >= ctx->item_last) {
+            LLOGE("哈希表lan2wan数据损坏,idx %d >= item_last %d", idx, ctx->item_last);
+            return -1;
+        }
+        
+        luat_netdrv_napt_tcpudp_t *item = &ctx->items[idx];
+        if (item->is_vaild == 0) continue;  // 跳过已删除的项
+        
+        if (item->inet_ip == inet_ip && item->inet_port == inet_port && 
+            item->wnet_ip == wnet_ip && item->wnet_port == wnet_port) {
+            return idx;
+        }
+    }
+    return -1;
+}
+
+// 重建哈希表(在清理后调用)
+static void napt_hash_rebuild(luat_netdrv_napt_ctx_t *ctx) {
+    if (ctx->hash_table_wan2lan) {
+        for (size_t i = 0; i < NAPT_HASH_TABLE_SIZE; i++) {
+            ctx->hash_table_wan2lan[i].item_index = NAPT_HASH_INVALID_INDEX;
+        }
+    }
+    if (ctx->hash_table_lan2wan) {
+        for (size_t i = 0; i < NAPT_HASH_TABLE_SIZE; i++) {
+            ctx->hash_table_lan2wan[i].item_index = NAPT_HASH_INVALID_INDEX;
+        }
+    }
+    
+    // 重新添加所有有效映射
+    for (size_t i = 0; i < ctx->item_last; i++) {
+        if (ctx->items[i].is_vaild == 0) continue;  // 跳过已删除的项
+        
+        if (napt_hash_add_wan2lan(ctx, i, &ctx->items[i]) != 0) {
+            LLOGE("哈希表wan2lan重建时添加失败,项 %d", i);
+        }
+        if (napt_hash_add_lan2wan(ctx, i, &ctx->items[i]) != 0) {
+            LLOGE("哈希表lan2wan重建时添加失败,项 %d", i);
+        }
+    }
+}
+
+// ============ 哈希表优化函数结束 ============
+
+// 统计哈希表使用情况(调试用)
+#ifdef NAPT_DEBUG
+static void napt_hash_stats(luat_netdrv_napt_ctx_t *ctx, const char* tag) {
+    if (ctx == NULL) return;
+    
+    size_t used_wan2lan = 0, used_lan2wan = 0, collisions_wan2lan = 0, collisions_lan2wan = 0;
+    
+    if (ctx->hash_table_wan2lan) {
+        for (size_t i = 0; i < NAPT_HASH_TABLE_SIZE; i++) {
+            if (ctx->hash_table_wan2lan[i].item_index != NAPT_HASH_INVALID_INDEX) {
+                used_wan2lan++;
+            }
+        }
+    }
+    
+    if (ctx->hash_table_lan2wan) {
+        for (size_t i = 0; i < NAPT_HASH_TABLE_SIZE; i++) {
+            if (ctx->hash_table_lan2wan[i].item_index != NAPT_HASH_INVALID_INDEX) {
+                used_lan2wan++;
+            }
+        }
+    }
+    
+    double load_wan2lan = (double)used_wan2lan / NAPT_HASH_TABLE_SIZE * 100;
+    double load_lan2wan = (double)used_lan2wan / NAPT_HASH_TABLE_SIZE * 100;
+    
+    LLOGD("[%s] WAN2LAN: %d/%d (%.1f%%), LAN2WAN: %d/%d (%.1f%%)", 
+          tag, used_wan2lan, NAPT_HASH_TABLE_SIZE, load_wan2lan,
+          used_lan2wan, NAPT_HASH_TABLE_SIZE, load_lan2wan);
+}
+#endif
+
+// ============ 哈希表优化函数结束 ============
 
 #if !defined(LUAT_USE_PSRAM) && !defined(LUAT_USE_NETDRV_NAPT)
 __NETDRV_CODE_IN_RAM__ int luat_netdrv_napt_pkg_input(int id, uint8_t* buff, size_t len) {
@@ -192,7 +415,8 @@ err_t netdrv_ip_input_cb(int id, struct pbuf *p, struct netif *inp) {
     pbuf_copy_partial(p, napt_buff, len, 0);
     int ret = luat_netdrv_napt_pkg_input(id, napt_buff, len);
     // LLOGD("napt_pkg_input ret %d", ret);
-    return ret == 0 ? 1 : 0;
+    // 返回: NAPT_RET_OK(0)表示已转发,其他值表示LWIP继续处理
+    return ret == NAPT_RET_OK ? NAPT_RET_OK : NAPT_RET_SKIP;
     // return 1;
 }
 
@@ -225,11 +449,32 @@ static int ctx_init(luat_netdrv_napt_ctx_t** ctx_ptrptr) {
     }
     memset(ctx, 0, sizeof(luat_netdrv_napt_ctx_t));
     luat_rtos_mutex_create(&ctx->lock);
-    size_t port_len = (NAPT_PORT_RANGE_END - NAPT_PORT_RANGE_START) / 4;
-    ctx->port_used = luat_heap_malloc(port_len + 8);
-    memset(ctx->port_used, 0, port_len + 8);
+    // 正确计算端口位图大小: (端口数 + 31) / 32 * 4 字节
+    size_t port_len = NAPT_PORT_BITMAP_SIZE;
+    ctx->port_used = luat_heap_malloc(port_len);
+    if (ctx->port_used == NULL) {
+        LLOGE("初始化napt port_used失败");
+        luat_heap_free(ctx);
+        return -1;
+    }
+    memset(ctx->port_used, 0, port_len);
     ctx->clean_tm = 1;
     ctx->item_max = NAPT_TCP_MAP_ITEM_MAX;
+    
+    // 初始化哈希表
+    ctx->hash_table_wan2lan = NULL;
+    ctx->hash_table_lan2wan = NULL;
+    if (napt_hash_init(ctx) != 0) {
+        LLOGE("哈希表初始化失败");
+        luat_heap_free(ctx->port_used);
+        luat_heap_free(ctx);
+        return -1;
+    }
+
+#ifdef NAPT_DEBUG
+    LLOGD("NAPT初始化: 哈希表大小%d, 最大项%d, 最大端口数%d", 
+          NAPT_HASH_TABLE_SIZE, NAPT_TCP_MAP_ITEM_MAX, NAPT_PORT_COUNT);
+#endif
 
     *ctx_ptrptr = ctx;
     return 0;
@@ -248,12 +493,64 @@ void luat_netdrv_napt_enable(int adapter_id) {
     }
     s_gw_adapter_id = adapter_id;
 }
+
+void luat_netdrv_napt_disable(void) {
+    s_gw_adapter_id = -1;
+    
+    // 清理TCP上下文
+    if (g_napt_tcp_ctx) {
+        if (g_napt_tcp_ctx->port_used) {
+            luat_heap_free(g_napt_tcp_ctx->port_used);
+            g_napt_tcp_ctx->port_used = NULL;
+        }
+        if (g_napt_tcp_ctx->hash_table_wan2lan) {
+            luat_heap_free(g_napt_tcp_ctx->hash_table_wan2lan);
+            g_napt_tcp_ctx->hash_table_wan2lan = NULL;
+        }
+        if (g_napt_tcp_ctx->hash_table_lan2wan) {
+            luat_heap_free(g_napt_tcp_ctx->hash_table_lan2wan);
+            g_napt_tcp_ctx->hash_table_lan2wan = NULL;
+        }
+        luat_rtos_mutex_delete(g_napt_tcp_ctx->lock);
+        luat_heap_free(g_napt_tcp_ctx);
+        g_napt_tcp_ctx = NULL;
+    }
+    
+    // 清理UDP上下文
+    if (g_napt_udp_ctx) {
+        if (g_napt_udp_ctx->port_used) {
+            luat_heap_free(g_napt_udp_ctx->port_used);
+            g_napt_udp_ctx->port_used = NULL;
+        }
+        if (g_napt_udp_ctx->hash_table_wan2lan) {
+            luat_heap_free(g_napt_udp_ctx->hash_table_wan2lan);
+            g_napt_udp_ctx->hash_table_wan2lan = NULL;
+        }
+        if (g_napt_udp_ctx->hash_table_lan2wan) {
+            luat_heap_free(g_napt_udp_ctx->hash_table_lan2wan);
+            g_napt_udp_ctx->hash_table_lan2wan = NULL;
+        }
+        luat_rtos_mutex_delete(g_napt_udp_ctx->lock);
+        luat_heap_free(g_napt_udp_ctx);
+        g_napt_udp_ctx = NULL;
+    }
+    
+    // 清理缓冲区
+    if (napt_buff) {
+        luat_heap_free(napt_buff);
+        napt_buff = NULL;
+    }
+    
+    // 调用TCP/UDP的cleanup函数
+    luat_netdrv_napt_tcp_cleanup();
+    luat_netdrv_napt_udp_cleanup();
+}
 __NETDRV_CODE_IN_RAM__ static size_t luat_napt_tcp_port_alloc(luat_netdrv_napt_ctx_t *napt_ctx) {
     size_t offset;
     size_t soffset;
-    for (size_t i = 0; i <= NAPT_PORT_RANGE_END - NAPT_PORT_RANGE_START; i++) {
-        offset = i / ( 4 * 8);
-        soffset = i % ( 4 * 8);
+    for (size_t i = 0; i < NAPT_PORT_COUNT; i++) {
+        offset = i / 32;  // 每个uint32_t占32位
+        soffset = i % 32;
         if ((napt_ctx->port_used[offset] & (1 << soffset)) == 0) {
             napt_ctx->port_used[offset] |= (1 << soffset);
             return i + NAPT_PORT_RANGE_START;
@@ -293,18 +590,23 @@ __NETDRV_CODE_IN_RAM__ static void mapping_cleanup(luat_netdrv_napt_ctx_t *napt_
     for (size_t i = 0; i < napt_ctx->item_last; i++) {
         flag = 0;
         it = &napt_ctx->items[i];
+        // 跳过已标记删除的映射项
+        if (!it->is_vaild) {
+            continue;
+        }
         tdiff = tnow - it->tm_ms;
         if (napt_ctx->ip_tp == IP_PROTO_TCP) {
-            if (tdiff > 20*60*1000) { // TCP是20分钟
+            if (tdiff > NAPT_TCP_TIMEOUT_MS) { // TCP是20分钟
                 flag = 1;
             }
-            else if ((((it->finack1 && it->finack2) || !it->synack) && tdiff > IP_NAPT_TIMEOUT_MS_TCP_DISCON)) {
-                // print_item("TCP链接已关闭,移除", it);
+            // 仅当连接明确关闭(FIN-ACK双向)或重置(RST)时才删除,避免误删活动连接
+            else if (((it->finack1 && it->finack2) || it->rst) && tdiff > NAPT_TCP_DISCON_TIMEOUT_MS) {
+                // print_item("TCP连接已关闭,移除", it);
                 flag = 1;
             }
         }
         else if (napt_ctx->ip_tp == IP_PROTO_UDP) {
-            if (tdiff > 2*60*1000) { // UDP 是2分钟
+            if (tdiff > NAPT_UDP_TIMEOUT_MS) { // UDP 是2分钟
                 flag = 1;
             }
         }
@@ -313,10 +615,12 @@ __NETDRV_CODE_IN_RAM__ static void mapping_cleanup(luat_netdrv_napt_ctx_t *napt_
             it->is_vaild = 0;
             it->tm_ms = 0;
             port = it->wnet_local_port - NAPT_PORT_RANGE_START;
-            offset = port / ( 4 * 8);
-            soffset = port % ( 4 * 8);
-            if (offset > 1024) {
-                LLOGE("非法的offset %d", offset);
+            offset = port / 32;  // 每个uint32_t占32位
+            soffset = port % 32;
+            // 检查offset是否在有效范围内
+            size_t max_offset = (NAPT_PORT_COUNT + 31) / 32;
+            if (offset >= max_offset) {
+                LLOGE("非法的offset %d, 最大值应为 %d", offset, max_offset - 1);
             }
             else {
                 napt_ctx->port_used[offset] &= (~(1 << soffset));
@@ -333,7 +637,19 @@ __NETDRV_CODE_IN_RAM__ static void mapping_cleanup(luat_netdrv_napt_ctx_t *napt_
     }
     // 全部标记完成了, 记录最后的位置
     // LLOGD("清理前后对比 %ld -> %ld", ctx->item_last, cur_index);
+    size_t removed_count = napt_ctx->item_last - cur_index;
     napt_ctx->item_last = cur_index;
+    
+    // 重建哈希表
+    napt_hash_rebuild(napt_ctx);
+    
+    // 调试日志:输出清理和哈希表统计信息
+#ifdef NAPT_DEBUG
+    if (removed_count > 0) {
+        LLOGD("NAPT清理: 移除%d项,剩余%d项", removed_count, cur_index);
+        napt_hash_stats(napt_ctx, "清理后");
+    }
+#endif
 }
 
 
@@ -387,28 +703,27 @@ __NETDRV_CODE_IN_RAM__ int luat_netdrv_napt_tcp_wan2lan(napt_ctx_t* ctx, luat_ne
         tmp.wnet_local_port = udp_hdr->dest;
     }
 
-    luat_rtos_mutex_lock(napt_ctx->lock, 5000);
+    luat_rtos_mutex_lock(napt_ctx->lock, NAPT_MUTEX_TIMEOUT_MS);
     // 清理映射关系
-    if (tsec - napt_ctx->clean_tm > 5) {
+    if (tsec - napt_ctx->clean_tm > NAPT_CLEANUP_INTERVAL_SEC) {
         // LLOGD("执行映射关系清理 %ld %ld", tsec, napt_ctx->clean_tm);
         mapping_cleanup(napt_ctx);
         napt_ctx->clean_tm = tsec;
         // LLOGD("完成映射关系清理 %ld %ld", tsec, napt_ctx->clean_tm);
     }
-    // size_t c_all = 0;
-    for (size_t i = 0; i < napt_ctx->item_last; i++) {
-        it = &napt_ctx->items[i];
-        // 远程ip(4 byte), 远程端口(2 byte), 本地映射端口(2 byte)
-        if (memcmp(&tmp.wnet_ip, &it->wnet_ip, 8) == 0) {
-            it->tm_ms = tnow;
-            memcpy(mapping, it, sizeof(luat_netdrv_napt_tcpudp_t));
-            ret = 0;
-            if (napt_ctx->ip_tp == IP_PROTO_TCP) {
-                update_tcp_stat_wnet(tcp_hdr, it);
-            }
-            break;
+    
+    // 使用哈希表快速查找
+    int idx = napt_hash_find_wan2lan(napt_ctx, tmp.wnet_ip, tmp.wnet_port, tmp.wnet_local_port);
+    if (idx >= 0 && idx < (int)napt_ctx->item_last) {
+        it = &napt_ctx->items[idx];
+        it->tm_ms = tnow;
+        ret = NAPT_RET_OK;
+        *mapping = *it;  // 优化: 直接赋值代替memcpy
+        if (napt_ctx->ip_tp == IP_PROTO_TCP) {
+            update_tcp_stat_wnet(tcp_hdr, it);
         }
     }
+    
     luat_rtos_mutex_unlock(napt_ctx->lock);
     return ret;
 }
@@ -425,7 +740,6 @@ __NETDRV_CODE_IN_RAM__ int luat_netdrv_napt_tcp_lan2wan(napt_ctx_t* ctx, luat_ne
     // size_t tmpaddr = 0;
     struct tcp_hdr *tcp_hdr = (struct tcp_hdr*)(((uint8_t*)ctx->iphdr) + iphdr_len);
     struct udp_hdr *udp_hdr = (struct udp_hdr*)(((uint8_t*)ctx->iphdr) + iphdr_len);
-    size_t c_all = 0;
 
     tmp.inet_ip = ctx->iphdr->src.addr;
     tmp.wnet_ip = ctx->iphdr->dest.addr;
@@ -438,31 +752,29 @@ __NETDRV_CODE_IN_RAM__ int luat_netdrv_napt_tcp_lan2wan(napt_ctx_t* ctx, luat_ne
         tmp.wnet_port = udp_hdr->dest;
     }
     
-    ret = luat_rtos_mutex_lock(napt_ctx->lock, 1000);
+    ret = luat_rtos_mutex_lock(napt_ctx->lock, NAPT_MUTEX_TIMEOUT_MS);
     if (ret) {
         LLOGE("napt加锁失败!!! ret %d", ret);
-        return -4;
+        return NAPT_RET_LOCK_FAIL;
     }
     ret = -1;
 
     // 清理映射关系
-    if (tsec - napt_ctx->clean_tm > 5) {
+    if (tsec - napt_ctx->clean_tm > NAPT_CLEANUP_INTERVAL_SEC) {
         mapping_cleanup(napt_ctx);
         napt_ctx->clean_tm = tsec;
     }
-    for (size_t i = 0; i < napt_ctx->item_last; i++) {
-        it = &napt_ctx->items[i];
-        c_all ++;
-        // 本地ip(4 byte), 本地端口(2 byte), 远程ip(4 byte), 远程端口(2 byte)
-        if (memcmp(&tmp.inet_ip, &it->inet_ip, 6 + 6) == 0) {
-            it->tm_ms = tnow;
-            ret = 0;
-            memcpy(mapping, it, sizeof(luat_netdrv_napt_tcpudp_t));
-            // 映射关系找到了,那就关联一下情况
-            if (napt_ctx->ip_tp == IP_PROTO_TCP) {
-                update_tcp_stat_inet(tcp_hdr, it);
-            }
-            break;
+    
+    // 使用哈希表快速查找
+    int idx = napt_hash_find_lan2wan(napt_ctx, tmp.inet_ip, tmp.inet_port, tmp.wnet_ip, tmp.wnet_port);
+    if (idx >= 0 && idx < (int)napt_ctx->item_last) {
+        it = &napt_ctx->items[idx];
+        it->tm_ms = tnow;
+        ret = NAPT_RET_OK;
+        *mapping = *it;  // 优化: 直接赋值代替memcpy
+        // 映射关系找到了,那就关联一下情况
+        if (napt_ctx->ip_tp == IP_PROTO_TCP) {
+            update_tcp_stat_inet(tcp_hdr, it);
         }
     }
     while (ret != 0) {
@@ -482,8 +794,24 @@ __NETDRV_CODE_IN_RAM__ int luat_netdrv_napt_tcp_lan2wan(napt_ctx_t* ctx, luat_ne
         }
         tmp.adapter_id = ctx->net->id;
         tmp.tm_ms = tnow;
+        tmp.is_vaild = 1;  // 标记映射为有效
         it = &napt_ctx->items[napt_ctx->item_last];
         memcpy(it, &tmp, sizeof(luat_netdrv_napt_tcpudp_t));
+        
+        // 添加到哈希表(两个表都必须成功)
+        if (napt_hash_add_wan2lan(napt_ctx, napt_ctx->item_last, it) != 0) {
+            LLOGE("哈希表wan2lan添加失败,映射表满");
+            memset(it, 0, sizeof(luat_netdrv_napt_tcpudp_t));  // 清理未成功的项
+            ret = NAPT_RET_NO_MEMORY;
+            break;
+        }
+        if (napt_hash_add_lan2wan(napt_ctx, napt_ctx->item_last, it) != 0) {
+            LLOGE("哈希表lan2wan添加失败,映射表满");
+            memset(it, 0, sizeof(luat_netdrv_napt_tcpudp_t));  // 清理未成功的项
+            ret = NAPT_RET_NO_MEMORY;
+            break;
+        }
+        
         napt_ctx->item_last ++;
         if (ctx->eth) {
             memcpy(it->inet_mac, ctx->eth->src.addr, 6);
@@ -491,8 +819,8 @@ __NETDRV_CODE_IN_RAM__ int luat_netdrv_napt_tcp_lan2wan(napt_ctx_t* ctx, luat_ne
         else {
             memset(it->inet_mac, 0, 6);
         }
-        memcpy(mapping, it, sizeof(luat_netdrv_napt_tcpudp_t));
-        ret = 0;
+        *mapping = *it;  // 优化: 直接赋值代替memcpy
+        ret = NAPT_RET_OK;
         break;
     }
 

+ 23 - 9
components/network/netdrv/src/luat_netdrv_napt_tcp.c

@@ -17,6 +17,13 @@
 static uint8_t *tcp_buff;
 extern luat_netdrv_napt_ctx_t *g_napt_tcp_ctx;
 
+void luat_netdrv_napt_tcp_cleanup(void) {
+    if (tcp_buff) {
+        luat_heap_free(tcp_buff);
+        tcp_buff = NULL;
+    }
+}
+
 #define u32 uint32_t
 #define u16 uint16_t
 #define u8 uint8_t
@@ -30,8 +37,15 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_tcp_handle(napt_ctx_t* ctx) {
     // luat_netdrv_napt_tcpudp_t* it = NULL;
     luat_netdrv_napt_tcpudp_t* it_map = NULL;
     int ret = 0;
+    
+    // P1检查: 验证NAPT上下文初始化
+    if (g_napt_tcp_ctx == NULL) {
+        LLOGD("TCP NAPT context not initialized");
+        return NAPT_RET_SKIP;
+    }
+    
     if (gw == NULL || gw->netif == NULL) {
-        return 0;
+        return NAPT_RET_SKIP;
     }
     if (tcp_buff == NULL) {
         tcp_buff = luat_heap_opt_zalloc(LUAT_HEAP_AUTO, 1600);
@@ -42,7 +56,7 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_tcp_handle(napt_ctx_t* ctx) {
         // 这是从外网到内网的TCP包
         // LLOGD("wnet.search dst port %d", ntohs(tcp_hdr->dest));
         ret = luat_netdrv_napt_tcp_wan2lan(ctx, &mapping, g_napt_tcp_ctx);
-        if (ret == 0) {
+        if (ret == NAPT_RET_OK) {
             // 修改目标端口
             tcp_hdr->dest = mapping.inet_port;
 
@@ -69,7 +83,7 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_tcp_handle(napt_ctx_t* ctx) {
             luat_netdrv_t* dst = luat_netdrv_get(mapping.adapter_id);
             if (dst == NULL) {
                 LLOGE("能找到TCP映射关系, 但目标netdrv不存在, 这肯定是BUG啊!!");
-                return 1;
+                return NAPT_RET_OK;
             }
             if (dst->dataout) {
                 if (ctx->eth && dst->netif->flags & NETIF_FLAG_ETHARP) {
@@ -94,10 +108,10 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_tcp_handle(napt_ctx_t* ctx) {
             else {
                 LLOGE("能找到TCP映射关系, 但目标netdrv不支持dataout!!");
             }
-            return 1; // 全部修改完成
+            return NAPT_RET_OK; // 全部修改完成
         }
         // LLOGD("没有找到TCP映射关系, 放行给LWIP处理");
-        return 0;
+        return NAPT_RET_SKIP;
     }
     else {
         // 内网, 尝试对外网的请求吗?
@@ -107,8 +121,8 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_tcp_handle(napt_ctx_t* ctx) {
         // 第一轮循环, 是否有已知映射
         // LLOGD("inet.search src port %d -> %d", ntohs(tcp_hdr->src), ntohs(tcp_hdr->dest));
         ret = luat_netdrv_napt_tcp_lan2wan(ctx, &mapping, g_napt_tcp_ctx);
-        if (ret != 0) {
-            return 0;
+        if (ret != NAPT_RET_OK) {
+            return NAPT_RET_SKIP;
         }
         it_map = &mapping;
 
@@ -138,7 +152,7 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_tcp_handle(napt_ctx_t* ctx) {
                 }
                 else {
                     LLOGD("网关netdrv是ETH,源网卡不是ETH, 当前不支持");
-                    return 0;
+                    return NAPT_RET_SKIP;
                 }
             }
             else {
@@ -153,7 +167,7 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_tcp_handle(napt_ctx_t* ctx) {
         else {
             LLOGD("TCP改写完成, 但GW不支持dataout回调?!!");
         }
-        return 1;
+        return NAPT_RET_OK;
     }
     return 0;
 }

+ 24 - 10
components/network/netdrv/src/luat_netdrv_napt_udp.c

@@ -20,6 +20,13 @@ extern luat_netdrv_napt_ctx_t *g_napt_udp_ctx;
 #define NAPT_ETH_HDR_LEN sizeof(struct ethhdr)
 
 static uint8_t *udp_buff;
+
+void luat_netdrv_napt_udp_cleanup(void) {
+    if (udp_buff) {
+        luat_heap_free(udp_buff);
+        udp_buff = NULL;
+    }
+}
 __NETDRV_CODE_IN_RAM__ int luat_napt_udp_handle(napt_ctx_t *ctx)
 {
     uint16_t iphdr_len = (ctx->iphdr->_v_hl & 0x0F) * 4;
@@ -27,6 +34,13 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_udp_handle(napt_ctx_t *ctx)
     struct udp_hdr *udp_hdr = (struct udp_hdr *)(((uint8_t *)ctx->iphdr) + iphdr_len);
     luat_netdrv_t *gw = ctx->drv_gw;
     int ret = 0;
+    
+    // P1检查: 验证NAPT上下文初始化
+    if (g_napt_udp_ctx == NULL) {
+        LLOGD("UDP NAPT context not initialized");
+        return NAPT_RET_SKIP;
+    }
+    
     if (udp_buff == NULL)
     {
         udp_buff = luat_heap_opt_zalloc(LUAT_HEAP_AUTO, 1600);
@@ -37,7 +51,7 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_udp_handle(napt_ctx_t *ctx)
     {
         // 这是从外网到内网的UDP包
         ret = luat_netdrv_napt_tcp_wan2lan(ctx, &mapping, g_napt_udp_ctx);
-        if (ret == 0)
+        if (ret == NAPT_RET_OK)
         {
             // 找到映射关系了!!! 修改目标ID
             udp_hdr->dest = mapping.inet_port;
@@ -68,7 +82,7 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_udp_handle(napt_ctx_t *ctx)
             if (dst == NULL)
             {
                 LLOGE("能找到UDP映射关系, 但目标netdrv不存在, 这肯定是BUG啊!!");
-                return 1;
+                return NAPT_RET_OK;
             }
             if (dst->dataout)
             {
@@ -98,21 +112,21 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_udp_handle(napt_ctx_t *ctx)
             {
                 LLOGE("能找到UDP映射关系, 但目标netdrv不支持dataout!!");
             }
-            return 1; // 全部修改完成
+            return NAPT_RET_OK; // 全部修改完成
         }
         // LLOGD("没有找到UDP映射关系, 放行给LWIP处理");
-        return 0;
+        return NAPT_RET_SKIP;
     }
     else
     {
         // 内网, 尝试对外网的请求吗?
         if (ip_hdr->dest.addr == ip_addr_get_ip4_u32(&ctx->net->netif->ip_addr))
         {
-            return 0; // 对网关的UDP请求, 交给LWIP处理
+            return NAPT_RET_SKIP; // 对网关的UDP请求, 交给LWIP处理
         }
         ret = luat_netdrv_napt_tcp_lan2wan(ctx, &mapping, g_napt_udp_ctx);
-        if (ret != 0) {
-            return 0;
+        if (ret != NAPT_RET_OK) {
+            return NAPT_RET_SKIP;
         }
         // 2. 修改信息
         ip_hdr->src.addr = ip_addr_get_ip4_u32(&gw->netif->ip_addr);
@@ -146,7 +160,7 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_udp_handle(napt_ctx_t *ctx)
                 else
                 {
                     LLOGD("网关netdrv是ETH,源网卡不是ETH, 当前不支持");
-                    return 0;
+                    return NAPT_RET_SKIP;
                 }
             }
             else
@@ -165,7 +179,7 @@ __NETDRV_CODE_IN_RAM__ int luat_napt_udp_handle(napt_ctx_t *ctx)
         {
             LLOGD("UDP改写完成, 但GW不支持dataout回调?!!");
         }
-        return 1;
+        return NAPT_RET_OK;
     }
-    return 0;
+    return NAPT_RET_SKIP;
 }

+ 88 - 2
components/rtmp/include/luat_rtmp_push.h

@@ -138,13 +138,23 @@ typedef struct {
  * 用于查询RTMP连接的实时统计数据
  */
 typedef struct {
-    uint32_t bytes_sent;            /**< 已发送的字节数 */
+    uint64_t bytes_sent;            /**< 已发送的字节数 */
     uint32_t video_frames_sent;     /**< 已发送的视频帧数 */
     uint32_t audio_frames_sent;     /**< 已发送的音频帧数 */
     uint32_t connection_time;       /**< 连接持续时间(毫秒) */
     uint32_t packets_sent;          /**< 已发送的包数 */
     uint32_t last_video_timestamp;  /**< 最后视频时间戳(毫秒) */
     uint32_t last_audio_timestamp;  /**< 最后音频时间戳(毫秒) */
+
+    /* 细分统计 */
+    uint32_t i_frames;              /**< 发送的I帧数量 */
+    uint32_t p_frames;              /**< 发送的P帧数量 */
+    uint64_t i_bytes;               /**< 发送的I帧字节数(NAL数据长度累加) */
+    uint64_t p_bytes;               /**< 发送的P帧字节数(NAL数据长度累加) */
+    uint64_t audio_bytes;           /**< 发送的音频字节数 */
+
+    uint32_t dropped_frames;        /**< 被丢弃的帧数量 */
+    uint64_t dropped_bytes;         /**< 被丢弃的帧字节数 */
 } rtmp_stats_t;
 
 /**
@@ -194,8 +204,24 @@ typedef struct {
     
     /** ============ 统计信息 ============ */
     uint32_t packets_sent;          /**< 已发送的包数 */
-    uint32_t bytes_sent;            /**< 已发送的字节数 */
+    uint64_t bytes_sent;            /**< 已发送的字节数 */
     uint32_t command_id;            /**< 当前命令ID */
+
+    /* 帧统计 */
+    uint32_t i_frames;              /**< 发送的I帧数量 */
+    uint32_t p_frames;              /**< 发送的P帧数量 */
+    uint64_t i_bytes;               /**< 发送的I帧字节数 */
+    uint64_t p_bytes;               /**< 发送的P帧字节数 */
+    uint32_t audio_frames_sent;     /**< 发送的音频帧数量 */
+    uint64_t audio_bytes;           /**< 发送的音频字节数 */
+    uint32_t dropped_frames;        /**< 被丢弃的帧数量 */
+    uint64_t dropped_bytes;         /**< 被丢弃的帧字节数 */
+    uint32_t last_stats_log_ms;     /**< 上次统计日志输出时间 */
+    uint64_t last_stats_bytes;      /**< 上次统计日志输出时的总字节数 */
+    uint32_t stats_interval_ms;     /**< 统计输出间隔(毫秒),默认10000 */
+    uint32_t stats_window_ms;       /**< 统计窗口长度(毫秒),默认与间隔相同 */
+    uint32_t last_window_ms;        /**< 上次窗口采样时间戳(ms) */
+    uint64_t last_window_bytes;     /**< 上次窗口采样时的总字节数 */
     
     /** ============ 用户数据 ============ */
     void *user_data;                /**< 用户自定义数据指针 */
@@ -300,6 +326,48 @@ int rtmp_send_nalu_multi(rtmp_ctx_t *ctx, const uint8_t **nalus,
                          const uint32_t *lengths, uint32_t count, 
                          uint32_t timestamp);
 
+/**
+ * 发送音频数据帧
+ * 
+ * 将音频数据打包为RTMP音频消息并发送。
+ * 支持AAC等多种音频格式。
+ * 
+ * 使用示例(AAC):
+ * @code
+ * // 发送AAC Sequence Header
+ * uint8_t aac_header[] = {0xAF, 0x00, 0x12, 0x10}; // AAC-LC, 44.1kHz, Stereo
+ * rtmp_send_audio(ctx, aac_header, sizeof(aac_header), 0);
+ * 
+ * // 发送AAC音频帧
+ * uint8_t audio_frame[256];
+ * audio_frame[0] = 0xAF; // AAC, 44.1kHz, 16bit, Stereo
+ * audio_frame[1] = 0x01; // AAC raw data
+ * memcpy(&audio_frame[2], aac_raw_data, aac_raw_len);
+ * rtmp_send_audio(ctx, audio_frame, 2 + aac_raw_len, timestamp);
+ * @endcode
+ * 
+ * 音频标签头格式(第1字节):
+ * - Bit[7:4]: SoundFormat (10=AAC, 2=MP3, 3=PCM等)
+ * - Bit[3:2]: SoundRate (0=5.5kHz, 1=11kHz, 2=22kHz, 3=44kHz)
+ * - Bit[1]: SoundSize (0=8bit, 1=16bit)
+ * - Bit[0]: SoundType (0=Mono, 1=Stereo)
+ * 
+ * AAC格式需要第2字节指定AACPacketType:
+ * - 0 = AAC sequence header (AudioSpecificConfig)
+ * - 1 = AAC raw data
+ * 
+ * @param ctx RTMP上下文指针
+ * @param audio_data 音频数据指针,应包含完整的音频标签(标签头+数据)
+ * @param audio_len 音频数据总长度
+ * @param timestamp 音频时间戳(毫秒),从0开始递增
+ * @return RTMP_OK表示发送成功,其他值表示错误
+ *         - RTMP_ERR_INVALID_PARAM: 参数无效
+ *         - RTMP_ERR_NO_MEMORY: 内存不足
+ *         - RTMP_ERR_FAILED: 发送失败
+ */
+int rtmp_send_audio(rtmp_ctx_t *ctx, const uint8_t *audio_data,
+                    uint32_t audio_len, uint32_t timestamp);
+
 /**
  * 获取当前连接状态
  * 
@@ -361,6 +429,24 @@ void* rtmp_get_user_data(rtmp_ctx_t *ctx);
  */
 int rtmp_get_stats(rtmp_ctx_t *ctx, rtmp_stats_t *stats);
 
+/**
+ * 设置统计输出间隔
+ * 
+ * @param ctx RTMP上下文指针
+ * @param interval_ms 间隔毫秒数(例如10000表示10秒)
+ * @return RTMP_OK表示成功
+ */
+int rtmp_set_stats_interval(rtmp_ctx_t *ctx, uint32_t interval_ms);
+
+/**
+ * 设置统计窗口长度
+ * 
+ * @param ctx RTMP上下文指针
+ * @param window_ms 窗口毫秒数(例如10000表示10秒)
+ * @return RTMP_OK表示成功
+ */
+int rtmp_set_stats_window(rtmp_ctx_t *ctx, uint32_t window_ms);
+
 /* ======================== 回调函数定义 ======================== */
 
 /**

+ 179 - 6
components/rtmp/src/luat_rtmp_push.c

@@ -141,6 +141,7 @@ typedef struct rtmp_frame_node {
     uint32_t len;               /* 消息总长度 */
     uint32_t sent;              /* 已发送字节数 */
     bool is_key;                /* 是否关键帧 */
+    uint32_t enqueue_ms;        /* 入队时间戳(ms) */
     struct rtmp_frame_node *next;
 } rtmp_frame_node_t;
 
@@ -537,6 +538,14 @@ rtmp_ctx_t* rtmp_create(void) {
     
     /* 初始化状态 */
     ctx->state = RTMP_STATE_IDLE;
+
+    /* 统计初始化 */
+    ctx->last_stats_log_ms = (uint32_t)luat_mcu_tick64_ms();
+    ctx->last_stats_bytes = 0;
+    ctx->stats_interval_ms = 10000;
+    ctx->stats_window_ms = 10000;
+    ctx->last_window_ms = ctx->last_stats_log_ms;
+    ctx->last_window_bytes = 0;
     
     RTMP_LOGV("RTMP: Context created successfully");
     g_rtmp_ctx = ctx;
@@ -902,6 +911,7 @@ static int rtmp_send_avc_sequence_header(rtmp_ctx_t *ctx, const uint8_t *seq_hea
     node->len = rtmp_len;
     node->sent = 0;
     node->is_key = true; /* 配置按关键帧优先处理 */
+    node->enqueue_ms = (uint32_t)luat_mcu_tick64_ms();
     node->next = NULL;
     
     ret = rtmp_queue_frame(ctx, node);
@@ -984,6 +994,7 @@ static int rtmp_send_single_nalu(rtmp_ctx_t *ctx, const uint8_t *nalu_data,
     node->len = rtmp_len;
     node->sent = 0;
     node->is_key = is_key_frame;
+    node->enqueue_ms = (uint32_t)luat_mcu_tick64_ms();
     node->next = NULL;
 
     ret = rtmp_queue_frame(ctx, node);
@@ -995,7 +1006,14 @@ static int rtmp_send_single_nalu(rtmp_ctx_t *ctx, const uint8_t *nalu_data,
     /* 更新统计 */
     ctx->video_timestamp = timestamp;
     ctx->packets_sent++;
-    ctx->bytes_sent += nalu_len;
+    ctx->bytes_sent += (uint64_t)nalu_len;
+    if (is_key_frame) {
+        ctx->i_frames++;
+        ctx->i_bytes += nalu_len;
+    } else {
+        ctx->p_frames++;
+        ctx->p_bytes += nalu_len;
+    }
 
     return RTMP_OK;
 }
@@ -1020,6 +1038,63 @@ int rtmp_send_nalu_multi(rtmp_ctx_t *ctx, const uint8_t **nalus,
     return RTMP_OK;
 }
 
+/**
+ * 发送音频数据帧
+ */
+int rtmp_send_audio(rtmp_ctx_t *ctx, const uint8_t *audio_data,
+                    uint32_t audio_len, uint32_t timestamp) {
+    if (!ctx || !audio_data || audio_len == 0) {
+        return RTMP_ERR_INVALID_PARAM;
+    }
+    
+    if (ctx->state != RTMP_STATE_PUBLISHING) {
+        LLOGE("RTMP: Cannot send audio, not in publishing state");
+        return RTMP_ERR_FAILED;
+    }
+    
+    /* 构建RTMP音频消息 (消息类型8=音频, 流ID=1) */
+    uint8_t *rtmp_buf = NULL;
+    uint32_t rtmp_len = 0;
+    int ret = rtmp_build_rtmp_message(ctx, RTMP_MSG_AUDIO, audio_data, audio_len, 
+                                     timestamp, ctx->audio_stream_id, 
+                                     &rtmp_buf, &rtmp_len);
+    if (ret != RTMP_OK) {
+        LLOGE("RTMP: Failed to build RTMP message for audio");
+        return ret;
+    }
+    
+    /* 创建帧节点并加入队列 */
+    rtmp_frame_node_t *node = (rtmp_frame_node_t *)luat_heap_malloc(sizeof(rtmp_frame_node_t));
+    if (!node) {
+        luat_heap_free(rtmp_buf);
+        LLOGE("RTMP: Failed to allocate frame node for audio");
+        return RTMP_ERR_NO_MEMORY;
+    }
+    
+    node->data = rtmp_buf;
+    node->len = rtmp_len;
+    node->sent = 0;
+    node->is_key = false; /* 音频帧不是关键帧 */
+    node->enqueue_ms = (uint32_t)luat_mcu_tick64_ms();
+    node->next = NULL;
+    
+    ret = rtmp_queue_frame(ctx, node);
+    if (ret != RTMP_OK) {
+        rtmp_free_frame_node(node);
+        LLOGE("RTMP: Failed to queue audio frame");
+        return ret;
+    }
+    
+    /* 更新音频时间戳和统计 */
+    ctx->audio_timestamp = timestamp;
+    ctx->audio_frames_sent++;
+    ctx->audio_bytes += (uint64_t)audio_len;
+    
+    RTMP_LOGV("RTMP: Audio frame queued, len=%u, ts=%u", audio_len, timestamp);
+    
+    return RTMP_OK;
+}
+
 /**
  * 获取当前连接状态
  */
@@ -1070,6 +1145,58 @@ int rtmp_poll(rtmp_ctx_t *ctx) {
             return ret;
         }
     }
+
+    /* 周期性打印统计信息(每10秒) */
+    uint32_t now_ms = (uint32_t)luat_mcu_tick64_ms();
+    /* 独立刷新窗口采样 */
+    if (now_ms - ctx->last_window_ms >= ctx->stats_window_ms) {
+        ctx->last_window_ms = now_ms;
+        ctx->last_window_bytes = ctx->bytes_sent;
+    }
+
+    if (now_ms - ctx->last_stats_log_ms >= ctx->stats_interval_ms) {
+        uint32_t elapsed_ms = 0;
+        if (ctx->base_timestamp > 0 && now_ms > ctx->base_timestamp) {
+            elapsed_ms = now_ms - ctx->base_timestamp;
+        }
+        /* 平均码率(单位kbps): 总比特 / 秒 / 1000 */
+        uint32_t avg_kbps = 0;
+        if (elapsed_ms > 0) {
+            double kbps = ((double)ctx->bytes_sent * 8.0) / ((double)elapsed_ms / 1000.0) / 1000.0;
+            avg_kbps = (uint32_t)(kbps + 0.5);
+        }
+        /* 区间平均码率(最近10秒) */
+        uint32_t win_kbps = 0;
+        /* 统计窗口以 stats_window_ms 为准;若比实际间隔更长,则按实际间隔计算 */
+        uint32_t win_ms = (now_ms >= ctx->last_window_ms) ? (now_ms - ctx->last_window_ms) : 0;
+        uint64_t win_bytes = (ctx->bytes_sent >= ctx->last_window_bytes) ? (ctx->bytes_sent - ctx->last_window_bytes) : 0;
+        if (win_ms > 0 && win_bytes > 0) {
+            double wkbps = ((double)win_bytes * 8.0) / ((double)win_ms / 1000.0) / 1000.0;
+            win_kbps = (uint32_t)(wkbps + 0.5);
+        }
+        /* 以kB为单位显示总字节和各类字节(四舍五入) */
+        uint64_t total_kB = ctx->bytes_sent / 1024ULL;
+        uint64_t i_kB = ctx->i_bytes / 1024ULL;
+        uint64_t p_kB = ctx->p_bytes / 1024ULL;
+        uint64_t drop_kB = ctx->dropped_bytes / 1024ULL;
+
+        LLOGI("RTMP stats: total=%llu kB packets=%u I=%u (%llukB) P=%u (%llukB) dropped=%u (%llukB) queue=%u avg=%u kbps win=%u kbps",
+              (unsigned long long)total_kB,
+              ctx->packets_sent,
+              ctx->i_frames,
+              (unsigned long long)i_kB,
+              ctx->p_frames,
+              (unsigned long long)p_kB,
+              ctx->dropped_frames,
+              (unsigned long long)drop_kB,
+              ctx->frame_queue_bytes,
+              avg_kbps,
+              win_kbps);
+
+        /* 更新日志基准 */
+        ctx->last_stats_log_ms = now_ms;
+        ctx->last_stats_bytes = ctx->bytes_sent;
+    }
     
     return RTMP_OK;
 }
@@ -1109,10 +1236,18 @@ int rtmp_get_stats(rtmp_ctx_t *ctx, rtmp_stats_t *stats) {
     // 填充统计结构体
     stats->bytes_sent = ctx->bytes_sent;
     stats->packets_sent = ctx->packets_sent;
-    stats->video_frames_sent = (ctx->video_timestamp > 0) ? (ctx->video_timestamp / 33 + 1) : 0;  // 估计帧数(30fps约33ms)
-    stats->audio_frames_sent = 0;  // 当前仅支持视频
+    stats->video_frames_sent = ctx->i_frames + ctx->p_frames;
+    stats->audio_frames_sent = ctx->audio_frames_sent;
     stats->last_video_timestamp = ctx->video_timestamp;
     stats->last_audio_timestamp = ctx->audio_timestamp;
+
+    stats->i_frames = ctx->i_frames;
+    stats->p_frames = ctx->p_frames;
+    stats->i_bytes = ctx->i_bytes;
+    stats->p_bytes = ctx->p_bytes;
+    stats->audio_bytes = ctx->audio_bytes;
+    stats->dropped_frames = ctx->dropped_frames;
+    stats->dropped_bytes = ctx->dropped_bytes;
     
     // 计算连接持续时间
     if (ctx->base_timestamp > 0 && current_time >= ctx->base_timestamp) {
@@ -1136,6 +1271,35 @@ int rtmp_set_state_callback(rtmp_ctx_t *ctx, rtmp_state_callback callback) {
     return RTMP_OK;
 }
 
+/**
+ * 设置统计输出间隔
+ */
+int rtmp_set_stats_interval(rtmp_ctx_t *ctx, uint32_t interval_ms) {
+    if (!ctx) {
+        return RTMP_ERR_INVALID_PARAM;
+    }
+    if (interval_ms == 0) {
+        /* 防止除零/过于频繁,设定最小1000ms */
+        interval_ms = 1000;
+    }
+    ctx->stats_interval_ms = interval_ms;
+    return RTMP_OK;
+}
+
+/**
+ * 设置统计窗口长度
+ */
+int rtmp_set_stats_window(rtmp_ctx_t *ctx, uint32_t window_ms) {
+    if (!ctx) {
+        return RTMP_ERR_INVALID_PARAM;
+    }
+    if (window_ms == 0) {
+        window_ms = 1000;
+    }
+    ctx->stats_window_ms = window_ms;
+    return RTMP_OK;
+}
+
 /* ======================== 内部函数实现 ======================== */
 
 /**
@@ -1405,7 +1569,7 @@ static err_t rtmp_tcp_sent_callback(void *arg, struct tcp_pcb *pcb, u16_t len) {
     total_sent += len;
     //LLOGD("RTMP: TCP sent callback, len=%d, total_sent=%llu", len, total_sent);
     if (ctx) {
-        ctx->bytes_sent += len;
+        ctx->bytes_sent += (uint64_t)len;
         /* 继续发送队列中的数据 */
         rtmp_try_send_queue(ctx);
     }
@@ -1730,17 +1894,20 @@ static int rtmp_build_rtmp_message(rtmp_ctx_t *ctx, uint8_t msg_type,
 static int rtmp_queue_frame(rtmp_ctx_t *ctx, rtmp_frame_node_t *node) {
     if (!ctx || !node) return RTMP_ERR_INVALID_PARAM;
 
-    /* 拥堵且来了关键帧,丢弃所有未开始发送的帧(sent==0) */
+    /* 拥堵且来了关键帧,优先丢弃缓存超过1秒且未开始发送的帧(sent==0) */
     if (node->is_key && ctx->frame_head) {
+        uint32_t now_ms = (uint32_t)luat_mcu_tick64_ms();
         rtmp_frame_node_t *cur = ctx->frame_head;
         rtmp_frame_node_t *prev = NULL;
         while (cur) {
-            if (cur->sent == 0) {
+            if (cur->sent == 0 && (now_ms - cur->enqueue_ms) > 2000) {
                 rtmp_frame_node_t *to_free = cur;
                 cur = cur->next;
                 if (prev) prev->next = cur; else ctx->frame_head = cur;
                 if (to_free == ctx->frame_tail) ctx->frame_tail = prev;
                 ctx->frame_queue_bytes -= to_free->len;
+                ctx->dropped_frames++;
+                ctx->dropped_bytes += (uint64_t)to_free->len;
                 rtmp_free_frame_node(to_free);
                 continue;
             }
@@ -1760,6 +1927,8 @@ static int rtmp_queue_frame(rtmp_ctx_t *ctx, rtmp_frame_node_t *node) {
             if (prev) prev->next = cur; else ctx->frame_head = cur;
             if (to_free == ctx->frame_tail) ctx->frame_tail = prev;
             ctx->frame_queue_bytes -= to_free->len;
+            ctx->dropped_frames++;
+            ctx->dropped_bytes += (uint64_t)to_free->len;
             rtmp_free_frame_node(to_free);
             continue;
         }
@@ -1776,6 +1945,8 @@ static int rtmp_queue_frame(rtmp_ctx_t *ctx, rtmp_frame_node_t *node) {
             if (prev) prev->next = cur; else ctx->frame_head = cur;
             if (to_free == ctx->frame_tail) ctx->frame_tail = prev;
             ctx->frame_queue_bytes -= to_free->len;
+            ctx->dropped_frames++;
+            ctx->dropped_bytes += (uint64_t)to_free->len;
             rtmp_free_frame_node(to_free);
             continue;
         }
@@ -1786,6 +1957,8 @@ static int rtmp_queue_frame(rtmp_ctx_t *ctx, rtmp_frame_node_t *node) {
     /* 仍然超限,则放弃当前帧 */
     if (ctx->frame_queue_bytes + need_bytes > RTMP_MAX_QUEUE_BYTES) {
         LLOGE("RTMP: Drop frame, queue bytes %u exceed max %u", ctx->frame_queue_bytes + need_bytes, RTMP_MAX_QUEUE_BYTES);
+        ctx->dropped_frames++;
+        ctx->dropped_bytes += (uint64_t)node->len;
         return RTMP_ERR_BUFFER_OVERFLOW;
     }
 

+ 531 - 0
components/rtsp/binding/luat_lib_rtsp.c

@@ -0,0 +1,531 @@
+/*
+@module  rtsp
+@summary RTSP 直播推流
+@version 1.0
+@date    2025.12.11
+@tag     LUAT_USE_RTSP
+@usage
+-- RTSP推流示例
+local rtsp = rtsp.create("rtsp://example.com:554/stream")
+rtsp:setCallback(function(state, ...)
+    if state == rtsp.STATE_CONNECTED then
+        print("已连接到推流服务器")
+    elseif state == rtsp.STATE_PLAYING then
+        print("已开始推流")
+    elseif state == rtsp.STATE_ERROR then
+        print("出错:", ...)
+    end
+end)
+rtsp:connect()
+
+-- 开始处理
+rtsp:start()
+
+-- 30秒后停止
+sys.wait(30000)
+rtsp:stop()
+
+-- 断开连接
+rtsp:disconnect()
+rtsp:destroy()
+*/
+
+#include "luat_base.h"
+#include "luat_rtsp_push.h"
+#include "luat_msgbus.h"
+#include "luat_mem.h"
+#include "lauxlib.h"
+#include <stdlib.h>
+#include "lwip/timeouts.h"
+#include "lwip/tcpip.h"
+
+#define LUAT_LOG_TAG "rtsp"
+#include "luat_log.h"
+
+typedef struct {
+    rtsp_ctx_t *rtsp;
+    int callback_ref;
+} luat_rtsp_userdata_t;
+
+/**
+创建RTSP推流上下文
+@api rtsp.create(url)
+@string url RTSP服务器地址, 格式: rtsp://host:port/stream
+@return userdata RTSP上下文对象
+@usage
+local rtsp = rtsp.create("rtsp://example.com:554/stream")
+*/
+static int l_rtsp_create(lua_State *L) {
+    const char *url = luaL_checkstring(L, 1);
+    
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)lua_newuserdata(L, sizeof(luat_rtsp_userdata_t));
+    if (!ud) {
+        LLOGE("内存分配失败");
+        lua_pushnil(L);
+        return 1;
+    }
+    
+    ud->rtsp = rtsp_create();
+    if (!ud->rtsp) {
+        LLOGE("RTSP上下文创建失败");
+        lua_pushnil(L);
+        return 1;
+    }
+    ud->rtsp->user_data = (void *)ud;
+    
+    ud->callback_ref = LUA_NOREF;
+    
+    if (rtsp_set_url(ud->rtsp, url) != 0) {
+        LLOGE("RTSP URL设置失败");
+        rtsp_destroy(ud->rtsp);
+        lua_pushnil(L);
+        return 1;
+    }
+    
+    luaL_getmetatable(L, "rtsp_ctx");
+    lua_setmetatable(L, -2);
+    
+    LLOGD("RTSP上下文创建成功: %s", url);
+    return 1;
+}
+
+/**
+设置RTSP状态回调函数
+@api rtsp:setCallback(func)
+@function func 回调函数, 参数为 (state, ...) 
+@return nil 无返回值
+@usage
+rtsp:setCallback(function(state, ...)
+    if state == rtsp.STATE_IDLE then
+        print("空闲状态")
+    elseif state == rtsp.STATE_CONNECTING then
+        print("正在连接")
+    elseif state == rtsp.STATE_OPTIONS then
+        print("发送OPTIONS")
+    elseif state == rtsp.STATE_DESCRIBE then
+        print("发送DESCRIBE")
+    elseif state == rtsp.STATE_SETUP then
+        print("发送SETUP")
+    elseif state == rtsp.STATE_PLAY then
+        print("发送PLAY请求")
+    elseif state == rtsp.STATE_PLAYING then
+        print("正在推流")
+    elseif state == rtsp.STATE_DISCONNECTING then
+        print("正在断开")
+    elseif state == rtsp.STATE_ERROR then
+        print("错误:", ...)
+    end
+end)
+*/
+static int l_rtsp_set_callback(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        lua_pushboolean(L, 0);
+        return 1;
+    }
+    
+    if (lua_isfunction(L, 2)) {
+        if (ud->callback_ref != LUA_NOREF) {
+            luaL_unref(L, LUA_REGISTRYINDEX, ud->callback_ref);
+        }
+        lua_pushvalue(L, 2);
+        ud->callback_ref = luaL_ref(L, LUA_REGISTRYINDEX);
+        LLOGD("RTSP回调函数已设置");
+    } else if (lua_isnil(L, 2)) {
+        if (ud->callback_ref != LUA_NOREF) {
+            luaL_unref(L, LUA_REGISTRYINDEX, ud->callback_ref);
+            ud->callback_ref = LUA_NOREF;
+        }
+        LLOGD("RTSP回调函数已清除");
+    } else {
+        LLOGE("参数错误,需要function或nil");
+        lua_pushboolean(L, 0);
+        return 1;
+    }
+    
+    lua_pushboolean(L, 1);
+    return 1;
+}
+
+static int l_rtsp_handler(lua_State *L, void *udata) {
+    rtos_msg_t* msg = (rtos_msg_t*)lua_topointer(L, -1);
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)msg->ptr;
+    if (!ud || ud->callback_ref == LUA_NOREF) {
+        return 0;
+    }
+    int state = msg->arg1;
+    lua_rawgeti(L, LUA_REGISTRYINDEX, ud->callback_ref);
+    if (lua_isfunction(L, -1)) {
+        lua_pushinteger(L, state);
+        lua_call(L, 1, 0);
+    }
+    return 0;
+}
+
+/**
+状态回调函数(内部使用)
+*/
+static void l_state_callback(rtsp_ctx_t *ctx, rtsp_state_t oldstate, rtsp_state_t newstate, int error_code) {
+    rtos_msg_t msg = {0};
+    msg.handler = l_rtsp_handler;
+    msg.ptr = ctx->user_data;
+    msg.arg1 = (int)newstate;
+    msg.arg2 = (int)oldstate;
+    LLOGD("RTSP状态(%d)回调消息入队 %p %p", (int)newstate, &msg, ctx->user_data);
+    // luat_msgbus_put(&msg, 0);
+}
+
+/**
+连接到RTSP服务器
+@api rtsp:connect()
+@return boolean 成功返回true, 失败返回false
+@usage
+local ok = rtsp:connect()
+if ok then
+    print("连接请求已发送")
+else
+    print("连接失败")
+end
+*/
+static int l_rtsp_connect(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        lua_pushboolean(L, 0);
+        return 1;
+    }
+    
+    rtsp_set_state_callback(ud->rtsp, l_state_callback);
+    
+    int ret = tcpip_callback_with_block(rtsp_connect, (void *)ud->rtsp, 0);
+    LLOGD("RTSP连接请求: %s", ret == 0 ? "成功" : "失败");
+    lua_pushboolean(L, ret == 0 ? 1 : 0);
+    return 1;
+}
+
+/**
+断开RTSP连接
+@api rtsp:disconnect()
+@return boolean 成功返回true, 失败返回false
+@usage
+rtsp:disconnect()
+*/
+static int l_rtsp_disconnect(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        lua_pushboolean(L, 0);
+        return 1;
+    }
+    
+    int ret = rtsp_disconnect(ud->rtsp);
+    LLOGD("RTSP断开连接: %s", ret == 0 ? "成功" : "失败");
+    lua_pushboolean(L, ret == 0 ? 1 : 0);
+    return 1;
+}
+
+static void t_rtsp_poll(void *arg) {
+    rtsp_ctx_t *ctx = (rtsp_ctx_t *)arg;
+    rtsp_poll(ctx);
+    sys_timeout(20, t_rtsp_poll, ctx);
+}
+
+/**
+处理RTSP事件
+@api rtsp:start()
+@return nil 无返回值
+@usage
+rtsp:start()
+*/
+static int l_rtsp_start(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        return 0;
+    }
+    sys_timeout(20, t_rtsp_poll, ud->rtsp);
+    return 0;
+}
+
+/**
+停止处理RTSP事件
+@api rtsp:stop()
+@return nil 无返回值
+@usage
+rtsp:stop()
+*/
+static int l_rtsp_stop(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        return 0;
+    }
+    // 移除定时器回调
+    sys_untimeout(t_rtsp_poll, ud->rtsp);
+    return 0;
+}
+
+/**
+获取RTSP连接状态
+@api rtsp:getState()
+@return int 当前状态值
+@usage
+local state = rtsp:getState()
+if state == rtsp.STATE_CONNECTED then
+    print("已连接")
+elseif state == rtsp.STATE_PLAYING then
+    print("正在推流")
+end
+*/
+static int l_rtsp_get_state(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        lua_pushinteger(L, -1);
+        return 1;
+    }
+    
+    rtsp_state_t state = rtsp_get_state(ud->rtsp);
+    lua_pushinteger(L, (lua_Integer)state);
+    return 1;
+}
+
+/**
+设置H.264 SPS参数
+@api rtsp:setSPS(sps_data)
+@string sps_data 或 
+@userdata sps_data H.264序列参数集数据
+@return boolean 成功返回true, 失败返回false
+@usage
+local sps = string.fromBinary("\x67\x42...") -- H.264 SPS数据
+rtsp:setSPS(sps)
+*/
+static int l_rtsp_set_sps(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        lua_pushboolean(L, 0);
+        return 1;
+    }
+    
+    size_t len = 0;
+    const uint8_t *data = (const uint8_t *)luaL_checklstring(L, 2, &len);
+    
+    if (rtsp_set_sps(ud->rtsp, data, (uint32_t)len) == RTSP_OK) {
+        LLOGD("SPS已设置: %u字节", (uint32_t)len);
+        lua_pushboolean(L, 1);
+    } else {
+        lua_pushboolean(L, 0);
+    }
+    return 1;
+}
+
+/**
+设置H.264 PPS参数
+@api rtsp:setPPS(pps_data)
+@string pps_data 或 
+@userdata pps_data H.264图像参数集数据
+@return boolean 成功返回true, 失败返回false
+@usage
+local pps = string.fromBinary("\x68\xCB...") -- H.264 PPS数据
+rtsp:setPPS(pps)
+*/
+static int l_rtsp_set_pps(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        lua_pushboolean(L, 0);
+        return 1;
+    }
+    
+    size_t len = 0;
+    const uint8_t *data = (const uint8_t *)luaL_checklstring(L, 2, &len);
+    
+    if (rtsp_set_pps(ud->rtsp, data, (uint32_t)len) == RTSP_OK) {
+        LLOGD("PPS已设置: %u字节", (uint32_t)len);
+        lua_pushboolean(L, 1);
+    } else {
+        lua_pushboolean(L, 0);
+    }
+    return 1;
+}
+
+/**
+推送H.264视频帧
+@api rtsp:pushFrame(frame_data, timestamp)
+@string frame_data 或 
+@userdata frame_data H.264编码的视频帧数据
+@int timestamp 时间戳(毫秒), 可选,为0则使用内部时间戳
+@return int 成功时返回已发送或已入队的字节数, 失败返回负数
+@usage
+-- 持续推送H.264帧
+local frame_data = ... -- 获取H.264帧数据
+local timestamp = sys.now() % 0x100000000
+local ret = rtsp:pushFrame(frame_data, timestamp)
+if ret >= 0 then
+    print("已发送", ret, "字节")
+else
+    print("发送失败:", ret)
+end
+*/
+static int l_rtsp_push_frame(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        lua_pushinteger(L, -1);
+        return 1;
+    }
+    
+    size_t len = 0;
+    const uint8_t *data = (const uint8_t *)luaL_checklstring(L, 2, &len);
+    uint32_t timestamp = 0;
+    
+    if (!lua_isnil(L, 3)) {
+        timestamp = (uint32_t)luaL_checkinteger(L, 3);
+    }
+    
+    if (len == 0 || len > (1024 * 1024)) {
+        LLOGE("帧数据大小无效: %u", (uint32_t)len);
+        lua_pushinteger(L, RTSP_ERR_INVALID_PARAM);
+        return 1;
+    }
+    
+    int ret = rtsp_push_h264_frame(ud->rtsp, data, (uint32_t)len, timestamp);
+    lua_pushinteger(L, ret);
+    return 1;
+}
+
+/**
+获取RTSP统计信息
+@api rtsp:getStats()
+@return table 统计信息表
+@usage
+local stats = rtsp:getStats()
+print("已发送字节数:", stats.bytes_sent)
+print("已发送视频帧数:", stats.video_frames_sent)
+print("已发送RTP包数:", stats.rtp_packets_sent)
+*/
+static int l_rtsp_get_stats(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        lua_pushnil(L);
+        return 1;
+    }
+    
+    rtsp_stats_t stats = {0};
+    rtsp_get_stats(ud->rtsp, &stats);
+    
+    lua_newtable(L);
+    lua_pushinteger(L, stats.bytes_sent);
+    lua_setfield(L, -2, "bytes_sent");
+    lua_pushinteger(L, stats.video_frames_sent);
+    lua_setfield(L, -2, "video_frames_sent");
+    lua_pushinteger(L, stats.rtp_packets_sent);
+    lua_setfield(L, -2, "rtp_packets_sent");
+    lua_pushinteger(L, stats.connection_time);
+    lua_setfield(L, -2, "connection_time");
+    lua_pushinteger(L, stats.last_video_timestamp);
+    lua_setfield(L, -2, "last_video_timestamp");
+    
+    return 1;
+}
+
+/**
+销毁RTSP上下文,释放所有资源
+@api rtsp:destroy()
+@return nil 无返回值
+@usage
+rtsp:destroy()
+*/
+static int l_rtsp_destroy(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (!ud || !ud->rtsp) {
+        return 0;
+    }
+    
+    if (ud->callback_ref != LUA_NOREF) {
+        luaL_unref(L, LUA_REGISTRYINDEX, ud->callback_ref);
+        ud->callback_ref = LUA_NOREF;
+    }
+    
+    rtsp_destroy(ud->rtsp);
+    ud->rtsp = NULL;
+    
+    LLOGD("RTSP上下文已销毁");
+    return 0;
+}
+
+static int l_rtsp_gc(lua_State *L) {
+    luat_rtsp_userdata_t *ud = (luat_rtsp_userdata_t *)luaL_checkudata(L, 1, "rtsp_ctx");
+    if (ud && ud->rtsp) {
+        if (ud->callback_ref != LUA_NOREF) {
+            luaL_unref(L, LUA_REGISTRYINDEX, ud->callback_ref);
+        }
+        rtsp_destroy(ud->rtsp);
+        ud->rtsp = NULL;
+    }
+    return 0;
+}
+
+#include "rotable2.h"
+
+static const rotable_Reg_t reg_rtsp_ctx[] = {
+    {"setCallback",   ROREG_FUNC(l_rtsp_set_callback)},
+    {"connect",       ROREG_FUNC(l_rtsp_connect)},
+    {"disconnect",    ROREG_FUNC(l_rtsp_disconnect)},
+    {"start",         ROREG_FUNC(l_rtsp_start)},
+    {"stop",          ROREG_FUNC(l_rtsp_stop)},
+    {"getState",      ROREG_FUNC(l_rtsp_get_state)},
+    {"setSPS",        ROREG_FUNC(l_rtsp_set_sps)},
+    {"setPPS",        ROREG_FUNC(l_rtsp_set_pps)},
+    {"pushFrame",     ROREG_FUNC(l_rtsp_push_frame)},
+    {"getStats",      ROREG_FUNC(l_rtsp_get_stats)},
+    {"destroy",       ROREG_FUNC(l_rtsp_destroy)},
+    {"__gc",          ROREG_FUNC(l_rtsp_gc)},
+    {NULL,            ROREG_INT(0)}
+};
+
+static const rotable_Reg_t reg_rtsp[] = {
+    {"create",            ROREG_FUNC(l_rtsp_create)},
+    
+    // RTSP状态常量
+    {"STATE_IDLE",        ROREG_INT(RTSP_STATE_IDLE)},
+    {"STATE_CONNECTING",  ROREG_INT(RTSP_STATE_CONNECTING)},
+    {"STATE_OPTIONS",     ROREG_INT(RTSP_STATE_OPTIONS)},
+    {"STATE_DESCRIBE",    ROREG_INT(RTSP_STATE_DESCRIBE)},
+    {"STATE_SETUP",       ROREG_INT(RTSP_STATE_SETUP)},
+    {"STATE_PLAY",        ROREG_INT(RTSP_STATE_PLAY)},
+    {"STATE_PLAYING",     ROREG_INT(RTSP_STATE_PLAYING)},
+    {"STATE_DISCONNECTING",ROREG_INT(RTSP_STATE_DISCONNECTING)},
+    {"STATE_ERROR",       ROREG_INT(RTSP_STATE_ERROR)},
+    
+    // 返回值常量
+    {"OK",                ROREG_INT(RTSP_OK)},
+    {"ERR_FAILED",        ROREG_INT(RTSP_ERR_FAILED)},
+    {"ERR_INVALID_PARAM", ROREG_INT(RTSP_ERR_INVALID_PARAM)},
+    {"ERR_NO_MEMORY",     ROREG_INT(RTSP_ERR_NO_MEMORY)},
+    {"ERR_CONNECT_FAILED",ROREG_INT(RTSP_ERR_CONNECT_FAILED)},
+    {"ERR_HANDSHAKE_FAILED",ROREG_INT(RTSP_ERR_HANDSHAKE_FAILED)},
+    {"ERR_NETWORK",       ROREG_INT(RTSP_ERR_NETWORK)},
+    {"ERR_TIMEOUT",       ROREG_INT(RTSP_ERR_TIMEOUT)},
+    {"ERR_BUFFER_OVERFLOW",ROREG_INT(RTSP_ERR_BUFFER_OVERFLOW)},
+    
+    {NULL,                ROREG_INT(0)}
+};
+
+static int _rtsp_struct_newindex(lua_State *L) {
+	const rotable_Reg_t* reg = reg_rtsp_ctx;
+    const char* key = luaL_checkstring(L, 2);
+	while (1) {
+		if (reg->name == NULL)
+			return 0;
+		if (!strcmp(reg->name, key)) {
+			lua_pushcfunction(L, reg->value.value.func);
+			return 1;
+		}
+		reg ++;
+	}
+}
+
+LUAMOD_API int luaopen_rtsp(lua_State *L) {
+    luat_newlib2(L, reg_rtsp);
+    
+    luaL_newmetatable(L, "rtsp_ctx");
+    lua_pushcfunction(L, _rtsp_struct_newindex);
+    lua_setfield(L, -2, "__index");
+    lua_pop(L, 1);
+    
+    return 1;
+}

+ 362 - 0
components/rtsp/include/luat_rtsp_push.h

@@ -0,0 +1,362 @@
+/**
+ * @file luat_rtsp_push.h
+ * @brief RTSP推流组件 - 基于lwip raw API实现
+ * @author LuatOS Team
+ * 
+ * 该组件实现了RTSP(Real Time Streaming Protocol)推流功能,
+ * 支持将H.264视频流推送到RTSP服务器。
+ * 
+ * 主要特性:
+ * - 基于lwip raw socket API,适应嵌入式环境
+ * - 支持自定义H.264帧来源,灵活的NALU帧注入
+ * - 完整的RTSP握手和连接管理
+ * - 支持RTP/RTCP协议实现
+ * - 支持H.264基础配置文件
+ * - C99语法,内存使用优化
+ * 
+ * 调试说明:
+ * - 在 luat_rtsp_push.c 中修改 RTSP_DEBUG_VERBOSE 宏来控制详细日志输出
+ * - 设置为 1 开启详细调试信息,设置为 0 关闭(仅保留关键日志)
+ */
+
+#ifndef __LUAT_RTSP_PUSH_H__
+#define __LUAT_RTSP_PUSH_H__
+
+#include "luat_base.h"
+#include "lwip/tcp.h"
+#include "lwip/udp.h"
+#include <stdint.h>
+#include <stddef.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* ======================== RTSP常量定义 ======================== */
+
+/** RTSP默认端口 */
+#define RTSP_DEFAULT_PORT 554
+
+/** RTSP缓冲区大小(字节) - 需要足够大以容纳RTSP信令 */
+#define RTSP_BUFFER_SIZE (64 * 1024)
+
+/** RTP缓冲区大小(字节) */
+#define RTP_BUFFER_SIZE (256 * 1024)
+
+/** RTP UDP缓冲区最大包大小 */
+#define RTP_MAX_PACKET_SIZE 1400
+
+/** 发送帧队列最大字节数上限,超出将丢弃未发送帧 */
+#define RTSP_MAX_QUEUE_BYTES (1024 * 1024)
+
+/** RTSP命令超时时间(毫秒) */
+#define RTSP_CMD_TIMEOUT 5000
+
+/* ======================== 返回值定义 ======================== */
+
+/** 操作成功 */
+#define RTSP_OK 0
+
+/** 通用错误 */
+#define RTSP_ERR_FAILED (-1)
+
+/** 参数无效 */
+#define RTSP_ERR_INVALID_PARAM (-2)
+
+/** 内存不足 */
+#define RTSP_ERR_NO_MEMORY (-3)
+
+/** 连接错误 */
+#define RTSP_ERR_CONNECT_FAILED (-4)
+
+/** 握手失败 */
+#define RTSP_ERR_HANDSHAKE_FAILED (-5)
+
+/** 网络错误 */
+#define RTSP_ERR_NETWORK (-6)
+
+/** 超时 */
+#define RTSP_ERR_TIMEOUT (-7)
+
+/** 缓冲区溢出 */
+#define RTSP_ERR_BUFFER_OVERFLOW (-8)
+
+/* ======================== 数据类型定义 ======================== */
+
+/**
+ * RTSP连接状态枚举
+ */
+typedef enum {
+    RTSP_STATE_IDLE = 0,            /**< 空闲状态 */
+    RTSP_STATE_CONNECTING = 1,      /**< 正在连接 */
+    RTSP_STATE_OPTIONS = 2,         /**< 发送OPTIONS请求 */
+    RTSP_STATE_DESCRIBE = 3,        /**< 发送DESCRIBE请求 */
+    RTSP_STATE_SETUP = 4,           /**< 发送SETUP请求 */
+    RTSP_STATE_PLAY = 5,            /**< 发送PLAY请求,准备推流 */
+    RTSP_STATE_PLAYING = 6,         /**< 正在推流 */
+    RTSP_STATE_DISCONNECTING = 7,   /**< 正在断开连接 */
+    RTSP_STATE_ERROR = 8            /**< 错误状态 */
+} rtsp_state_t;
+
+/**
+ * H.264 NALU类型枚举
+ */
+typedef enum {
+    NALU_TYPE_NON_IDR = 1,          /**< 非IDR帧 */
+    NALU_TYPE_IDR = 5,              /**< IDR帧(关键帧) */
+    NALU_TYPE_SEI = 6,              /**< SEI(补充增强信息) */
+    NALU_TYPE_SPS = 7,              /**< SPS(序列参数集) */
+    NALU_TYPE_PPS = 8,              /**< PPS(图像参数集) */
+    NALU_TYPE_AUD = 9               /**< AUD(访问单元分隔符) */
+} nalu_type_t;
+
+/**
+ * H.264视频帧信息结构体
+ */
+typedef struct {
+    uint8_t *data;                  /**< 视频数据指针 */
+    uint32_t len;                   /**< 视频数据长度 */
+    uint32_t timestamp;             /**< 时间戳(ms) */
+    uint8_t nalu_type;              /**< NALU类型 */
+    uint8_t is_keyframe;            /**< 是否为关键帧 */
+} h264_frame_t;
+
+/**
+ * RTSP推流统计信息结构体
+ */
+typedef struct {
+    uint32_t bytes_sent;            /**< 已发送的字节数 */
+    uint32_t video_frames_sent;     /**< 已发送的视频帧数 */
+    uint32_t rtp_packets_sent;      /**< 已发送的RTP包数 */
+    uint32_t connection_time;       /**< 连接持续时间(毫秒) */
+    uint32_t last_video_timestamp;  /**< 最后视频时间戳(毫秒) */
+} rtsp_stats_t;
+
+/**
+ * RTSP推流上下文结构体
+ * 管理单个RTSP连接的所有状态和缓冲区
+ */
+typedef struct {
+    /** ============ 连接信息 ============ */
+    char *url;                      /**< RTSP服务器URL */
+    char *host;                     /**< RTSP服务器主机名/IP地址 */
+    char *stream;                   /**< 推流名 */
+    char *auth;                     /**< 认证信息(用户名:密码) */
+    uint16_t port;                  /**< 连接端口 */
+    
+    /** ============ TCP连接状态(RTSP控制通道) ============ */
+    struct tcp_pcb *control_pcb;    /**< lwip TCP控制块(RTSP信令) */
+    rtsp_state_t state;             /**< 当前连接状态 */
+    uint32_t last_activity_time;    /**< 最后活动时间戳 */
+    
+    /** ============ UDP连接状态(RTP媒体通道) ============ */
+    struct udp_pcb *rtp_pcb;        /**< RTP UDP控制块 */
+    struct udp_pcb *rtcp_pcb;       /**< RTCP UDP控制块 */
+    ip_addr_t remote_ip;            /**< 远端IP地址 */
+    uint16_t remote_rtp_port;       /**< 远端RTP端口 */
+    uint16_t remote_rtcp_port;      /**< 远端RTCP端口 */
+    uint16_t local_rtp_port;        /**< 本地RTP端口 */
+    uint16_t local_rtcp_port;       /**< 本地RTCP端口 */
+    
+    /** ============ RTSP协议状态 ============ */
+    uint32_t cseq;                  /**< RTSP序列号 */
+    char *session_id;               /**< RTSP会话ID */
+    uint32_t video_stream_id;       /**< 视频流ID(SSRC) */
+    
+    /** ============ RTP状态 ============ */
+    uint32_t rtp_sequence;          /**< RTP序列号 */
+    uint32_t rtp_timestamp;         /**< RTP时间戳 */
+    uint32_t rtp_ssrc;              /**< RTP同步源标识符 */
+    
+    /** ============ 缓冲区管理 ============ */
+    uint8_t *recv_buf;              /**< 接收缓冲区 */
+    uint32_t recv_buf_size;         /**< 接收缓冲区大小 */
+    uint32_t recv_pos;              /**< 接收缓冲区写位置 */
+    
+    uint8_t *send_buf;              /**< 发送缓冲区 */
+    uint32_t send_buf_size;         /**< 发送缓冲区大小 */
+    uint32_t send_pos;              /**< 发送缓冲区写位置 */
+    
+    uint8_t *rtp_buf;               /**< RTP发送缓冲区 */
+    uint32_t rtp_buf_size;          /**< RTP缓冲区大小 */
+
+    /** ============ 帧发送队列 ============ */
+    struct rtsp_frame_node *frame_head; /**< 待发送帧队列头 */
+    struct rtsp_frame_node *frame_tail; /**< 待发送帧队列尾 */
+    uint32_t frame_queue_bytes;          /**< 队列占用的总字节数 */
+    
+    /** ============ 时间戳管理 ============ */
+    uint32_t video_timestamp;       /**< 当前视频时间戳(ms) */
+    uint32_t base_timestamp;        /**< 基准时间戳 */
+    uint32_t start_tick;            /**< 启动时刻的系统tick */
+    
+    /** ============ H.264编码信息 ============ */
+    uint8_t *sps_data;              /**< SPS(序列参数集)数据 */
+    uint32_t sps_len;               /**< SPS长度 */
+    uint8_t *pps_data;              /**< PPS(图像参数集)数据 */
+    uint32_t pps_len;               /**< PPS长度 */
+    char *sprop_parameter_sets;     /**< SDP中的sprop-parameter-sets */
+    
+    /** ============ 统计信息 ============ */
+    uint32_t packets_sent;          /**< 已发送的包数 */
+    uint32_t bytes_sent;            /**< 已发送的字节数 */
+    uint32_t video_frames_sent;     /**< 已发送的视频帧数 */
+    uint32_t rtp_packets_sent;      /**< 已发送的RTP包数 */
+    uint32_t last_rtcp_time;        /**< 上次发送RTCP SR的时间(tick) */
+    
+    /** ============ 用户数据 ============ */
+    void *user_data;                /**< 用户自定义数据指针 */
+} rtsp_ctx_t;
+
+/**
+ * RTSP状态变化回调函数类型
+ * 
+ * @param ctx RTSP上下文指针
+ * @param old_state 旧状态
+ * @param new_state 新状态
+ * @param error_code 错误代码(仅在STATE_ERROR时有效)
+ */
+typedef void (*rtsp_state_callback_t)(rtsp_ctx_t *ctx, rtsp_state_t old_state, 
+                                      rtsp_state_t new_state, int error_code);
+
+/* ======================== 核心接口函数 ======================== */
+
+/**
+ * 创建RTSP推流上下文
+ * 
+ * @return RTSP上下文指针,失败返回NULL
+ * @note 返回的指针需要使用rtsp_destroy()释放
+ */
+rtsp_ctx_t* rtsp_create(void);
+
+/**
+ * 销毁RTSP推流上下文,释放所有资源
+ * 
+ * @param ctx RTSP上下文指针
+ * @return 0=成功, 负数=失败
+ */
+int rtsp_destroy(rtsp_ctx_t *ctx);
+
+/**
+ * 设置RTSP服务器URL
+ * 
+ * @param ctx RTSP上下文指针
+ * @param url RTSP服务器地址,格式: rtsp://host:port/stream
+ * @return 0=成功, 负数=失败
+ */
+int rtsp_set_url(rtsp_ctx_t *ctx, const char *url);
+
+/**
+ * 设置状态变化回调函数
+ * 
+ * @param ctx RTSP上下文指针
+ * @param callback 回调函数指针
+ * @return 0=成功, 负数=失败
+ */
+int rtsp_set_state_callback(rtsp_ctx_t *ctx, rtsp_state_callback_t callback);
+
+/**
+ * 连接到RTSP服务器
+ * 
+ * @param ctx RTSP上下文指针
+ * @return 0=成功, 负数=失败
+ * @note 此函数应在lwip tcpip线程中调用
+ */
+int rtsp_connect(rtsp_ctx_t *ctx);
+
+/**
+ * 断开RTSP连接
+ * 
+ * @param ctx RTSP上下文指针
+ * @return 0=成功, 负数=失败
+ */
+int rtsp_disconnect(rtsp_ctx_t *ctx);
+
+/**
+ * 获取当前连接状态
+ * 
+ * @param ctx RTSP上下文指针
+ * @return 当前状态值
+ */
+rtsp_state_t rtsp_get_state(rtsp_ctx_t *ctx);
+
+/**
+ * 设置H.264 SPS数据(序列参数集)
+ * 
+ * @param ctx RTSP上下文指针
+ * @param sps_data SPS数据指针
+ * @param sps_len SPS数据长度
+ * @return 0=成功, 负数=失败
+ * @note 数据会被复制,调用者可以释放原数据
+ */
+int rtsp_set_sps(rtsp_ctx_t *ctx, const uint8_t *sps_data, uint32_t sps_len);
+
+/**
+ * 设置H.264 PPS数据(图像参数集)
+ * 
+ * @param ctx RTSP上下文指针
+ * @param pps_data PPS数据指针
+ * @param pps_len PPS数据长度
+ * @return 0=成功, 负数=失败
+ * @note 数据会被复制,调用者可以释放原数据
+ */
+int rtsp_set_pps(rtsp_ctx_t *ctx, const uint8_t *pps_data, uint32_t pps_len);
+
+/**
+ * 推送H.264视频帧数据
+ * 
+ * 该函数接收H.264编码的视频帧数据(可以是完整的访问单元或单个NALU)
+ * 内部会自动进行RTP打包并通过UDP发送到服务器。
+ * 
+ * @param ctx RTSP上下文指针
+ * @param frame_data 视频帧数据指针(包含起始码或不包含均可)
+ * @param frame_len 视频帧数据长度(字节数)
+ * @param timestamp 时间戳(毫秒),如果为0则使用内部时间戳
+ * @return 0=成功,正数=已入队,负数=失败
+ * 
+ * @note 
+ * - frame_data 可以包含H.264起始码(0x00000001或0x000001)或不包含
+ * - 支持单个NALU或多个NALU的访问单元
+ * - 如果在PLAYING状态下调用,数据会立即发送;否则进入队列
+ * - 返回值为正数时表示字节数已加入队列
+ * 
+ * @example
+ * // 推送一帧H.264视频
+ * uint8_t *frame_data = ...;  // H.264编码帧数据
+ * uint32_t frame_len = ...;   // 帧长度
+ * uint32_t timestamp = 0;     // 使用内部时间戳
+ * 
+ * int ret = rtsp_push_h264_frame(ctx, frame_data, frame_len, timestamp);
+ * if (ret >= 0) {
+ *     printf("成功推送%d字节\n", ret);
+ * } else {
+ *     printf("推送失败: %d\n", ret);
+ * }
+ */
+int rtsp_push_h264_frame(rtsp_ctx_t *ctx, const uint8_t *frame_data, 
+                         uint32_t frame_len, uint32_t timestamp);
+
+/**
+ * 处理RTSP连接事件,应定期调用此函数
+ * 
+ * @param ctx RTSP上下文指针
+ * @return 0=成功, 负数=失败
+ * @note 建议每10-20ms调用一次此函数
+ */
+int rtsp_poll(rtsp_ctx_t *ctx);
+
+/**
+ * 获取推流统计信息
+ * 
+ * @param ctx RTSP上下文指针
+ * @param stats 统计信息指针
+ * @return 0=成功, 负数=失败
+ */
+int rtsp_get_stats(rtsp_ctx_t *ctx, rtsp_stats_t *stats);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __LUAT_RTSP_PUSH_H__ */

+ 1854 - 0
components/rtsp/src/luat_rtsp_push.c

@@ -0,0 +1,1854 @@
+/**
+ * @file luat_rtsp_push.c
+ * @brief RTSP推流组件实现 - 基于lwip raw API
+ * @author LuatOS Team
+ * 
+ * 实现了RTSP协议的核心功能,包括:
+ * - TCP连接管理(RTSP控制通道)
+ * - UDP连接管理(RTP媒体通道)
+ * - RTSP握手流程(OPTIONS, DESCRIBE, SETUP, PLAY)
+ * - RTP打包和发送
+ * - H.264视频数据处理
+ * - 网络数据收发
+ */
+
+#include "luat_rtsp_push.h"
+#include "luat_debug.h"
+#include "luat_mcu.h"
+#include "luat_mem.h"
+#include "luat_rtos.h"
+#include "luat_sntp.h"
+#include "lwip/tcp.h"
+#include "lwip/udp.h"
+#include "lwip/tcpip.h"
+#include "lwip/timeouts.h"
+#include "lwip/ip_addr.h"
+#include <string.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <stdio.h>
+
+#define LUAT_LOG_TAG "rtsp_push"
+#include "luat_log.h"
+
+/* ======================== 调试开关 ======================== */
+
+/** 启用详细调试日志 (0=关闭, 1=开启) */
+#define RTSP_DEBUG_VERBOSE 0
+
+#if RTSP_DEBUG_VERBOSE
+    #define RTSP_LOGV(...) LLOGD(__VA_ARGS__)
+#else
+    #define RTSP_LOGV(...)
+#endif
+
+/* ======================== 内部常量定义 ======================== */
+
+/** 内部帧队列节点结构体 */
+struct rtsp_frame_node {
+    uint8_t *data;
+    uint32_t len;
+    uint32_t timestamp;
+    struct rtsp_frame_node *next;
+};
+
+/** RTP固定头大小 */
+#define RTP_HEADER_SIZE 12
+
+/** RTP H.264 Payload Type */
+#define RTP_PAYLOAD_TYPE_H264 96
+
+/** RTP时间戳基数(90000 Hz for video) */
+#define RTP_TIMESTAMP_BASE 90000
+
+/** H.264 NALU类型定义 */
+#define NALU_TYPE_SLICE    1
+#define NALU_TYPE_DPA      2
+#define NALU_TYPE_DPB      3
+#define NALU_TYPE_DPC      4
+#define NALU_TYPE_IDR      5
+#define NALU_TYPE_SEI      6
+#define NALU_TYPE_SPS      7
+#define NALU_TYPE_PPS      8
+#define NALU_TYPE_AUD      9
+
+/** FU-A分片标识 */
+#define FU_A_TYPE          28
+
+rtsp_ctx_t *g_rtsp_ctx;
+
+/* ======================== 内部函数声明 ======================== */
+
+/**
+ * 解析URL并提取主机名、端口、流名
+ */
+static int rtsp_parse_url(rtsp_ctx_t *ctx, const char *url);
+
+/**
+ * 详细解析SPS数据
+ */
+static void rtsp_parse_sps_detail(const uint8_t *sps_data, uint32_t sps_len);
+
+/**
+ * 详细解析PPS数据
+ */
+static void rtsp_parse_pps_detail(const uint8_t *pps_data, uint32_t pps_len);
+
+/**
+ * 详细解析SPS数据
+ */
+static void rtsp_parse_sps_detail(const uint8_t *sps_data, uint32_t sps_len);
+
+/**
+ * 详细解析PPS数据
+ */
+static void rtsp_parse_pps_detail(const uint8_t *pps_data, uint32_t pps_len);
+
+/**
+ * TCP连接回调函数
+ */
+static err_t rtsp_tcp_connect_callback(void *arg, struct tcp_pcb *pcb, err_t err);
+
+/**
+ * TCP接收回调函数
+ */
+static err_t rtsp_tcp_recv_callback(void *arg, struct tcp_pcb *pcb, 
+                                   struct pbuf *p, err_t err);
+
+/**
+ * TCP错误回调函数
+ */
+static void rtsp_tcp_error_callback(void *arg, err_t err);
+
+/**
+ * 状态转移函数
+ */
+static void rtsp_set_state(rtsp_ctx_t *ctx, rtsp_state_t new_state, int error_code);
+
+/**
+ * 发送RTSP命令
+ */
+static int rtsp_send_command(rtsp_ctx_t *ctx, const char *method, 
+                             const char *resource, const char *extra_headers,
+                             const char *data, uint32_t data_len);
+
+/**
+ * 处理RTSP响应
+ */
+static int rtsp_handle_response(rtsp_ctx_t *ctx);
+
+/**
+ * 发送RTP包
+ */
+static int rtsp_send_rtp_packet(rtsp_ctx_t *ctx, const uint8_t *data, 
+                                uint32_t len, uint32_t timestamp, uint8_t marker);
+
+/**
+ * 解析H.264帧并构建RTP包
+ */
+static int rtsp_build_rtp_payload(rtsp_ctx_t *ctx, const uint8_t *frame_data, 
+                                  uint32_t frame_len, uint32_t timestamp);
+
+/**
+ * 从队列中提取并发送帧
+ */
+static int rtsp_send_queued_frames(rtsp_ctx_t *ctx);
+
+/**
+ * 清空帧队列
+ */
+static void rtsp_clear_frame_queue(rtsp_ctx_t *ctx);
+
+/** 简单的大小写不敏感比较 */
+static int rtsp_strncasecmp(const char *a, const char *b, size_t n);
+
+/**
+ * Base64编码函数
+ */
+static int rtsp_base64_encode(const uint8_t *src, uint32_t src_len, char *dst, uint32_t dst_len);
+
+/**
+ * 查找下一个NALU起始码位置
+ */
+static const uint8_t* rtsp_find_next_nalu(const uint8_t *data, uint32_t len, uint32_t *start_code_len);
+
+/**
+ * 发送单个NALU(支持FU-A分片)
+ */
+static int rtsp_send_nalu(rtsp_ctx_t *ctx, const uint8_t *nalu_data, 
+                          uint32_t nalu_len, uint32_t timestamp, uint8_t is_last_nalu);
+
+/**
+ * 转换系统时间到NTP时间戳
+ */
+static void rtsp_get_ntp_timestamp(uint32_t *ntp_sec, uint32_t *ntp_frac);
+
+/**
+ * 发送RTCP Sender Report
+ */
+static int rtsp_send_rtcp_sr(rtsp_ctx_t *ctx);
+
+/* ======================== 核心实现 ======================== */
+
+/**
+ * 创建RTSP推流上下文
+ */
+rtsp_ctx_t* rtsp_create(void) {
+    rtsp_ctx_t *ctx = (rtsp_ctx_t *)luat_heap_malloc(sizeof(rtsp_ctx_t));
+    if (!ctx) {
+        LLOGE("内存分配失败");
+        return NULL;
+    }
+    
+    memset(ctx, 0, sizeof(rtsp_ctx_t));
+    
+    // 初始化缓冲区
+    ctx->recv_buf = (uint8_t *)luat_heap_malloc(RTSP_BUFFER_SIZE);
+    if (!ctx->recv_buf) {
+        LLOGE("接收缓冲区分配失败");
+        luat_heap_free(ctx);
+        return NULL;
+    }
+    ctx->recv_buf_size = RTSP_BUFFER_SIZE;
+    ctx->recv_pos = 0;
+    
+    ctx->send_buf = (uint8_t *)luat_heap_malloc(RTSP_BUFFER_SIZE);
+    if (!ctx->send_buf) {
+        LLOGE("发送缓冲区分配失败");
+        luat_heap_free(ctx->recv_buf);
+        luat_heap_free(ctx);
+        return NULL;
+    }
+    ctx->send_buf_size = RTSP_BUFFER_SIZE;
+    ctx->send_pos = 0;
+    
+    ctx->rtp_buf = (uint8_t *)luat_heap_malloc(RTP_BUFFER_SIZE);
+    if (!ctx->rtp_buf) {
+        LLOGE("RTP缓冲区分配失败");
+        luat_heap_free(ctx->send_buf);
+        luat_heap_free(ctx->recv_buf);
+        luat_heap_free(ctx);
+        return NULL;
+    }
+    ctx->rtp_buf_size = RTP_BUFFER_SIZE;
+    
+    // 初始化RTSP状态
+    ctx->state = RTSP_STATE_IDLE;
+    ctx->cseq = 1;
+    ctx->rtp_sequence = (uint32_t)luat_mcu_ticks();
+    ctx->rtp_ssrc = (uint32_t)luat_mcu_ticks();
+    ctx->start_tick = luat_mcu_ticks();
+    ctx->last_rtcp_time = luat_mcu_ticks();
+    
+    // 初始化帧队列
+    ctx->frame_head = NULL;
+    ctx->frame_tail = NULL;
+    ctx->frame_queue_bytes = 0;
+    
+    g_rtsp_ctx = ctx;
+    LLOGD("RTSP上下文创建成功");
+    return ctx;
+}
+
+/**
+ * 销毁RTSP推流上下文
+ */
+int rtsp_destroy(rtsp_ctx_t *ctx) {
+    if (!ctx) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    // 断开连接
+    if (ctx->state != RTSP_STATE_IDLE) {
+        rtsp_disconnect(ctx);
+    }
+    
+    // 清空队列
+    rtsp_clear_frame_queue(ctx);
+    
+    // 释放TCP连接
+    if (ctx->control_pcb) {
+        tcp_abort(ctx->control_pcb);
+        ctx->control_pcb = NULL;
+    }
+    
+    // 释放UDP连接
+    if (ctx->rtp_pcb) {
+        udp_remove(ctx->rtp_pcb);
+        ctx->rtp_pcb = NULL;
+    }
+    if (ctx->rtcp_pcb) {
+        udp_remove(ctx->rtcp_pcb);
+        ctx->rtcp_pcb = NULL;
+    }
+    
+    // 释放缓冲区
+    if (ctx->recv_buf) {
+        luat_heap_free(ctx->recv_buf);
+        ctx->recv_buf = NULL;
+    }
+    if (ctx->send_buf) {
+        luat_heap_free(ctx->send_buf);
+        ctx->send_buf = NULL;
+    }
+    if (ctx->rtp_buf) {
+        luat_heap_free(ctx->rtp_buf);
+        ctx->rtp_buf = NULL;
+    }
+    
+    // 释放字符串
+    if (ctx->url) {
+        luat_heap_free(ctx->url);
+        ctx->url = NULL;
+    }
+    if (ctx->host) {
+        luat_heap_free(ctx->host);
+        ctx->host = NULL;
+    }
+    if (ctx->stream) {
+        luat_heap_free(ctx->stream);
+        ctx->stream = NULL;
+    }
+    if (ctx->auth) {
+        luat_heap_free(ctx->auth);
+        ctx->auth = NULL;
+    }
+    if (ctx->session_id) {
+        luat_heap_free(ctx->session_id);
+        ctx->session_id = NULL;
+    }
+    if (ctx->sps_data) {
+        luat_heap_free(ctx->sps_data);
+        ctx->sps_data = NULL;
+    }
+    if (ctx->pps_data) {
+        luat_heap_free(ctx->pps_data);
+        ctx->pps_data = NULL;
+    }
+    if (ctx->sprop_parameter_sets) {
+        luat_heap_free(ctx->sprop_parameter_sets);
+        ctx->sprop_parameter_sets = NULL;
+    }
+    
+    luat_heap_free(ctx);
+    LLOGD("RTSP上下文已销毁");
+    return RTSP_OK;
+}
+
+/**
+ * 设置RTSP服务器URL
+ */
+int rtsp_set_url(rtsp_ctx_t *ctx, const char *url) {
+    if (!ctx || !url) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    if (ctx->url) {
+        luat_heap_free(ctx->url);
+    }
+    
+    ctx->url = (char *)luat_heap_malloc(strlen(url) + 1);
+    if (!ctx->url) {
+        LLOGE("URL内存分配失败");
+        return RTSP_ERR_NO_MEMORY;
+    }
+    
+    strcpy(ctx->url, url);
+    
+    if (rtsp_parse_url(ctx, url) != RTSP_OK) {
+        LLOGE("URL解析失败: %s", url);
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    LLOGD("RTSP URL设置: %s", url);
+    return RTSP_OK;
+}
+
+/**
+ * 设置状态变化回调函数
+ */
+int rtsp_set_state_callback(rtsp_ctx_t *ctx, rtsp_state_callback_t callback) {
+    if (!ctx) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    ctx->user_data = (void *)callback;
+    return RTSP_OK;
+}
+
+/**
+ * 设置H.264 SPS数据
+ */
+int rtsp_set_sps(rtsp_ctx_t *ctx, const uint8_t *sps_data, uint32_t sps_len) {
+    if (!ctx || !sps_data || sps_len == 0 || sps_len > 1024) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    if (ctx->sps_data) {
+        luat_heap_free(ctx->sps_data);
+    }
+    
+    ctx->sps_data = (uint8_t *)luat_heap_malloc(sps_len);
+    if (!ctx->sps_data) {
+        LLOGE("SPS数据内存分配失败");
+        return RTSP_ERR_NO_MEMORY;
+    }
+    
+    memcpy(ctx->sps_data, sps_data, sps_len);
+    ctx->sps_len = sps_len;
+    
+    LLOGD("SPS设置完毕: %u字节", sps_len);
+    return RTSP_OK;
+}
+
+/**
+ * 设置H.264 PPS数据
+ */
+int rtsp_set_pps(rtsp_ctx_t *ctx, const uint8_t *pps_data, uint32_t pps_len) {
+    if (!ctx || !pps_data || pps_len == 0 || pps_len > 1024) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    if (ctx->pps_data) {
+        luat_heap_free(ctx->pps_data);
+    }
+    
+    ctx->pps_data = (uint8_t *)luat_heap_malloc(pps_len);
+    if (!ctx->pps_data) {
+        LLOGE("PPS数据内存分配失败");
+        return RTSP_ERR_NO_MEMORY;
+    }
+    
+    memcpy(ctx->pps_data, pps_data, pps_len);
+    ctx->pps_len = pps_len;
+    
+    LLOGD("PPS设置完毕: %u字节", pps_len);
+    return RTSP_OK;
+}
+
+/**
+ * 推送H.264视频帧
+ */
+int rtsp_push_h264_frame(rtsp_ctx_t *ctx, const uint8_t *frame_data, 
+                         uint32_t frame_len, uint32_t timestamp) {
+    if (!ctx || !frame_data || frame_len == 0) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    // 如果在PLAYING状态,直接处理;否则进入队列
+    if (ctx->state == RTSP_STATE_PLAYING) {
+        // 直接构建RTP包并发送
+        return rtsp_build_rtp_payload(ctx, frame_data, frame_len, timestamp);
+    } else if (1) {
+        return 0; // 丢弃非PLAYING状态下的帧
+    } else {
+        LLOGI("当前状态非PLAYING,帧加入发送队列 %d %d", frame_len, ctx->state);
+        // 加入队列
+        struct rtsp_frame_node *node = (struct rtsp_frame_node *)
+            luat_heap_malloc(sizeof(struct rtsp_frame_node));
+        if (!node) {
+            LLOGE("帧队列节点内存分配失败");
+            return RTSP_ERR_NO_MEMORY;
+        }
+        
+        node->data = (uint8_t *)luat_heap_malloc(frame_len);
+        if (!node->data) {
+            LLOGE("帧数据内存分配失败");
+            luat_heap_free(node);
+            return RTSP_ERR_NO_MEMORY;
+        }
+        
+        memcpy(node->data, frame_data, frame_len);
+        node->len = frame_len;
+        node->timestamp = timestamp;
+        node->next = NULL;
+        
+        // 检查队列大小
+        if (ctx->frame_queue_bytes + frame_len > RTSP_MAX_QUEUE_BYTES) {
+            // 丢弃最早的非关键帧
+            if (ctx->frame_head && ctx->frame_head != ctx->frame_tail) {
+                struct rtsp_frame_node *temp = ctx->frame_head;
+                ctx->frame_head = ctx->frame_head->next;
+                ctx->frame_queue_bytes -= temp->len;
+                
+                luat_heap_free(temp->data);
+                luat_heap_free(temp);
+                LLOGD("队列溢出,丢弃一帧");
+            }
+        }
+        
+        if (!ctx->frame_head) {
+            ctx->frame_head = node;
+        } else {
+            ctx->frame_tail->next = node;
+        }
+        ctx->frame_tail = node;
+        ctx->frame_queue_bytes += frame_len;
+        
+        //RTSP_LOGV("帧已加入队列: %u字节, 队列总大小: %u字节", frame_len, ctx->frame_queue_bytes);
+        return (int)frame_len;
+    }
+}
+
+/**
+ * 获取当前连接状态
+ */
+rtsp_state_t rtsp_get_state(rtsp_ctx_t *ctx) {
+    if (!ctx) {
+        return RTSP_STATE_IDLE;
+    }
+    return ctx->state;
+}
+
+/**
+ * 处理RTSP事件轮询
+ */
+int rtsp_poll(rtsp_ctx_t *ctx) {
+    if (!ctx) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    uint32_t now = luat_mcu_ticks();
+    
+    // 检查连接超时
+    if (ctx->state != RTSP_STATE_IDLE && ctx->state != RTSP_STATE_ERROR && ctx->state != RTSP_STATE_PLAYING) {
+        if (now - ctx->last_activity_time > RTSP_CMD_TIMEOUT) {
+            LLOGE("RTSP连接超时");
+            rtsp_set_state(ctx, RTSP_STATE_ERROR, RTSP_ERR_TIMEOUT);
+            return RTSP_ERR_TIMEOUT;
+        }
+    }
+    
+    // 发送队列中的帧
+    if (ctx->state == RTSP_STATE_PLAYING) {
+        rtsp_send_queued_frames(ctx);
+        
+        // 定期发送RTCP Sender Report (每5秒一次)
+        if (now - ctx->last_rtcp_time >= 5000) {
+            LLOGD("触发RTCP SR发送: 距上次%u毫秒", now - ctx->last_rtcp_time);
+            int ret = rtsp_send_rtcp_sr(ctx);
+            if (ret == RTSP_OK) {
+                ctx->last_rtcp_time = now;
+            } else {
+                LLOGE("RTCP SR发送失败: %d", ret);
+            }
+        }
+    }
+    
+    return RTSP_OK;
+}
+
+/**
+ * 获取推流统计信息
+ */
+int rtsp_get_stats(rtsp_ctx_t *ctx, rtsp_stats_t *stats) {
+    if (!ctx || !stats) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    stats->bytes_sent = ctx->bytes_sent;
+    stats->video_frames_sent = ctx->video_frames_sent;
+    stats->rtp_packets_sent = ctx->rtp_packets_sent;
+    stats->connection_time = luat_mcu_ticks() - ctx->start_tick;
+    stats->last_video_timestamp = ctx->video_timestamp;
+    
+    return RTSP_OK;
+}
+
+/* ======================== 内部函数实现 ======================== */
+
+/**
+ * 解析RTSP URL
+ */
+static int rtsp_parse_url(rtsp_ctx_t *ctx, const char *url) {
+    if (strncmp(url, "rtsp://", 7) != 0) {
+        LLOGE("非法的RTSP URL格式");
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    const char *ptr = url + 7;
+    const char *host_end = strchr(ptr, ':');
+    const char *stream_start;
+    
+    // 解析主机名
+    if (!host_end) {
+        host_end = strchr(ptr, '/');
+        if (!host_end) {
+            host_end = ptr + strlen(ptr);
+        }
+    }
+    
+    int host_len = host_end - ptr;
+    ctx->host = (char *)luat_heap_malloc(host_len + 1);
+    if (!ctx->host) {
+        return RTSP_ERR_NO_MEMORY;
+    }
+    strncpy(ctx->host, ptr, host_len);
+    ctx->host[host_len] = '\0';
+    
+    // 解析端口
+    if (*host_end == ':') {
+        ctx->port = (uint16_t)atoi(host_end + 1);
+        stream_start = strchr(host_end + 1, '/');
+    } else {
+        ctx->port = RTSP_DEFAULT_PORT;
+        stream_start = host_end;
+    }
+    
+    // 解析流名
+    if (stream_start && *stream_start == '/') {
+        stream_start++;
+        int stream_len = strlen(stream_start);
+        ctx->stream = (char *)luat_heap_malloc(stream_len + 1);
+        if (!ctx->stream) {
+            return RTSP_ERR_NO_MEMORY;
+        }
+        strcpy(ctx->stream, stream_start);
+    } else {
+        ctx->stream = (char *)luat_heap_malloc(1);
+        if (!ctx->stream) {
+            return RTSP_ERR_NO_MEMORY;
+        }
+        ctx->stream[0] = '\0';
+    }
+    
+    LLOGD("RTSP URL解析: host=%s, port=%u, stream=%s", 
+          ctx->host, ctx->port, ctx->stream);
+    
+    return RTSP_OK;
+}
+
+/**
+ * 状态转移
+ */
+static void rtsp_set_state(rtsp_ctx_t *ctx, rtsp_state_t new_state, int error_code) {
+    if (ctx->state == new_state) {
+        return;
+    }
+    
+    rtsp_state_t old_state = ctx->state;
+    ctx->state = new_state;
+    ctx->last_activity_time = luat_mcu_ticks();
+    
+    LLOGD("RTSP状态转移: %d -> %d", (int)old_state, (int)new_state);
+    
+    // 调用回调函数
+    rtsp_state_callback_t callback = (rtsp_state_callback_t)ctx->user_data;
+    if (callback) {
+        callback(ctx, old_state, new_state, error_code);
+    }
+}
+
+/**
+ * 发送RTSP命令
+ */
+static int rtsp_send_command(rtsp_ctx_t *ctx, const char *method, 
+                             const char *resource, const char *extra_headers,
+                             const char *data, uint32_t data_len) {
+    if (!ctx->control_pcb) {
+        LLOGE("RTSP TCP连接未建立");
+        return RTSP_ERR_CONNECT_FAILED;
+    }
+    
+    // 构建RTSP请求行
+    int len = snprintf((char *)ctx->send_buf, ctx->send_buf_size,
+        "%s %s RTSP/1.0\r\n"
+        "CSeq: %u\r\n"
+        "User-Agent: LuatOS-RTSP/1.0\r\n",
+        method, resource, ctx->cseq++);
+    
+    if (len < 0 || len >= (int)ctx->send_buf_size) {
+        LLOGE("发送缓冲区溢出");
+        return RTSP_ERR_BUFFER_OVERFLOW;
+    }
+    
+    // 添加Session头(如果已建立会话)
+    if (ctx->session_id) {
+        int n = snprintf((char *)ctx->send_buf + len, ctx->send_buf_size - len,
+            "Session: %s\r\n", ctx->session_id);
+        if (n < 0 || len + n >= (int)ctx->send_buf_size) {
+            LLOGE("发送缓冲区溢出");
+            return RTSP_ERR_BUFFER_OVERFLOW;
+        }
+        len += n;
+    }
+    
+    // 添加额外头部
+    if (extra_headers) {
+        int n = strlen(extra_headers);
+        if (len + n >= (int)ctx->send_buf_size) {
+            LLOGE("发送缓冲区溢出");
+            return RTSP_ERR_BUFFER_OVERFLOW;
+        }
+        memcpy(ctx->send_buf + len, extra_headers, n);
+        len += n;
+    }
+    
+    // 添加空行分隔头部和body(仅当没有data时自动添加)
+    if (data_len == 0) {
+        if (len + 2 >= (int)ctx->send_buf_size) {
+            LLOGE("发送缓冲区溢出");
+            return RTSP_ERR_BUFFER_OVERFLOW;
+        }
+        memcpy(ctx->send_buf + len, "\r\n", 2);
+        len += 2;
+    }
+    
+    RTSP_LOGV("RTSP命令:\n%.*s", len, (char *)ctx->send_buf);
+    
+    // 发送TCP头部数据
+    err_t err = tcp_write(ctx->control_pcb, ctx->send_buf, len, TCP_WRITE_FLAG_COPY);
+    if (err != ERR_OK) {
+        LLOGE("TCP头部发送失败: %d", err);
+        return RTSP_ERR_NETWORK;
+    }
+    
+    // 发送body数据(如果有)
+    if (data && data_len > 0) {
+        // 需要发送空行 + body
+        err = tcp_write(ctx->control_pcb, (const void *)"\r\n", 2, TCP_WRITE_FLAG_COPY);
+        if (err != ERR_OK) {
+            LLOGE("TCP空行发送失败: %d", err);
+            return RTSP_ERR_NETWORK;
+        }
+        
+        err = tcp_write(ctx->control_pcb, (const void *)data, data_len, TCP_WRITE_FLAG_COPY);
+        if (err != ERR_OK) {
+            LLOGE("TCP数据体发送失败: %d", err);
+            return RTSP_ERR_NETWORK;
+        }
+    }
+    
+    tcp_output(ctx->control_pcb);
+    return RTSP_OK;
+}
+
+/**
+ * TCP连接回调
+ */
+static err_t rtsp_tcp_connect_callback(void *arg, struct tcp_pcb *pcb, err_t err) {
+    rtsp_ctx_t *ctx = (rtsp_ctx_t *)arg;
+    
+    if (err != ERR_OK) {
+        LLOGE("TCP连接失败: %d", err);
+        rtsp_set_state(ctx, RTSP_STATE_ERROR, RTSP_ERR_CONNECT_FAILED);
+        return ERR_ABRT;
+    }
+    
+    LLOGD("TCP连接成功");
+    rtsp_set_state(ctx, RTSP_STATE_OPTIONS, 0);
+    
+    // 设置TCP回调函数
+    tcp_recv(pcb, rtsp_tcp_recv_callback);
+    tcp_err(pcb, rtsp_tcp_error_callback);
+    tcp_arg(pcb, ctx);
+    
+    // 发送OPTIONS请求(使用具体的stream URI)
+    if (rtsp_send_command(ctx, "OPTIONS", ctx->url, NULL, NULL, 0) == RTSP_OK) {
+        rtsp_set_state(ctx, RTSP_STATE_OPTIONS, 0);
+    }
+    
+    return ERR_OK;
+}
+
+/**
+ * TCP接收回调
+ */
+static err_t rtsp_tcp_recv_callback(void *arg, struct tcp_pcb *pcb, 
+                                   struct pbuf *p, err_t err) {
+    rtsp_ctx_t *ctx = (rtsp_ctx_t *)arg;
+    
+    if (err != ERR_OK) {
+        LLOGE("TCP接收错误: %d", err);
+        if (p) pbuf_free(p);
+        return ERR_ABRT;
+    }
+    
+    if (!p) {
+        LLOGD("TCP连接已关闭");
+        rtsp_set_state(ctx, RTSP_STATE_IDLE, 0);
+        return ERR_OK;
+    }
+    
+    // 复制数据到接收缓冲区
+    if (ctx->recv_pos + p->tot_len > ctx->recv_buf_size) {
+        LLOGE("接收缓冲区溢出");
+        pbuf_free(p);
+        return ERR_ABRT;
+    }
+    
+    pbuf_copy_partial(p, ctx->recv_buf + ctx->recv_pos, p->tot_len, 0);
+    ctx->recv_pos += p->tot_len;
+    
+    // 更新活动时间
+    ctx->last_activity_time = luat_mcu_ticks();
+    
+    RTSP_LOGV("接收RTSP数据: %u字节", p->tot_len);
+    pbuf_free(p);
+    
+    // 处理RTSP响应
+    rtsp_handle_response(ctx);
+    tcp_recved(pcb, p->tot_len);
+    
+    return ERR_OK;
+}
+
+/**
+ * TCP错误回调
+ */
+static void rtsp_tcp_error_callback(void *arg, err_t err) {
+    rtsp_ctx_t *ctx = (rtsp_ctx_t *)arg;
+    LLOGE("TCP错误: %d", err);
+    rtsp_set_state(ctx, RTSP_STATE_ERROR, RTSP_ERR_NETWORK);
+}
+
+/**
+ * 处理RTSP响应
+ */
+static int rtsp_handle_response(rtsp_ctx_t *ctx) {
+    // 检查是否已收到完整的响应
+    char *resp_end = strstr((char *)ctx->recv_buf, "\r\n\r\n");
+    if (!resp_end) {
+        // 尚未接收完整响应
+        return RTSP_OK;
+    }
+    
+    // 打印完整的RTSP响应以便调试
+    int resp_len = (resp_end - (char *)ctx->recv_buf) + 4;
+    LLOGD("RTSP响应内容:\n%.*s", resp_len, (char *)ctx->recv_buf);
+    
+    // 解析响应行: RTSP/1.0 <status> <reason>
+    char *line_start = (char *)ctx->recv_buf;
+    char *line_end = strstr(line_start, "\r\n");
+    if (!line_end) {
+        LLOGE("无效的RTSP响应");
+        rtsp_set_state(ctx, RTSP_STATE_ERROR, RTSP_ERR_HANDSHAKE_FAILED);
+        return RTSP_ERR_HANDSHAKE_FAILED;
+    }
+    
+    *line_end = '\0';
+    int status_code = 0;
+    if (sscanf(line_start, "RTSP/1.0 %d", &status_code) != 1) {
+        LLOGE("RTSP响应行解析失败");
+        *line_end = '\r';
+        rtsp_set_state(ctx, RTSP_STATE_ERROR, RTSP_ERR_HANDSHAKE_FAILED);
+        return RTSP_ERR_HANDSHAKE_FAILED;
+    }
+    *line_end = '\r';
+    
+    LLOGD("RTSP响应: 状态码=%d", status_code);
+    
+    if (status_code != 200) {
+        LLOGE("RTSP服务器错误: %d", status_code);
+        rtsp_set_state(ctx, RTSP_STATE_ERROR, RTSP_ERR_HANDSHAKE_FAILED);
+        return RTSP_ERR_HANDSHAKE_FAILED;
+    }
+    
+    // 解析响应头
+    char *header_start = line_end + 2;
+    char *session_id = NULL;
+    
+    while (header_start < resp_end) {
+        line_end = strstr(header_start, "\r\n");
+        if (!line_end) break;
+        
+        *line_end = '\0';
+        
+        // 提取Session头
+        if (rtsp_strncasecmp(header_start, "Session:", 8) == 0) {
+            char *value = header_start + 8;
+            while (*value == ' ' || *value == '\t') value++;
+            
+            // Session ID可能包含超时参数 (e.g. "12345678;timeout=60")
+            char *semicolon = strchr(value, ';');
+            int len = semicolon ? (semicolon - value) : strlen(value);
+            
+            if (len > 0) {
+                session_id = (char *)luat_heap_malloc(len + 1);
+                if (session_id) {
+                    strncpy(session_id, value, len);
+                    session_id[len] = '\0';
+                    LLOGD("提取Session ID: %s", session_id);
+                }
+            }
+        }
+        
+        // 提取Transport头
+        if (rtsp_strncasecmp(header_start, "Transport:", 10) == 0) {
+            char *value = header_start + 10;
+            while (*value == ' ' || *value == '\t') value++;
+            
+            // 解析server_port参数
+            char *server_port = strstr(value, "server_port=");
+            if (server_port) {
+                server_port += 12;  // strlen("server_port=")
+                int rtp_port = 0, rtcp_port = 0;
+                if (sscanf(server_port, "%d-%d", &rtp_port, &rtcp_port) == 2) {
+                    ctx->remote_rtp_port = (uint16_t)rtp_port;
+                    ctx->remote_rtcp_port = (uint16_t)rtcp_port;
+                    LLOGD("服务器端口: RTP=%u, RTCP=%u", 
+                          ctx->remote_rtp_port, ctx->remote_rtcp_port);
+                }
+            }
+            
+            // 解析client_port参数(如果服务器返回)
+            char *client_port = strstr(value, "client_port=");
+            if (client_port) {
+                client_port += 12;  // strlen("client_port=")
+                int rtp_port = 0, rtcp_port = 0;
+                if (sscanf(client_port, "%d-%d", &rtp_port, &rtcp_port) == 2) {
+                    ctx->local_rtp_port = (uint16_t)rtp_port;
+                    ctx->local_rtcp_port = (uint16_t)rtcp_port;
+                    LLOGD("本地端口: RTP=%u, RTCP=%u", 
+                          ctx->local_rtp_port, ctx->local_rtcp_port);
+                }
+            }
+        }
+        
+        *line_end = '\r';
+        header_start = line_end + 2;
+    }
+    
+    // 保存Session ID
+    if (session_id && !ctx->session_id) {
+        ctx->session_id = session_id;
+    } else if (session_id) {
+        luat_heap_free(session_id);
+    }
+    
+    // 根据当前状态处理下一步
+    switch (ctx->state) {
+        case RTSP_STATE_OPTIONS:
+            // OPTIONS成功,发送ANNOUNCE
+            {
+                char sdp[2048];
+                int sdp_len = 0;
+                
+                // 如果有SPS/PPS,构建完整的SDP;否则使用基础SDP
+                if (ctx->sps_data && ctx->sps_len > 0 && ctx->pps_data && ctx->pps_len > 0) {
+                    // 计算profile-level-id (从SPS中提取)
+                    char profile_level_id[16] = {0};
+                    if (ctx->sps_len >= 4) {
+                        snprintf(profile_level_id, sizeof(profile_level_id), "%02X%02X%02X",
+                                ctx->sps_data[1], ctx->sps_data[2], ctx->sps_data[3]);
+                    } else {
+                        strcpy(profile_level_id, "42C01E");  // 默认Baseline Profile Level 3.0
+                    }
+                    
+                    // Base64编码SPS
+                    char sps_b64[512] = {0};
+                    int sps_b64_len = rtsp_base64_encode(ctx->sps_data, ctx->sps_len, 
+                                                         sps_b64, sizeof(sps_b64));
+                    
+                    // Base64编码PPS
+                    char pps_b64[512] = {0};
+                    int pps_b64_len = rtsp_base64_encode(ctx->pps_data, ctx->pps_len,
+                                                         pps_b64, sizeof(pps_b64));
+                    
+                    if (sps_b64_len > 0 && pps_b64_len > 0) {
+                        // 构建完整的SDP
+                        sdp_len = snprintf(sdp, sizeof(sdp),
+                            "v=0\r\n"
+                            "o=- 0 0 IN IP4 127.0.0.1\r\n"
+                            "s=LuatOS RTSP Stream\r\n"
+                            "c=IN IP4 0.0.0.0\r\n"
+                            "t=0 0\r\n"
+                            "m=video 0 RTP/AVP 96\r\n"
+                            "a=rtpmap:96 H264/90000\r\n"
+                            "a=fmtp:96 packetization-mode=1;profile-level-id=%s;sprop-parameter-sets=%s\r\n"
+                            "a=control:streamid=0\r\n",
+                            profile_level_id, (ctx->sprop_parameter_sets ? ctx->sprop_parameter_sets : "") );
+                        
+                        LLOGD("完整SDP生成: profile-level-id=%s", profile_level_id);
+                    }
+                }
+                
+                // 如果没有生成完整SDP,使用基础SDP
+                if (sdp_len <= 0) {
+                    sdp_len = snprintf(sdp, sizeof(sdp),
+                        "v=0\r\n"
+                        "o=- 0 0 IN IP4 127.0.0.1\r\n"
+                        "s=LuatOS RTSP Stream\r\n"
+                        "c=IN IP4 %s\r\n"
+                        "t=0 0\r\n"
+                        "m=video 0 RTP/AVP 96\r\n"
+                        "a=rtpmap:96 H264/90000\r\n"
+                        "a=fmtp:96 profile-level-id=1; sprop-parameter-sets=Z0IAKeNQCgC2QgAAB9AAAOpg0YAAmJgAL/L3gAE=,aM48gA==\r\n"
+                        "a=control:streamid=0\r\n", ctx->host);
+                    
+                    LLOGD("使用基础SDP(无SPS/PPS)");
+                }
+                
+                if (sdp_len > 0 && sdp_len < (int)sizeof(sdp)) {
+                    char headers[512] = {0};
+                    snprintf(headers, sizeof(headers),
+                        "Content-Type: application/sdp\r\n"
+                        "Content-Length: %d\r\n", sdp_len);
+                    
+                    if (rtsp_send_command(ctx, "ANNOUNCE", ctx->url, headers, sdp, sdp_len) == RTSP_OK) {
+                        rtsp_set_state(ctx, RTSP_STATE_DESCRIBE, 0);
+                    }
+                }
+            }
+            break;
+            
+        case RTSP_STATE_DESCRIBE:
+            // ANNOUNCE/DESCRIBE成功,发送SETUP
+            {
+                char setup_headers[512] = {0};
+                snprintf(setup_headers, sizeof(setup_headers),
+                    "Transport: RTP/AVP/UDP;unicast;client_port=%u-%u;mode=record\r\n",
+                    ctx->local_rtp_port ? ctx->local_rtp_port : 19030,
+                    ctx->local_rtcp_port ? ctx->local_rtcp_port : 19031);
+                
+                char resource[512] = {0};
+                snprintf(resource, sizeof(resource), "%s/streamid=0", ctx->url);
+                
+                if (rtsp_send_command(ctx, "SETUP", resource, setup_headers, NULL, 0) == RTSP_OK) {
+                    rtsp_set_state(ctx, RTSP_STATE_SETUP, 0);
+                }
+            }
+            break;
+            
+        case RTSP_STATE_SETUP:
+            // SETUP成功,发送PLAY
+            if (rtsp_send_command(ctx, "RECORD", ctx->url, NULL, NULL, 0) == RTSP_OK) {
+                rtsp_set_state(ctx, RTSP_STATE_PLAY, 0);
+            }
+            break;
+            
+        case RTSP_STATE_PLAY:
+            // RECORD/PLAY成功,进入推流状态
+            rtsp_set_state(ctx, RTSP_STATE_PLAYING, 0);
+            LLOGD("RTSP握手完成,开始推流");
+            
+            // 立即发送一次RTCP Sender Report
+            LLOGD("RTSP就绪,发送初始RTCP SR");
+            rtsp_send_rtcp_sr(ctx);
+            ctx->last_rtcp_time = luat_mcu_ticks();
+            
+            // 通知摄像头开始采集
+            extern int luat_camera_capture(int id, uint8_t quality, const char *path);
+            luat_camera_capture(0, 80, "rtsp");
+            break;
+            
+        default:
+            break;
+    }
+    
+    // 清空接收缓冲区
+    resp_len = (resp_end - (char *)ctx->recv_buf) + 4;
+    if (ctx->recv_pos > (uint32_t)resp_len) {
+        memmove(ctx->recv_buf, ctx->recv_buf + resp_len, ctx->recv_pos - resp_len);
+        ctx->recv_pos -= resp_len;
+    } else {
+        ctx->recv_pos = 0;
+    }
+    
+    return RTSP_OK;
+}
+
+/**
+ * 发送RTP包
+ */
+static int rtsp_send_rtp_packet(rtsp_ctx_t *ctx, const uint8_t *data, 
+                                uint32_t len, uint32_t timestamp, uint8_t marker) {
+    if (!ctx->rtp_pcb) {
+        LLOGE("RTP UDP连接未建立");
+        return RTSP_ERR_CONNECT_FAILED;
+    }
+    
+    // 构建RTP头
+    uint8_t rtp_header[RTP_HEADER_SIZE];
+    uint32_t rtp_ts = (timestamp * RTP_TIMESTAMP_BASE) / 1000;
+    
+    // 计算与上个包的时间戳差值(用于诊断时间戳跳跃)
+    static uint32_t last_rtp_ts = 0;
+    int32_t ts_delta = (int32_t)(rtp_ts - last_rtp_ts);
+    
+    RTSP_LOGV("RTP包: seq=%u, ts=%u, ts_delta=%d, marker=%u, len=%u, timestamp_ms=%u",
+              ctx->rtp_sequence, rtp_ts, ts_delta, marker, len, timestamp);
+    
+    // 如果时间戳跳跃异常大(可能导致抖动),记录警告
+    if (last_rtp_ts > 0 && (ts_delta < -90000 || ts_delta > 270000)) {
+        LLOGW("时间戳异常跳跃: seq=%u, last_ts=%u, curr_ts=%u, delta=%d (应在-90000~270000范围内,对应帧率25~120fps)",
+              ctx->rtp_sequence, last_rtp_ts, rtp_ts, ts_delta);
+    }
+    last_rtp_ts = rtp_ts;
+    
+    // V=2, P=0, X=0, CC=0
+    rtp_header[0] = (2 << 6) | 0;
+    // M=marker, PT=96(H.264)
+    rtp_header[1] = (marker << 7) | RTP_PAYLOAD_TYPE_H264;
+    // Sequence number (big-endian)
+    rtp_header[2] = (ctx->rtp_sequence >> 8) & 0xFF;
+    rtp_header[3] = ctx->rtp_sequence & 0xFF;
+    // Timestamp (big-endian)
+    rtp_header[4] = (rtp_ts >> 24) & 0xFF;
+    rtp_header[5] = (rtp_ts >> 16) & 0xFF;
+    rtp_header[6] = (rtp_ts >> 8) & 0xFF;
+    rtp_header[7] = rtp_ts & 0xFF;
+    // SSRC (big-endian)
+    rtp_header[8] = (ctx->rtp_ssrc >> 24) & 0xFF;
+    rtp_header[9] = (ctx->rtp_ssrc >> 16) & 0xFF;
+    rtp_header[10] = (ctx->rtp_ssrc >> 8) & 0xFF;
+    rtp_header[11] = ctx->rtp_ssrc & 0xFF;
+    
+    ctx->rtp_sequence++;
+    
+    // 构建完整RTP包
+    if (RTP_HEADER_SIZE + len > ctx->rtp_buf_size) {
+        LLOGE("RTP缓冲区溢出");
+        return RTSP_ERR_BUFFER_OVERFLOW;
+    }
+    
+    memcpy(ctx->rtp_buf, rtp_header, RTP_HEADER_SIZE);
+    memcpy(ctx->rtp_buf + RTP_HEADER_SIZE, data, len);
+    
+    // 发送UDP包
+    struct pbuf *pb = pbuf_alloc(PBUF_TRANSPORT, RTP_HEADER_SIZE + len, PBUF_RAM);
+    if (!pb) {
+        LLOGE("UDP数据包分配失败");
+        return RTSP_ERR_NO_MEMORY;
+    }
+    
+    memcpy(pb->payload, ctx->rtp_buf, RTP_HEADER_SIZE + len);
+    
+    err_t err = udp_sendto(ctx->rtp_pcb, pb, &ctx->remote_ip, ctx->remote_rtp_port);
+    pbuf_free(pb);
+    
+    if (err != ERR_OK) {
+        LLOGE("RTP UDP发送失败: err=%d, remote_ip=%s, remote_rtp_port=%u, len=%u", 
+              err, ipaddr_ntoa(&ctx->remote_ip), ctx->remote_rtp_port, RTP_HEADER_SIZE + len);
+        return RTSP_ERR_NETWORK;
+    }
+    
+    ctx->bytes_sent += RTP_HEADER_SIZE + len;
+    ctx->rtp_packets_sent++;
+    
+    // RTSP_LOGV("RTP包已发送: %u字节, 序列号: %u", RTP_HEADER_SIZE + len, ctx->rtp_sequence - 1);
+    
+    return RTSP_OK;
+}
+
+/**
+ * 构建RTP载荷 - 支持多NALU解析和FU-A分片
+ */
+static int rtsp_build_rtp_payload(rtsp_ctx_t *ctx, const uint8_t *frame_data, 
+                                  uint32_t frame_len, uint32_t timestamp) {
+    if (!frame_data || frame_len == 0) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    // 如果传入的 timestamp 为 0,使用上下文中的时间戳
+    if (timestamp == 0) {
+        timestamp = ctx->video_timestamp;
+    }
+    const uint8_t *ptr = frame_data;
+    uint32_t remaining = frame_len;
+    int nalu_count = 0;
+    int total_sent = 0;
+    
+    RTSP_LOGV("处理帧数据: %u字节", frame_len);
+    
+    // 解析并发送所有NALU
+    while (remaining > 0) {
+        // 查找当前NALU的起始码
+        uint32_t start_code_len = 0;
+        const uint8_t *nalu_start = rtsp_find_next_nalu(ptr, remaining, &start_code_len);
+        
+        if (!nalu_start) {
+            // 没有找到起始码,可能整个数据就是一个NALU
+            if (nalu_count == 0 && remaining > 0) {
+                nalu_start = ptr;
+                start_code_len = 0;
+            } else {
+                break;
+            }
+        }
+        
+        // 跳过起始码
+        const uint8_t *nalu_data = nalu_start + start_code_len;
+        uint32_t nalu_available = remaining - (nalu_start - ptr) - start_code_len;
+        
+        if (nalu_available == 0) {
+            break;
+        }
+        
+        // 查找下一个NALU起始码,确定当前NALU长度
+        uint32_t next_start_code_len = 0;
+        const uint8_t *next_nalu = rtsp_find_next_nalu(nalu_data + 1, nalu_available - 1, &next_start_code_len);
+        
+        uint32_t nalu_len;
+        if (next_nalu) {
+            nalu_len = next_nalu - nalu_data;
+        } else {
+            nalu_len = nalu_available;
+        }
+        
+        if (nalu_len > 0) {
+            uint8_t nalu_type = nalu_data[0] & 0x1F;
+            RTSP_LOGV("发现NALU: 类型=%u, 长度=%u", nalu_type, nalu_len);
+            
+            // 自动保存SPS/PPS
+            if (nalu_type == NALU_TYPE_SPS && (!ctx->sps_data || ctx->sps_len != nalu_len)) {
+                if (ctx->sps_data) {
+                    luat_heap_free(ctx->sps_data);
+                }
+                ctx->sps_data = (uint8_t *)luat_heap_malloc(nalu_len);
+                if (ctx->sps_data) {
+                    memcpy(ctx->sps_data, nalu_data, nalu_len);
+                    ctx->sps_len = nalu_len;
+                    LLOGD("自动提取SPS: %u字节", nalu_len);
+                    // 详细解析SPS
+                    rtsp_parse_sps_detail(nalu_data, nalu_len);
+                    // 打印sprop-sps(Base64)
+                    char b64_sps[256];
+                    if (rtsp_base64_encode(nalu_data, nalu_len, b64_sps, sizeof(b64_sps)) >= 0) {
+                        LLOGD("sprop-sps=%s", b64_sps);
+                        // 如果已有PPS,合成sprop-parameter-sets
+                        if (ctx->pps_data && ctx->pps_len > 0) {
+                            char b64_pps[256];
+                            if (rtsp_base64_encode(ctx->pps_data, ctx->pps_len, b64_pps, sizeof(b64_pps)) >= 0) {
+                                size_t total_len = strlen(b64_sps) + 1 + strlen(b64_pps) + 1;
+                                if (ctx->sprop_parameter_sets) {
+                                    luat_heap_free(ctx->sprop_parameter_sets);
+                                    ctx->sprop_parameter_sets = NULL;
+                                }
+                                ctx->sprop_parameter_sets = (char*)luat_heap_malloc(total_len);
+                                if (ctx->sprop_parameter_sets) {
+                                    snprintf(ctx->sprop_parameter_sets, total_len, "%s,%s", b64_sps, b64_pps);
+                                    LLOGD("sprop-parameter-sets=%s", ctx->sprop_parameter_sets);
+                                } else {
+                                    LLOGW("分配sprop-parameter-sets失败");
+                                }
+                            }
+                        }
+                    } else {
+                        LLOGW("sprop-sps Base64编码失败");
+                    }
+                }
+            } else if (nalu_type == NALU_TYPE_PPS && (!ctx->pps_data || ctx->pps_len != nalu_len)) {
+                if (ctx->pps_data) {
+                    luat_heap_free(ctx->pps_data);
+                }
+                ctx->pps_data = (uint8_t *)luat_heap_malloc(nalu_len);
+                if (ctx->pps_data) {
+                    memcpy(ctx->pps_data, nalu_data, nalu_len);
+                    ctx->pps_len = nalu_len;
+                    LLOGD("自动提取PPS: %u字节", nalu_len);
+                    // 详细解析PPS
+                    rtsp_parse_pps_detail(nalu_data, nalu_len);
+                    // 打印sprop-pps(Base64)
+                    char b64_pps[256];
+                    if (rtsp_base64_encode(nalu_data, nalu_len, b64_pps, sizeof(b64_pps)) >= 0) {
+                        LLOGD("sprop-pps=%s", b64_pps);
+                        // 如果已有SPS,合成sprop-parameter-sets
+                        if (ctx->sps_data && ctx->sps_len > 0) {
+                            char b64_sps[256];
+                            if (rtsp_base64_encode(ctx->sps_data, ctx->sps_len, b64_sps, sizeof(b64_sps)) >= 0) {
+                                size_t total_len = strlen(b64_sps) + 1 + strlen(b64_pps) + 1;
+                                if (ctx->sprop_parameter_sets) {
+                                    luat_heap_free(ctx->sprop_parameter_sets);
+                                    ctx->sprop_parameter_sets = NULL;
+                                }
+                                ctx->sprop_parameter_sets = (char*)luat_heap_malloc(total_len);
+                                if (ctx->sprop_parameter_sets) {
+                                    snprintf(ctx->sprop_parameter_sets, total_len, "%s,%s", b64_sps, b64_pps);
+                                    LLOGD("sprop-parameter-sets=%s", ctx->sprop_parameter_sets);
+                                } else {
+                                    LLOGW("分配sprop-parameter-sets失败");
+                                }
+                            }
+                        }
+                    } else {
+                        LLOGW("sprop-pps Base64编码失败");
+                    }
+                }
+            }
+            
+            // 发送NALU(非SPS/PPS或在推流状态下也发送SPS/PPS)
+            if (nalu_type != NALU_TYPE_AUD) {  // 跳过AUD
+                // 判断是否为最后一个NALU
+                uint8_t is_last = (next_nalu == NULL) ? 1 : 0;
+                
+                int ret = rtsp_send_nalu(ctx, nalu_data, nalu_len, timestamp, is_last);
+                if (ret >= 0) {
+                    total_sent += ret;
+                    nalu_count++;
+                } else {
+                    LLOGE("NALU发送失败: %d", ret);
+                    return ret;
+                }
+            }
+        }
+        
+        // 移动到下一个NALU
+        if (next_nalu) {
+            uint32_t consumed = next_nalu - ptr;
+            ptr = next_nalu;
+            remaining -= consumed;
+        } else {
+            break;
+        }
+    }
+    
+    if (nalu_count > 0) {
+        ctx->video_frames_sent++;
+        ctx->video_timestamp = timestamp;
+        RTSP_LOGV("帧发送完成: %d个NALU, 总计%d字节", nalu_count, total_sent);
+    }
+    
+    return total_sent > 0 ? total_sent : RTSP_ERR_FAILED;
+}
+
+/**
+ * 发送队列中的帧
+ */
+static int rtsp_send_queued_frames(rtsp_ctx_t *ctx) {
+    int sent_count = 0;
+    
+    while (ctx->frame_head && sent_count < 5) {  // 每次轮询最多发送5帧
+        struct rtsp_frame_node *node = ctx->frame_head;
+        
+        if (rtsp_build_rtp_payload(ctx, node->data, node->len, node->timestamp) == RTSP_OK) {
+            ctx->frame_head = node->next;
+            ctx->frame_queue_bytes -= node->len;
+            
+            luat_heap_free(node->data);
+            luat_heap_free(node);
+            
+            sent_count++;
+        } else {
+            break;  // 发送失败,暂停发送
+        }
+    }
+    
+    if (!ctx->frame_head) {
+        ctx->frame_tail = NULL;
+    }
+    
+    //RTSP_LOGV("队列帧发送: %d帧, 剩余队列: %u字节", sent_count, ctx->frame_queue_bytes);
+    
+    return RTSP_OK;
+}
+
+/**
+ * 清空帧队列
+ */
+static void rtsp_clear_frame_queue(rtsp_ctx_t *ctx) {
+    struct rtsp_frame_node *node = ctx->frame_head;
+    
+    while (node) {
+        struct rtsp_frame_node *next = node->next;
+        if (node->data) {
+            luat_heap_free(node->data);
+        }
+        luat_heap_free(node);
+        node = next;
+    }
+    
+    ctx->frame_head = NULL;
+    ctx->frame_tail = NULL;
+    ctx->frame_queue_bytes = 0;
+}
+
+/**
+ * 大小写不敏感字符串比较(用于HTTP头解析)
+ */
+static int rtsp_strncasecmp(const char *a, const char *b, size_t n) {
+    for (size_t i = 0; i < n; i++) {
+        int ca = tolower((unsigned char)a[i]);
+        int cb = tolower((unsigned char)b[i]);
+        if (ca != cb || ca == 0) {
+            return ca - cb;
+        }
+    }
+    return 0;
+}
+
+/**
+ * Base64编码函数
+ */
+static int rtsp_base64_encode(const uint8_t *src, uint32_t src_len, char *dst, uint32_t dst_len) {
+    static const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+    uint32_t i = 0, j = 0;
+    
+    // 计算所需的输出长度
+    uint32_t required_len = ((src_len + 2) / 3) * 4;
+    if (required_len >= dst_len) {
+        return -1;  // 缓冲区不足
+    }
+    
+    while (i < src_len) {
+        uint32_t octet_a = i < src_len ? src[i++] : 0;
+        uint32_t octet_b = i < src_len ? src[i++] : 0;
+        uint32_t octet_c = i < src_len ? src[i++] : 0;
+        
+        uint32_t triple = (octet_a << 16) + (octet_b << 8) + octet_c;
+        
+        dst[j++] = base64_table[(triple >> 18) & 0x3F];
+        dst[j++] = base64_table[(triple >> 12) & 0x3F];
+        dst[j++] = base64_table[(triple >> 6) & 0x3F];
+        dst[j++] = base64_table[triple & 0x3F];
+    }
+    
+    // 添加填充
+    uint32_t mod = src_len % 3;
+    if (mod == 1) {
+        dst[j - 1] = '=';
+        dst[j - 2] = '=';
+    } else if (mod == 2) {
+        dst[j - 1] = '=';
+    }
+    
+    dst[j] = '\0';
+    return j;
+}
+
+/**
+ * 查找下一个NALU起始码
+ * @return 起始码的位置,如果未找到返回NULL
+ */
+static const uint8_t* rtsp_find_next_nalu(const uint8_t *data, uint32_t len, uint32_t *start_code_len) {
+    if (len < 3) {
+        return NULL;
+    }
+    
+    for (uint32_t i = 0; i < len - 2; i++) {
+        // 查找0x000001或0x00000001
+        if (data[i] == 0 && data[i+1] == 0) {
+            if (data[i+2] == 1) {
+                *start_code_len = 3;
+                return &data[i];
+            } else if (i < len - 3 && data[i+2] == 0 && data[i+3] == 1) {
+                *start_code_len = 4;
+                return &data[i];
+            }
+        }
+    }
+    
+    return NULL;
+}
+
+/**
+ * 发送单个NALU(支持FU-A分片)
+ */
+static int rtsp_send_nalu(rtsp_ctx_t *ctx, const uint8_t *nalu_data, 
+                          uint32_t nalu_len, uint32_t timestamp, uint8_t is_last_nalu) {
+    if (!nalu_data || nalu_len == 0) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    uint8_t nalu_header = nalu_data[0];
+    uint8_t nalu_type = nalu_header & 0x1F;
+    uint8_t nri = nalu_header & 0x60;
+    
+    // 如果NALU小于最大载荷,直接发送
+    if (nalu_len <= RTP_MAX_PACKET_SIZE) {
+        int ret = rtsp_send_rtp_packet(ctx, nalu_data, nalu_len, timestamp, is_last_nalu);
+        return (ret == RTSP_OK) ? (int)nalu_len : ret;
+    }
+    
+    // 需要FU-A分片
+    RTSP_LOGV("NALU过大(%u字节),使用FU-A分片", nalu_len);
+    
+    uint32_t nalu_payload_len = nalu_len - 1;  // 去掉NALU头
+    const uint8_t *nalu_payload = nalu_data + 1;
+    uint32_t fragment_size = RTP_MAX_PACKET_SIZE - 2;  // FU indicator + FU header
+    uint32_t offset = 0;
+    uint32_t fragment_count = 0;
+    int total_sent = 0;
+    
+    while (offset < nalu_payload_len) {
+        uint32_t this_fragment_size = (nalu_payload_len - offset > fragment_size) ? 
+                                      fragment_size : (nalu_payload_len - offset);
+        
+        uint8_t fu_packet[RTP_MAX_PACKET_SIZE];
+        
+        // FU indicator: F(1bit) + NRI(2bits) + Type(5bits=28)
+        fu_packet[0] = nri | FU_A_TYPE;
+        
+        // FU header: S(1bit) + E(1bit) + R(1bit) + Type(5bits)
+        uint8_t fu_header = nalu_type;
+        if (offset == 0) {
+            fu_header |= 0x80;  // S bit (start)
+        }
+        if (offset + this_fragment_size >= nalu_payload_len) {
+            fu_header |= 0x40;  // E bit (end)
+        }
+        fu_packet[1] = fu_header;
+        
+        // 复制分片数据
+        memcpy(fu_packet + 2, nalu_payload + offset, this_fragment_size);
+        
+        // 发送分片,只有最后一个分片且是最后一个NALU时才设置marker
+        uint8_t marker = ((fu_header & 0x40) && is_last_nalu) ? 1 : 0;
+        
+        int ret = rtsp_send_rtp_packet(ctx, fu_packet, 2 + this_fragment_size, timestamp, marker);
+        if (ret != RTSP_OK) {
+            LLOGE("FU-A分片发送失败: %d", ret);
+            return ret;
+        }
+        
+        offset += this_fragment_size;
+        fragment_count++;
+        total_sent += (2 + this_fragment_size);
+    }
+    
+    RTSP_LOGV("FU-A分片完成: %u个分片", fragment_count);
+    return total_sent;
+}
+
+/**
+ * 转换系统时间到NTP时间戳
+ * NTP时间戳: 64位, 高32位是秒数(从1900年1月1日开始), 低32位是小数秒
+ */
+static void rtsp_get_ntp_timestamp(uint32_t *ntp_sec, uint32_t *ntp_frac) {
+    // 使用 SNTP 获取准确的 NTP 时间戳(毫秒单位,从1970年开始)
+    extern uint64_t luat_sntp_time64_ms();
+    uint64_t unix_ms = luat_sntp_time64_ms();
+    
+    // NTP时间戳从1900年开始,1970年1月1日对应的秒数为2208988800秒
+    const uint64_t NTP_OFFSET_SEC = 2208988800UL;
+    
+    // 将UNIX时间(毫秒)转换为NTP时间
+    // 1. UNIX秒数 = unix_ms / 1000
+    // 2. NTP秒数 = UNIX秒数 + NTP_OFFSET
+    uint64_t unix_sec = unix_ms / 1000;
+    uint32_t unix_ms_frac = (uint32_t)(unix_ms % 1000);
+    
+    // NTP秒数
+    *ntp_sec = (uint32_t)(unix_sec + NTP_OFFSET_SEC);
+    
+    // NTP小数部分: 将毫秒转换为 NTP 小数格式 (2^32 * ms / 1000)
+    *ntp_frac = (unix_ms_frac * 4294967296ULL) / 1000;
+    
+    RTSP_LOGV("NTP时间戳: unix_ms=%llu, unix_sec=%llu, ntp_sec=%u, ntp_frac=%u", 
+              unix_ms, unix_sec, *ntp_sec, *ntp_frac);
+}
+
+/**
+ * 发送RTCP Sender Report
+ * RTCP SR格式 (RFC 3550):
+ * 0                   1                   2                   3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |V=2|P|    RC   |   PT=SR=200   |             length            |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                         SSRC of sender                        |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |              NTP timestamp, most significant word             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |             NTP timestamp, least significant word             |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                         RTP timestamp                         |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                     sender's packet count                     |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |                      sender's octet count                     |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ */
+static int rtsp_send_rtcp_sr(rtsp_ctx_t *ctx) {
+    if (!ctx->rtcp_pcb || ctx->remote_rtcp_port == 0) {
+        LLOGD("RTCP未配置,跳过SR发送: rtcp_pcb=%p, remote_rtcp_port=%u", 
+              ctx->rtcp_pcb, ctx->remote_rtcp_port);
+        return RTSP_OK;
+    }
+    
+    LLOGD("准备发送RTCP SR: packets=%u, bytes=%u, remote_ip=%s, remote_rtcp_port=%u",
+          ctx->rtp_packets_sent, ctx->bytes_sent, 
+          ipaddr_ntoa(&ctx->remote_ip), ctx->remote_rtcp_port);
+    
+    uint8_t rtcp_packet[28];  // RTCP SR固定头部28字节(不含接收报告块)
+    uint32_t ntp_sec, ntp_frac;
+    
+    // 获取NTP时间戳
+    rtsp_get_ntp_timestamp(&ntp_sec, &ntp_frac);
+    
+    // 计算RTP时间戳
+    uint32_t rtp_ts = (ctx->video_timestamp * RTP_TIMESTAMP_BASE) / 1000;
+    
+    // 构建RTCP SR包
+    // V=2, P=0, RC=0 (无接收报告块)
+    rtcp_packet[0] = (2 << 6) | 0;
+    // PT=200 (SR)
+    rtcp_packet[1] = 200;
+    // Length = 6 (28字节 / 4 - 1)
+    rtcp_packet[2] = 0;
+    rtcp_packet[3] = 6;
+    
+    // SSRC (big-endian)
+    rtcp_packet[4] = (ctx->rtp_ssrc >> 24) & 0xFF;
+    rtcp_packet[5] = (ctx->rtp_ssrc >> 16) & 0xFF;
+    rtcp_packet[6] = (ctx->rtp_ssrc >> 8) & 0xFF;
+    rtcp_packet[7] = ctx->rtp_ssrc & 0xFF;
+    
+    // NTP timestamp - 高32位 (big-endian)
+    rtcp_packet[8] = (ntp_sec >> 24) & 0xFF;
+    rtcp_packet[9] = (ntp_sec >> 16) & 0xFF;
+    rtcp_packet[10] = (ntp_sec >> 8) & 0xFF;
+    rtcp_packet[11] = ntp_sec & 0xFF;
+    
+    // NTP timestamp - 低32位 (big-endian)
+    rtcp_packet[12] = (ntp_frac >> 24) & 0xFF;
+    rtcp_packet[13] = (ntp_frac >> 16) & 0xFF;
+    rtcp_packet[14] = (ntp_frac >> 8) & 0xFF;
+    rtcp_packet[15] = ntp_frac & 0xFF;
+    
+    // RTP timestamp (big-endian)
+    rtcp_packet[16] = (rtp_ts >> 24) & 0xFF;
+    rtcp_packet[17] = (rtp_ts >> 16) & 0xFF;
+    rtcp_packet[18] = (rtp_ts >> 8) & 0xFF;
+    rtcp_packet[19] = rtp_ts & 0xFF;
+    
+    // Sender's packet count (big-endian)
+    rtcp_packet[20] = (ctx->rtp_packets_sent >> 24) & 0xFF;
+    rtcp_packet[21] = (ctx->rtp_packets_sent >> 16) & 0xFF;
+    rtcp_packet[22] = (ctx->rtp_packets_sent >> 8) & 0xFF;
+    rtcp_packet[23] = ctx->rtp_packets_sent & 0xFF;
+    
+    // Sender's octet count (big-endian)
+    rtcp_packet[24] = (ctx->bytes_sent >> 24) & 0xFF;
+    rtcp_packet[25] = (ctx->bytes_sent >> 16) & 0xFF;
+    rtcp_packet[26] = (ctx->bytes_sent >> 8) & 0xFF;
+    rtcp_packet[27] = ctx->bytes_sent & 0xFF;
+    
+    // 发送RTCP SR包
+    struct pbuf *pb = pbuf_alloc(PBUF_TRANSPORT, 28, PBUF_RAM);
+    if (!pb) {
+        LLOGE("RTCP SR包分配失败");
+        return RTSP_ERR_NO_MEMORY;
+    }
+    
+    memcpy(pb->payload, rtcp_packet, 28);
+    
+    LLOGD("执行udp_sendto: rtcp_pcb=%p, remote_ip=%s, remote_rtcp_port=%u, len=28",
+          ctx->rtcp_pcb, ipaddr_ntoa(&ctx->remote_ip), ctx->remote_rtcp_port);
+    
+    err_t err = udp_sendto(ctx->rtcp_pcb, pb, &ctx->remote_ip, ctx->remote_rtcp_port);
+    
+    LLOGD("udp_sendto执行完毕: err=%d (%s)", err, 
+          err == ERR_OK ? "成功" : (err == ERR_MEM ? "内存不足" : 
+          (err == ERR_RTE ? "路由错误" : "其他错误")));
+    
+    pbuf_free(pb);
+    
+    if (err != ERR_OK) {
+        LLOGE("RTCP SR发送失败: err=%d, remote_ip=%s, remote_rtcp_port=%u", 
+              err, ipaddr_ntoa(&ctx->remote_ip), ctx->remote_rtcp_port);
+        return RTSP_ERR_NETWORK;
+    }
+    
+    LLOGD("RTCP SR已成功发送: SSRC=0x%08X, packets=%u, bytes=%u, NTP=%u.%u, RTP_TS=%u", 
+          ctx->rtp_ssrc, ctx->rtp_packets_sent, ctx->bytes_sent, ntp_sec, ntp_frac, rtp_ts);
+    
+    return RTSP_OK;
+}
+
+/**
+ * 连接到RTSP服务器
+ */
+int rtsp_connect(rtsp_ctx_t *ctx) {
+    if (!ctx || !ctx->host || ctx->port == 0) {
+        LLOGE("RTSP上下文无效或URL未设置");
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    if (ctx->state != RTSP_STATE_IDLE) {
+        LLOGE("RTSP已处于连接状态");
+        return RTSP_ERR_FAILED;
+    }
+    
+    // 创建TCP控制连接
+    ctx->control_pcb = tcp_new();
+    if (!ctx->control_pcb) {
+        LLOGE("TCP控制块创建失败");
+        return RTSP_ERR_NO_MEMORY;
+    }
+    
+    // 创建RTP UDP连接
+    ctx->rtp_pcb = udp_new();
+    if (!ctx->rtp_pcb) {
+        LLOGE("RTP UDP块创建失败");
+        tcp_abort(ctx->control_pcb);
+        ctx->control_pcb = NULL;
+        return RTSP_ERR_NO_MEMORY;
+    }
+    
+    // 创建RTCP UDP连接
+    ctx->rtcp_pcb = udp_new();
+    if (!ctx->rtcp_pcb) {
+        LLOGE("RTCP UDP块创建失败");
+        tcp_abort(ctx->control_pcb);
+        udp_remove(ctx->rtp_pcb);
+        ctx->control_pcb = NULL;
+        ctx->rtp_pcb = NULL;
+        return RTSP_ERR_NO_MEMORY;
+    }
+    
+    rtsp_set_state(ctx, RTSP_STATE_CONNECTING, 0);
+    
+    // 发起TCP连接
+    ip_addr_t remote_ip;
+    if (ipaddr_aton(ctx->host, &remote_ip) == 0) {
+        // TODO: DNS解析
+        LLOGE("IP地址转换失败: %s", ctx->host);
+        tcp_abort(ctx->control_pcb);
+        udp_remove(ctx->rtp_pcb);
+        udp_remove(ctx->rtcp_pcb);
+        ctx->control_pcb = NULL;
+        ctx->rtp_pcb = NULL;
+        ctx->rtcp_pcb = NULL;
+        rtsp_set_state(ctx, RTSP_STATE_ERROR, RTSP_ERR_CONNECT_FAILED);
+        return RTSP_ERR_CONNECT_FAILED;
+    }
+    
+    // 保存远端IP地址
+    ip_addr_copy(ctx->remote_ip, remote_ip);
+    LLOGD("解析服务器地址: %s -> %s", ctx->host, ipaddr_ntoa(&ctx->remote_ip));
+    
+    tcp_arg(ctx->control_pcb, (void *)ctx);
+    tcp_connect(ctx->control_pcb, &remote_ip, ctx->port, rtsp_tcp_connect_callback);
+    
+    LLOGD("RTSP连接请求已发送: %s:%u", ctx->host, ctx->port);
+    return RTSP_OK;
+}
+
+/**
+ * 断开RTSP连接
+ */
+int rtsp_disconnect(rtsp_ctx_t *ctx) {
+    if (!ctx) {
+        return RTSP_ERR_INVALID_PARAM;
+    }
+    
+    rtsp_set_state(ctx, RTSP_STATE_DISCONNECTING, 0);
+    
+    // 关闭TCP连接
+    if (ctx->control_pcb) {
+        tcp_close(ctx->control_pcb);
+        ctx->control_pcb = NULL;
+    }
+    
+    // 关闭UDP连接
+    if (ctx->rtp_pcb) {
+        udp_remove(ctx->rtp_pcb);
+        ctx->rtp_pcb = NULL;
+    }
+    if (ctx->rtcp_pcb) {
+        udp_remove(ctx->rtcp_pcb);
+        ctx->rtcp_pcb = NULL;
+    }
+    
+    rtsp_clear_frame_queue(ctx);
+    rtsp_set_state(ctx, RTSP_STATE_IDLE, 0);
+    
+    LLOGD("RTSP连接已断开");
+    return RTSP_OK;
+}
+
+/**
+ * 详细解析SPS数据
+ */
+static void rtsp_parse_sps_detail(const uint8_t *sps_data, uint32_t sps_len) {
+    if (!sps_data || sps_len < 4) {
+        LLOGE("SPS数据无效");
+        return;
+    }
+    
+    LLOGD("========== SPS详细信息 ==========");
+    
+    // NALU头 (1字节)
+    uint8_t nalu_header = sps_data[0];
+    uint8_t forbidden_zero = (nalu_header >> 7) & 0x01;
+    uint8_t nal_ref_idc = (nalu_header >> 5) & 0x03;
+    uint8_t nal_unit_type = nalu_header & 0x1F;
+    
+    LLOGD("NALU Header: 0x%02X", nalu_header);
+    LLOGD("  forbidden_zero_bit: %u", forbidden_zero);
+    LLOGD("  nal_ref_idc: %u", nal_ref_idc);
+    LLOGD("  nal_unit_type: %u (SPS)", nal_unit_type);
+    
+    if (sps_len < 4) {
+        LLOGD("SPS数据太短,无法解析详细参数");
+        return;
+    }
+    
+    // Profile & Level (3字节)
+    uint8_t profile_idc = sps_data[1];
+    uint8_t constraint_flags = sps_data[2];
+    uint8_t level_idc = sps_data[3];
+    
+    LLOGD("Profile IDC: %u (0x%02X)", profile_idc, profile_idc);
+    const char *profile_name = "Unknown";
+    switch (profile_idc) {
+        case 66: profile_name = "Baseline"; break;
+        case 77: profile_name = "Main"; break;
+        case 88: profile_name = "Extended"; break;
+        case 100: profile_name = "High"; break;
+        case 110: profile_name = "High 10"; break;
+        case 122: profile_name = "High 4:2:2"; break;
+        case 244: profile_name = "High 4:4:4"; break;
+    }
+    LLOGD("  Profile: %s", profile_name);
+    
+    LLOGD("Constraint Flags: 0x%02X", constraint_flags);
+    LLOGD("  constraint_set0_flag: %u", (constraint_flags >> 7) & 0x01);
+    LLOGD("  constraint_set1_flag: %u", (constraint_flags >> 6) & 0x01);
+    LLOGD("  constraint_set2_flag: %u", (constraint_flags >> 5) & 0x01);
+    LLOGD("  constraint_set3_flag: %u", (constraint_flags >> 4) & 0x01);
+    LLOGD("  constraint_set4_flag: %u", (constraint_flags >> 3) & 0x01);
+    LLOGD("  constraint_set5_flag: %u", (constraint_flags >> 2) & 0x01);
+    
+    LLOGD("Level IDC: %u (Level %.1f)", level_idc, level_idc / 10.0);
+    
+    // 打印原始数据 (前16字节或全部)
+    uint32_t print_len = sps_len > 16 ? 16 : sps_len;
+    char hex_str[128];
+    int pos = 0;
+    for (uint32_t i = 0; i < print_len && pos < 120; i++) {
+        pos += snprintf(hex_str + pos, sizeof(hex_str) - pos, "%02X ", sps_data[i]);
+    }
+    if (sps_len > 16) {
+        snprintf(hex_str + pos, sizeof(hex_str) - pos, "...");
+    }
+    LLOGD("SPS Raw Data (%u bytes): %s", sps_len, hex_str);
+    LLOGD("==================================\n");
+}
+
+/**
+ * 详细解析PPS数据
+ */
+static void rtsp_parse_pps_detail(const uint8_t *pps_data, uint32_t pps_len) {
+    if (!pps_data || pps_len < 1) {
+        LLOGE("PPS数据无效");
+        return;
+    }
+    
+    LLOGD("========== PPS详细信息 ==========");
+    
+    // NALU头 (1字节)
+    uint8_t nalu_header = pps_data[0];
+    uint8_t forbidden_zero = (nalu_header >> 7) & 0x01;
+    uint8_t nal_ref_idc = (nalu_header >> 5) & 0x03;
+    uint8_t nal_unit_type = nalu_header & 0x1F;
+    
+    LLOGD("NALU Header: 0x%02X", nalu_header);
+    LLOGD("  forbidden_zero_bit: %u", forbidden_zero);
+    LLOGD("  nal_ref_idc: %u", nal_ref_idc);
+    LLOGD("  nal_unit_type: %u (PPS)", nal_unit_type);
+    
+    // 打印原始数据 (前16字节或全部)
+    uint32_t print_len = pps_len > 16 ? 16 : pps_len;
+    char hex_str[128];
+    int pos = 0;
+    for (uint32_t i = 0; i < print_len && pos < 120; i++) {
+        pos += snprintf(hex_str + pos, sizeof(hex_str) - pos, "%02X ", pps_data[i]);
+    }
+    if (pps_len > 16) {
+        snprintf(hex_str + pos, sizeof(hex_str) - pos, "...");
+    }
+    LLOGD("PPS Raw Data (%u bytes): %s", pps_len, hex_str);
+    LLOGD("==================================\n");
+}

+ 5 - 0
luat/include/luat_libs.h

@@ -21,6 +21,8 @@ LUAMOD_API int luaopen_adc( lua_State *L );
 LUAMOD_API int luaopen_pwm( lua_State *L );
 /** uart库*/
 LUAMOD_API int luaopen_uart( lua_State *L );
+/** usb流库*/
+LUAMOD_API int luaopen_usb(lua_State *L);
 /** pm库*/
 LUAMOD_API int luaopen_pm( lua_State *L );
 /** fs库*/
@@ -233,4 +235,7 @@ LUAMOD_API int luaopen_airtalk( lua_State *L );
 LUAMOD_API int luaopen_misc(lua_State *L);
 /** rtmp推流库*/
 LUAMOD_API int luaopen_rtmp(lua_State *L);
+/** rtsp推流库*/
+LUAMOD_API int luaopen_rtsp(lua_State *L);
+
 #endif

+ 2 - 0
luat/include/luat_mem.h

@@ -22,5 +22,7 @@ void luat_meminfo_opt_sys(LUAT_HEAP_TYPE_E type,size_t* total, size_t* used, siz
 #define LUAT_MEM_REALLOC luat_heap_realloc
 #define LUAT_MEM_CALLOC luat_heap_calloc
 
+// 查询并输出内存使用情况到日志
+void luat_meminfo_query(LUAT_HEAP_TYPE_E type,size_t* total, size_t* used, size_t* max_used, int log_out);
 
 #endif

+ 2 - 0
luat/include/luat_uart.h

@@ -45,6 +45,8 @@
 #define LUAT_1_5_STOP_BITS                   0xf1   /**< 1.5 */
 
 #define LUAT_VUART_ID_0						0x20    
+#define LUAT_VUART_ID_1						0x21
+#define LUAT_VUART_ID_2						0x22
 
 #define LUAT_UART_RX_ERROR_DROP_DATA		(0xD6)
 #define LUAT_UART_DEBUG_ENABLE				(0x3E)

+ 43 - 0
luat/include/luat_usb.h

@@ -0,0 +1,43 @@
+#ifndef LUAT_USB_H
+#define LUAT_USB_H
+
+#include "luat_base.h"
+enum
+{
+	LUAT_USB_MODE_DEVICE,
+	LUAT_USB_MODE_HOST,
+	LUAT_USB_MODE_OTG,
+	LUAT_USB_CLASS_CDC_ACM = 0,
+	LUAT_USB_CLASS_AUDIO,
+	LUAT_USB_CLASS_CAMERA,
+	LUAT_USB_CLASS_HID,
+	LUAT_USB_CLASS_MSC,
+	LUAT_USB_CLASS_WINUSB,
+	LUAT_USB_EVENT_NEW_RX	= 0,
+	LUAT_USB_EVENT_TX_DONE,
+	LUAT_USB_EVENT_CONNECT,
+	LUAT_USB_EVENT_DISCONNECT,
+	LUAT_USB_EVENT_SUSPEND,
+	LUAT_USB_EVENT_RESUME,
+};
+
+typedef void (*usb_callback_t)(int id, int event, uint8_t *data, uint32_t len);
+
+int luat_usb_set_vid(int id, uint16_t vid);
+int luat_usb_get_vid(int id, uint16_t *vid);
+
+int luat_usb_set_pid(int id, uint16_t pid);
+int luat_usb_get_pid(int id, uint16_t *pid);
+
+int luat_usb_set_mode(int id, uint8_t mode);
+
+int luat_usb_add_class(int id, uint8_t class, uint8_t num);
+int luat_usb_get_free_ep_num(int id);
+int luat_usb_clear_class(int id);
+
+int luat_usb_set_callback(int id, usb_callback_t callback);
+
+int luat_usb_tx(int id, uint8_t class, const void *data, uint32_t len);
+int luat_usb_hid_tx(int id, const char *string, uint32_t len);
+int luat_usb_rx(int id, uint8_t class, void *data, uint32_t len);
+#endif

+ 12 - 0
luat/modules/luat_lib_fota.c

@@ -221,6 +221,18 @@ static int l_fota_file(lua_State* L)
     luat_heap_free(buff);
     luat_fs_fclose(fd);
 
+    if (result == 0) {
+        LLOGI("fota file write done, call fota.done()");
+        result = luat_fota_done();
+        if (result == 0) {
+            LLOGI("fota done success, call fota.end()");
+            result = luat_fota_end(1);
+        }
+        else {
+            LLOGE("fota done fail %d", result);
+        }
+    }
+
     if (result > 0)
     {
     	lua_pushboolean(L, 1);

+ 12 - 0
luat/modules/luat_lib_rtos.c

@@ -299,13 +299,25 @@ static int l_rtos_meminfo(lua_State *L) {
     size_t max_used = 0;
     const char * str = luaL_optlstring(L, 1, "lua", &len);
     if (strcmp("sys", str) == 0) {
+        #ifdef LUAT_USE_MEM_LOGOUT
+        luat_meminfo_query(LUAT_HEAP_SRAM, &total, &used, &max_used, 1);
+        #else
         luat_meminfo_opt_sys(LUAT_HEAP_SRAM, &total, &used, &max_used);
+        #endif
     }
     else if(strcmp("psram", str) == 0){
+        #ifdef LUAT_USE_MEM_LOGOUT
+        luat_meminfo_query(LUAT_HEAP_PSRAM, &total, &used, &max_used, 1);
+        #else
         luat_meminfo_opt_sys(LUAT_HEAP_PSRAM, &total, &used, &max_used);
+        #endif
     }
     else {
+        #ifdef LUAT_USE_MEM_LOGOUT
+        luat_meminfo_query(0, &total, &used, &max_used, 1);
+        #else
         luat_meminfo_luavm(&total, &used, &max_used);
+        #endif
     }
     
     lua_pushinteger(L, total);

+ 5 - 1
luat/modules/luat_lib_uart.c

@@ -37,7 +37,7 @@
 #define MAX_DEVICE_COUNT 9
 #endif
 
-#define MAX_USB_DEVICE_COUNT 1
+#define MAX_USB_DEVICE_COUNT 3
 
 typedef struct luat_uart_cb {
     int received;//回调函数
@@ -1425,6 +1425,10 @@ static const rotable_Reg_t reg_uart[] =
 
     //@const VUART_0 number 虚拟串口0
 	{ "VUART_0",       ROREG_INT(LUAT_VUART_ID_0)},
+    //@const VUART_1 number 虚拟串口1
+	{ "VUART_1",       ROREG_INT(LUAT_VUART_ID_1)},
+    //@const VUART_2 number 虚拟串口2
+	{ "VUART_2",       ROREG_INT(LUAT_VUART_ID_2)},
     //@const ERROR_DROP number 遇到错误时抛弃缓存的数据
 	{ "ERROR_DROP",       ROREG_INT(LUAT_UART_RX_ERROR_DROP_DATA)},
     //@const DEBUG number 开启调试功能

+ 346 - 0
luat/modules/luat_lib_usb.c

@@ -0,0 +1,346 @@
+/*
+@module  usb
+@summary usb操作库
+@version 1.0
+@date    2025.12.15
+@demo usb
+@tag LUAT_USE_USB
+@usage
+--[[
+--简单举例
+pm.power(pm.USB, false)		--确保USB外设是掉电状态
+--usb.vid(0, 0x1234)		--配置VID,不是必须的
+--usb.pvid(0, 0x5678)		--配置PID,不是必须的
+usb.clear_all_class(0)				--清除掉之前配置的设备类
+usb.mode(0, usb.DEVICE)		--usb设置成从机模式
+usb.add_class(0, usb.CDC_ACM, 1)	--使用1个CDC-ACM虚拟串口功能
+usb.add_class(0, usb.WINUSB, 1)		--使用1个WINUSB功能
+pm.power(pm.USB, true)		--USB上电初始化开始工作
+--说明
+目前设备类只有usb.HID和usb.WINUSB可以通过usb操作库api和对端通讯,usb.CDC-ACM虚拟串口直接使用uart api
+]]
+*/
+#include "luat_base.h"
+#include "luat_sys.h"
+#include "luat_msgbus.h"
+#include "luat_mem.h"
+#include "luat_usb.h"
+#include "luat_zbuff.h"
+#define LUAT_LOG_TAG "usb"
+#include "luat_log.h"
+#include "rotable2.h"
+
+#define MAX_USB_DEVICE_COUNT 2
+static int l_usb_cb[MAX_USB_DEVICE_COUNT];
+
+int l_usb_handler(lua_State *L, void* ptr) {
+    (void)ptr;
+    rtos_msg_t* msg = (rtos_msg_t*)lua_topointer(L, -1);
+    lua_pop(L, 1);
+    int usb_id = msg->arg1 & 0x000000ff;
+    int class_id = (msg->arg1 & 0x0000ff00) >> 8;
+    if (l_usb_cb[usb_id] && usb_id < MAX_USB_DEVICE_COUNT)
+    {
+        lua_geti(L, LUA_REGISTRYINDEX, l_usb_cb[usb_id]);
+        lua_pushinteger(L, usb_id);
+        lua_pushinteger(L, msg->arg2);
+        if (class_id != 0x000000ff)
+        {
+        	lua_pushinteger(L, class_id);
+        }
+        else
+        {
+        	lua_pushnil(L);
+        }
+        lua_call(L, 3, 0);
+    }
+    // 给rtos.recv方法返回个空数据
+    lua_pushinteger(L, 0);
+    return 1;
+}
+
+
+/*
+USB发送数据,目前仅限于HID和WINUSB设备,CDC-ACM虚拟串口直接使用串口API操作
+@api usb.tx(id, data, class)
+@int 设备id,默认为0
+@zbuff or string 需要发送的数据
+@int 设备类
+@return bool 成功返回true,否则返回false,总线id填错,所填设备类不支持直接发送数据等情况下返回错误
+@usage
+-- HID上传数据
+usb.tx(0, "1234", usb.HID) -- usb hid上传0x31 0x32 0x33 0x34  + N个0
+*/
+static int l_usb_tx(lua_State* L) {
+	int result;
+    uint8_t class = luaL_optinteger(L, 3, LUAT_USB_CLASS_WINUSB);
+    int usb_id = luaL_optinteger(L, 1, 0);
+    const char *buf;
+    luat_zbuff_t *buff = NULL;
+    if(lua_isuserdata(L, 2)) {
+        buff = ((luat_zbuff_t *)luaL_checkudata(L, 2, LUAT_ZBUFF_TYPE));
+        result = luat_usb_tx(usb_id, class, buff->addr, buff->used);
+    } else {
+    	size_t len;
+    	buf = luaL_checklstring(L, 2, &len);
+    	if (LUAT_USB_CLASS_HID == class)
+    	{
+    		result = luat_usb_hid_tx(usb_id, buf, len);
+    	}
+    	else
+    	{
+    		result = luat_usb_tx(usb_id, class, buf, len);
+    	}
+        lua_pushboolean(L, !result);
+    }
+    return 1;
+}
+
+/*
+buff形式读接收到的数据,一次读出全部数据存入buff中,如果buff空间不够会自动扩展
+@api usb.rx(id, buff, class)
+@int 设备id,默认为0
+@zbuff zbuff对象
+@int 设备类
+@return int 返回读到的长度,并把zbuff指针后移
+@usage
+usb.rx(0, buff, usb.HID)
+*/
+static int l_usb_rx(lua_State *L)
+{
+    uint8_t class = luaL_optinteger(L, 3, LUAT_USB_CLASS_WINUSB);
+    int usb_id = luaL_optinteger(L, 1, 0);
+
+    if(lua_isuserdata(L, 2)){//zbuff对象特殊处理
+    	luat_zbuff_t *buff = ((luat_zbuff_t *)luaL_checkudata(L, 2, LUAT_ZBUFF_TYPE));
+        int result = luat_usb_rx(usb_id, class, NULL, 0);	//读出当前缓存的长度
+        if (result > (buff->len - buff->used))
+        {
+        	__zbuff_resize(buff, buff->len + result);
+        }
+        result = luat_usb_rx(usb_id, class, buff->addr + buff->used, result);
+        lua_pushinteger(L, result);
+        buff->used += result;
+        return 1;
+    }
+    else
+    {
+        lua_pushinteger(L, 0);
+        return 1;
+    }
+    return 1;
+}
+
+
+/*
+设置USB工作模式,必须在USB外设掉电不工作时进行设置
+@api usb.mode(id, mode)
+@int usb总线id,默认0,如果芯片只有1条USB线,填0
+@int 工作模式,只有3种,usb.HOST主机模式,usb.DEVICE从机模式,usb.OTG协商模式,默认是从机模式
+@return bool 成功返回true,否则返回false,总线id填错,所选模式不支持时,USB外设正在工作等情况下返回失败
+@usage
+pm.power(pm.USB, false)
+usb.mode(0, usb.DEVICE)
+pm.power(pm.USB, true)
+*/
+static int l_usb_mode(lua_State* L) {
+	int result = luat_usb_set_mode(luaL_optinteger(L, 1, 0), luaL_optinteger(L, 2, LUAT_USB_MODE_DEVICE));
+	lua_pushboolean(L, !result);
+    return 1;
+}
+
+/*
+注册USB事件回调
+@api usb.on(id, func)
+@int usb总线id,默认0,如果芯片只有1条USB线,填0
+@function 回调方法
+@return nil 无返回值
+@usage
+usb.on(0, function(id, class, event)
+    log.info("usb", id, class, event)
+end)
+--回调参数有3个
+1、usb总线id
+2、event,见usb.EV_XXX
+3、如果event是usb.EV_RX或usb.EV_TX,则第三个参数表示哪个设备类,目前只有usb.HID和usb.WINUSB
+*/
+static int l_usb_on(lua_State *L) {
+    int usb_id = luaL_optinteger(L, 1, 0);
+    if (usb_id >= MAX_USB_DEVICE_COUNT) return 0;
+    if (l_usb_cb[usb_id])
+    {
+    	luaL_unref(L, LUA_REGISTRYINDEX, l_usb_cb[usb_id]);
+    }
+	if (lua_isfunction(L, 2)) {
+		lua_pushvalue(L, 2);
+		l_usb_cb[usb_id] = luaL_ref(L, LUA_REGISTRYINDEX);
+	}
+    return 0;
+}
+
+
+
+/*
+设置/获取USB的VID,必须在USB外设掉电不工作时进行设置,获取没有限制
+@api usb.vid(id, vid)
+@int usb总线id,默认0,如果芯片只有1条USB线,填0
+@int 想要设置的VID值,留空则不做设置
+@return bool 成功或者获取VID时返回true,否则返回false,总线id填错,芯片不支持设置,USB外设正在工作等情况下返回失败
+@return int 当前VID值
+@usage
+pm.power(pm.USB, false)
+usb.vid(0, 0x1234)
+pm.power(pm.USB, true)
+*/
+static int l_usb_vid(lua_State* L) {
+	int result1 = 0;
+	int result2 = -1;
+	int bus_id = luaL_optinteger(L, 1, 0);
+	uint16_t vid;
+	if (lua_isinteger(L, 2))
+	{
+		result1 = luat_usb_set_vid(bus_id, lua_tointeger(L, 2));
+	}
+	result2 = luat_usb_get_vid(bus_id, &vid);
+
+	lua_pushboolean(L, !(result1 || result2));
+	lua_pushinteger(L, vid);
+    return 2;
+}
+
+/*
+设置/获取USB的PID,必须在USB外设掉电不工作时进行设置,获取没有限制
+@api usb.pid(id, pid)
+@int usb总线id,默认0,如果芯片只有1条USB线,填0
+@int 想要设置的PID值,留空则不做设置
+@return bool 成功或者获取VID时返回true,否则返回false,总线id填错,芯片不支持设置,USB外设正在工作等情况下返回失败
+@return int 当前PID值
+@usage
+pm.power(pm.USB, false)
+usb.pid(0, 0x1234)
+pm.power(pm.USB, true)
+*/
+static int l_usb_pid(lua_State* L) {
+	int result1 = 0;
+	int result2 = -1;
+	int bus_id = luaL_optinteger(L, 1, 0);
+	uint16_t pid;
+	if (lua_isinteger(L, 2))
+	{
+		result1 = luat_usb_set_pid(bus_id, lua_tointeger(L, 2));
+	}
+	result2 = luat_usb_get_pid(bus_id, &pid);
+
+	lua_pushboolean(L, !(result1 || result2));
+	lua_pushinteger(L, pid);
+    return 2;
+}
+
+/*
+设置USB支持的设备类和数量,必须在USB外设掉电不工作时进行设置
+@api usb.add_class(id, class, num)
+@int usb总线id,默认0,如果芯片只有1条USB线,填0
+@int 设备类,从机模式支持usb.CDC_ACM,usb.HID,usb.MSC,usb.WINUSB,主机模式支持usb.CAMERA
+@int 数量,目前只有从机的usb.CDC_ACM允许至多3个,其他只允许1个,超过时会强制改成所允许的最大值
+@return bool 成功返回true,否则返回false,总线id填错,所选设备类不支持时,端点数量超过芯片允许的最大值,USB外设正在工作等情况下返回失败
+@usage
+pm.power(pm.USB, false)
+usb.add_class(0, usb.CDC_ACM, 3)	--使用3个CDC-ACM虚拟串口功能
+usb.add_class(0, usb.WINUSB, 1)		--使用1个WINUSB功能
+pm.power(pm.USB, true)
+*/
+static int l_usb_add_class(lua_State* L) {
+	int result = luat_usb_add_class(luaL_optinteger(L, 1, 0), luaL_optinteger(L, 2, LUAT_USB_CLASS_CDC_ACM), luaL_optinteger(L, 3, 1));
+	lua_pushboolean(L, !result);
+    return 1;
+}
+
+/*
+清除掉当前配置的设备类,必须在USB外设掉电不工作时进行设置
+@api usb.clear_all_class(id)
+@int usb总线id,默认0,如果芯片只有1条USB线,填0
+@return bool 成功返回true,否则返回false,总线id填错,USB外设正在工作等情况下返回失败
+@usage
+pm.power(pm.USB, false)
+usb.clear_all_class(0)				--清除掉之前配置的设备类
+usb.add_class(0, usb.CDC_ACM, 3)	--使用3个CDC-ACM虚拟串口功能
+usb.add_class(0, usb.WINUSB, 1)		--使用1个WINUSB功能
+pm.power(pm.USB, true)
+*/
+static int l_usb_clear_all_class(lua_State* L) {
+	int result = luat_usb_clear_class(luaL_optinteger(L, 1, 0));
+	lua_pushboolean(L, !result);
+    return 1;
+}
+
+/*
+返回当前剩余的端点数
+@api usb.get_free_ep_num(id)
+@int usb总线id,默认0,如果芯片只有1条USB线,填0
+@return int 剩余的端点数,总线id填错时直接返回0
+@usage
+log.info(usb.get_free_ep_num(0))
+*/
+static int l_usb_get_free_ep_num(lua_State* L) {
+	int result = luat_usb_get_free_ep_num(luaL_optinteger(L, 1, 0));
+	if (result < 0)
+	{
+		lua_pushinteger(L, 0);
+	}
+	else
+	{
+		lua_pushinteger(L, result);
+	}
+    return 1;
+}
+
+static const rotable_Reg_t reg_usb[] =
+{
+	{ "tx",					ROREG_FUNC(l_usb_tx)},
+	{ "rx",					ROREG_FUNC(l_usb_rx)},
+	{ "mode",				ROREG_FUNC(l_usb_mode)},
+	{ "add_class",      	ROREG_FUNC(l_usb_add_class)},
+	{ "on",         		ROREG_FUNC(l_usb_on)},
+	{ "vid",         		ROREG_FUNC(l_usb_vid)},
+	{ "pid",         		ROREG_FUNC(l_usb_pid)},
+	{ "clear_all_class" ,   ROREG_FUNC(l_usb_clear_all_class)},
+	{ "get_free_ep_num" ,   ROREG_FUNC(l_usb_get_free_ep_num)},
+	//@const HOST number USB主机模式
+    { "HOST",        		ROREG_INT(LUAT_USB_MODE_HOST)},
+	//@const DEVICE number USB从机模式
+    { "DEVICE",       		ROREG_INT(LUAT_USB_MODE_DEVICE)},
+	//@const OTG number USB otg模式
+    { "OTG",       			ROREG_INT(LUAT_USB_MODE_OTG)},
+	//@const CDC_ACM number cdc_acm 虚拟串口类
+    { "CDC_ACM",        	ROREG_INT(LUAT_USB_CLASS_CDC_ACM)},
+	//@const AUDIO number audio音频类
+    { "AUDIO",       		ROREG_INT(LUAT_USB_CLASS_AUDIO)},
+	//@const CAMERA number 摄像头类
+    { "CAMERA",        		ROREG_INT(LUAT_USB_CLASS_CAMERA)},
+	//@const HID number HID设备类,只支持键盘和自定义
+    { "HID",       			ROREG_INT(LUAT_USB_CLASS_HID)},
+	//@const MSC number 大容量存储类,也就是U盘,TF卡
+    { "MSC",       			ROREG_INT(LUAT_USB_CLASS_MSC)},
+	//@const WINUSB number WINUSB类,透传数据
+    { "WINUSB",       		ROREG_INT(LUAT_USB_CLASS_WINUSB)},
+
+	//@const EV_RX number  有新的数据到来
+    { "EV_RX",       		ROREG_INT(LUAT_USB_EVENT_NEW_RX)},
+	//@const EV_TX number 所有数据都已发送
+    { "EV_TX",        		ROREG_INT(LUAT_USB_EVENT_TX_DONE)},
+	//@const EV_CONNECT number usb从机已经连接上并且枚举成功
+    { "EV_CONNECT",       	ROREG_INT(LUAT_USB_EVENT_CONNECT)},
+	//@const EV_DISCONNECT number usb从机断开
+    { "EV_DISCONNECT",      ROREG_INT(LUAT_USB_EVENT_DISCONNECT)},
+	//@const EV_SUSPEND number usb从机挂起
+    { "EV_SUSPEND",       	ROREG_INT(LUAT_USB_EVENT_SUSPEND)},
+	//@const EV_RESUME number usb从机恢复
+    { "EV_RESUME",       	ROREG_INT(LUAT_USB_EVENT_RESUME)},
+    { NULL,         ROREG_INT(0) }
+};
+
+LUAMOD_API int luaopen_usb( lua_State *L )
+{
+    luat_newlib2(L, reg_usb);
+    return 1;
+}

+ 65 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/main.lua

@@ -0,0 +1,65 @@
+--[[
+@module  main
+@summary OneWire综合演示项目主文件(单传感器 + 多传感器)
+@version 001.000.000
+@date    2025.11.25
+@author  王棚嶙
+@usage
+本演示项目整合单DS18B20和多DS18B20传感器功能:
+1. 单传感器模式:GPIO2默认OneWire功能、硬件通道0模式、CRC校验、3秒间隔连续监测
+2. 多传感器模式:引脚54/23切换、PWR_KEY按键控制、电源管理、2秒间隔双路监测
+3. 完整的OneWire API接口演示、错误处理、设备检测、温度报警
+]]
+
+-- 项目信息
+PROJECT = "onewire_demo"
+VERSION = "001.000.000"
+
+
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+--添加硬狗防止程序卡死
+if wdt then
+    wdt.init(9000)--初始化watchdog设置为9s
+    sys.timerLoopStart(wdt.feed, 3000)--3s喂一次狗
+end
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 在加载以下两个功能的时候建议分别打开,避免同时初始化OneWire总线,导致资源冲突
+-- 单设备模式:使用GPIO2默认OneWire功能
+-- 双设备模式:GPIO2默认 + 引脚54复用
+
+-- 加载单传感器应用模块
+-- require("onewire_single_app")
+
+-- 加载多传感器应用模块
+require("onewire_multi_app")
+
+
+-- 启动系统主循环
+sys.run()

+ 0 - 9
module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/onewire_multi_18b20_swich_read/main.lua

@@ -1,9 +0,0 @@
-PROJECT = "DS18B20_GPIO_SWITCH_READ_temperature"
-VERSION = "1.0.0"
-sys = require("sys")
-log.style(1)
-
-
-require"switch_read"
-
-sys.run()

+ 0 - 125
module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/onewire_multi_18b20_swich_read/switch_read.lua

@@ -1,125 +0,0 @@
-
-pm.ioVol(pm.IOVOL_ALL_GPIO, 3300) -- 所有GPIO高电平输出3.3V(方便使用VDD_EXT给18B20供电)
-gpio.setup(2, 1) --GPIO2控制780EPM开发板V1.3版本camera电源打开和关闭
-gpio.setup(31, 1) -- GPIO31控制780EPM开发板V1.4版本camera电源打开和关闭
-
-local onewire_pin = 54 --18B20接的pin 54脚
-
-
-pins.setup(onewire_pin, "ONEWIRE") -- PAD54脚既是GPIO3也是cam_mclk 
-
---读取当前pin脚上的18B20温度
-local function read_ds18b20(id)
-log.info("读取温度",id)
-    local tbuff = zbuff.create(10)
-    local succ, crc8c, range, t
-    local rbuff = zbuff.create(9)
-    tbuff:write(0x55)
-    tbuff:copy(nil, id)
-    tbuff:write(0xb8)
-  
-    tbuff[tbuff:used() - 1] = 0x44
-    succ = onewire.tx(0, tbuff, false, true, true)
-    if not succ then
-        return
-    end
-
-    succ = onewire.reset(0, true)
-    if not succ then
-        return
-    end
-    if onewire.bit(0) > 0 then
-        log.info("温度转换完成")
-    end
-    tbuff[tbuff:used() - 1] = 0xbe
-    succ = onewire.tx(0, tbuff, false, true, true)
-    if not succ then
-        return
-    end
-    succ, rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
-    crc8c = crypto.crc8(rbuff:toStr(0, 8), 0x31, 0, true)
-    if crc8c == rbuff[8] then
-        range = (rbuff[4] >> 5) & 0x03
-        -- rbuff[0] = 0xF8
-        -- rbuff[1] = 0xFF
-        t = rbuff:query(0, 2, false, true)
-        t = t * (5000 >> range)
-        t = t / 10000
-        log.info("当前温度", t,"原始值为",mcu.x32(rbuff[8]))
-    else
-        log.info("RAM DATA CRC校验不对", mcu.x32(crc8c), mcu.x32(rbuff[8]))
-        return
-    end
-
-end
-
---初始化当前pin脚上的18B20,并读取18B20的唯一识别ID
-local function test_ds18b20()
-    local succ, rx_data
-    local id = zbuff.create(8)
-    local crc8c
-    onewire.init(0) -- 初始化单总线
-    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
-    id:set() -- 清空id
-    succ, rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
-    if succ then
-        if id[0] == 0x28 then
-            crc8c = crypto.crc8(id:query(0, 7), 0x31, 0, true)
-            if crc8c == id[7] then
-                log.info("探测到DS18B20", "18B20对应唯一ID为", id:query(0, 7):toHex())
-                read_ds18b20(id)
-                -- log.info("DS18B20离线,重新探测")
-            else
-                log.info("ROM ID CRC校验不对", mcu.x32(crc8c), mcu.x32(id[7]))
-            end
-        else
-            log.info("ROM ID不正确", mcu.x32(id[0]))
-        end
-    else
-        log.info("没有检测到DS18B20")
-    end
-end
-
-local switchover_pin = gpio.PWR_KEY --选择powerkey作为切换18B20读数的信号脚
-
-gpio.debounce(switchover_pin, 100) --设置防抖
-
-
---设置powerkey按下和抬起的功能(18B20温度的取值再54pin和56pin之间切换)
---如果客户需要其他pin(22/54/56/78)则改动switchover_pin为用户需要的即可
---注意,由于powerkey不能做单边沿触发,所以一次按下和抬起的动作,会触发两次切换pin
---看效果的话,可以先一直按住powerkey几秒,再松开
-gpio.setup(switchover_pin, function()
-    log.info("Touch_pin", switchover_pin, "被触发")
-    log.info("当前单总线pad", onewire_pin)
-    if onewire_pin == 54 then
-        log.info("给PAD" .. onewire_pin .. "配置到GPIO3上去", pins.setup(onewire_pin, "GPIO3"))
-        log.info("给GPIO3设置为高电平输出模式",gpio.setup(3,1))--一定要执行gpio.setup,至于是哪种模式,用户根据自身需求
-        onewire_pin = 56
-    else
-        log.info("给PAD" .. onewire_pin .. "配置到GPIO7上去", pins.setup(onewire_pin, "GPIO7"))
-        log.info("给GPIO7设置为高电平输出模式",gpio.setup(7,1))
-
-        onewire_pin = 54
-    end
-    log.info("设置后单总线pad", onewire_pin)
-
-    onewire.deinit(0) -- 切换的时候一定要先关闭单总线,在读取的时候重新初始化
-
-    log.info("给" .. onewire_pin .. "配置到onewire上去", pins.setup(onewire_pin, "ONEWIRE"))
-    sys.publish("powerkey被按下")
-end, gpio.PULLUP, gpio.RISING)
-
-sys.taskInit(function()
-    while 1 do
-        -- sys.waitUntil("powerkey被按下")
-        log.info("1S后读取"..onewire_pin.."脚上的18B20")
-        sys.wait(1000)
-        log.info("开始读取18B20")
-        test_ds18b20()
-    end
-end)
-
-
-
-

+ 367 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/onewire_multi_app.lua

@@ -0,0 +1,367 @@
+--[[
+@module  onewire_multi_app
+@summary OneWire多DS18B20温度传感器应用演示模块(54和23切换版本)
+@version 1.0.0
+@date    2025.11.25
+@author  王棚嶙
+@usage
+本模块演示多DS18B20温度传感器的完整功能:
+1. 双传感器切换控制(引脚54和23)
+2. 电源管理(GPIO控制)
+3. 按键切换传感器
+4. 双路温度同时监测
+5. 使用引脚复用功能(pins.setup)
+]]
+
+log.info("onewire_multi_app", "多传感器模块版本: 1.0.0")
+
+-- 设置所有GPIO引脚电压为3.3V,确保DS18B20传感器正常供电
+pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
+
+-- 和GPIO31控制传感器电源使能,确保DS18B20供电正常
+gpio.setup(31, 1)
+
+-- 硬件配置(双设备模式:支持引脚54和23切换)
+local onewire_pin = 54
+local switchover_pin = gpio.PWR_KEY
+
+-- DS18B20命令定义
+local CMD_CONVERT_T = 0x44
+local CMD_READ_SCRATCHPAD = 0xBE
+local CMD_READ_ROM = 0x33
+
+-- 全局状态变量
+local pwr_key_pressed = false
+
+-- PWR_KEY按键中断处理函数
+-- 功能:处理引脚切换按键事件,设置标志位供主循环查询
+local function handle_pwr_key_interrupt()
+    pwr_key_pressed = true
+    log.info("onewire_multi_app", "切换按键被按下")
+end
+
+-- 初始化硬件配置
+local function init_hardware()
+    log.info("onewire_multi_app", "初始化硬件配置...")
+    
+    -- 配置PWR_KEY按键,使用上升沿触发并添加防抖
+    gpio.debounce(switchover_pin, 100)
+    gpio.setup(switchover_pin, handle_pwr_key_interrupt, gpio.PULLUP, gpio.RISING)
+    
+    -- 初始配置当前引脚为ONEWIRE功能
+    pins.setup(onewire_pin, "ONEWIRE")
+    
+    log.info("onewire_multi_app", "硬件初始化完成")
+    log.info("onewire_multi_app", "初始引脚: 引脚" .. onewire_pin .. " (ONEWIRE功能)")
+    log.info("onewire_multi_app", "切换按键: PWR_KEY")
+    log.info("onewire_multi_app", "支持引脚: 54 和 23 循环切换")
+    log.info("onewire_multi_app", "电源控制: GPIO31/GPIO2 (已设置为高电平)")
+    
+    return true
+end
+
+
+-- 时序要求:DS18B20上电后需要稳定时间,100ms延时确保电源稳定
+-- 技术背景:DS18B20在电源切换后需要tREC(恢复时间)完成内部初始化
+-- 实际测试:无延时可能导致设备检测失败或温度读取异常
+-- 建议值:最小50ms,推荐100ms以确保可靠性
+local function power_stabilization_delay()
+    log.info("onewire_multi_app", "电源稳定延时(确保DS18B20内部电路就绪)")
+    sys.wait(100)  -- DS18B20 tREC恢复时间,最小50ms,推荐100ms
+end
+
+-- 单总线分时使用引脚切换(同一条总线,分时复用)
+-- 核心逻辑:使用GPIO54和GPIO23两个引脚连接同一条OneWire总线,实现分时复用
+-- 应用场景:当需要在同一总线上分时访问不同设备时使用
+-- 技术原理:通过切换总线连接引脚,实现同一条物理总线的分时使用
+-- 切换效果:
+-- - GPIO54:当前时间段连接设备A(ROM ID: 28-9F-C4-93-00-00-00-14)
+-- - GPIO23:切换到时间段连接设备B(ROM ID: 28-59-F2-53-00-00-00-14)
+-- 注意:这不是多总线并行,而是单总线的分时复用策略
+local function switch_onewire_pin()
+    log.info("onewire_multi_app", "切换OneWire引脚...")
+    
+    -- 关闭当前OneWire总线
+    onewire.deinit(0)
+    
+    
+    -- 分时复用切换逻辑
+    -- 技术原理:将当前不使用的引脚配置为GPIO功能并输出高电平
+    -- 目的:确保非活动设备处于高阻态,避免干扰当前连接的设备
+    -- 电气特性:GPIO设置为开漏输出模式,高电平由上拉电阻提供
+    if onewire_pin == 54 then
+        -- 从54切换到23
+        -- 将PIN54配置为GPIO3功能,不再作为OneWire使用
+        log.info("onewire_multi_app", "将PIN54配置为GPIO3", pins.setup(54, "GPIO3"))
+        -- 设置GPIO3为高电平输出(开漏模式,高电平由上拉电阻提供)
+        log.info("onewire_multi_app", "将GPIO3设置为高电平输出(OneWire总线空闲状态)", gpio.setup(3, 1))
+        onewire_pin = 23
+        log.info("onewire_multi_app", "切换到引脚23")
+    else
+        -- 从23切换到54
+        -- 将PIN23配置为GPIO2功能,不再作为OneWire使用
+        log.info("onewire_multi_app", "将PIN23配置为GPIO2", pins.setup(23, "GPIO2"))
+        -- 设置GPIO2为高电平输出(开漏模式,高电平由上拉电阻提供)
+        log.info("onewire_multi_app", "将GPIO2设置为高电平输出(OneWire总线空闲状态)", gpio.setup(2, 1))
+        onewire_pin = 54
+        log.info("onewire_multi_app", "切换到引脚54")
+    end
+    
+    log.info("onewire_multi_app", "当前使用引脚:", onewire_pin)
+    
+    -- 配置新引脚为ONEWIRE功能
+    -- 分时复用原理:将选中的引脚配置为OneWire功能,连接到对应设备
+    -- 连接过程:先断开之前的设备连接,再连接新的设备
+    -- 电气特性:确保当前连接的设备具有完整的OneWire通信能力
+    log.info("onewire_multi_app", "将引脚" .. onewire_pin .. "配置为ONEWIRE功能", pins.setup(onewire_pin, "ONEWIRE"))
+    
+    log.info("onewire_multi_app", "引脚切换完成,当前使用: 引脚" .. onewire_pin)
+end
+
+-- 初始化OneWire总线
+local function init_onewire_bus()
+    log.info("onewire_multi_app", "初始化OneWire总线,通道: 0")
+    
+    -- 配置当前引脚
+    pins.setup(onewire_pin, "ONEWIRE")
+    
+    -- 初始化OneWire总线
+    onewire.init(0)
+    
+    -- 配置DS18B20标准时序参数
+    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
+    
+    log.info("onewire_multi_app", "OneWire总线初始化完成,通道: 0,引脚:" .. onewire_pin)
+    
+    return true
+end
+
+-- 检测DS18B20设备是否存在(分时复用场景)
+-- 分时逻辑:在当前连接的引脚上发送复位脉冲,检测该设备响应
+-- 单总线场景:只有当前连接的引脚上的设备会响应复位脉冲
+-- 返回值:true表示当前引脚连接的设备响应,false表示无设备响应
+local function detect_ds18b20_device()
+    log.info("onewire_multi_app", "检测DS18B20设备,引脚: " .. onewire_pin)
+    
+    -- 发送复位脉冲并检测设备
+    local present = onewire.reset(0, true)
+    
+    if present then
+        log.info("onewire_multi_app", "检测到DS18B20设备响应")
+        return true
+    else
+        log.warn("onewire_multi_app", "未检测到DS18B20设备响应")
+        return false
+    end
+end
+
+-- 读取DS18B20温度(单总线分时复用)
+-- 核心流程:读ROM ID → 选设备 → 温度转换 → 读数据 → CRC校验
+local function read_ds18b20_temperature()
+    log.info("onewire_multi_app", "开始读取DS18B20温度,引脚: " .. onewire_pin)
+    
+    local tbuff = zbuff.create(10)
+    local succ, crc8c, range, t
+    local rbuff = zbuff.create(9)
+    
+    -- 读取设备ROM ID(每个设备唯一)
+    log.info("onewire_multi_app", "读取设备ROM ID(64位唯一标识)")
+    
+    local id = zbuff.create(8)
+    id:set()
+    
+    succ, rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "读取ROM ID失败")
+        return nil
+    end
+    
+    -- 检查设备类型码(DS18B20应为0x28)
+    if id[0] ~= 0x28 then
+        log.warn("onewire_multi_app", "非DS18B20设备,类型码:", mcu.x32(id[0]))
+        return nil
+    end
+    
+    -- CRC校验设备ID
+    crc8c = crypto.crc8(id:query(0, 7), 0x31, 0, true)
+    if crc8c ~= id[7] then
+        log.warn("onewire_multi_app", "ROM ID CRC校验不对", 
+                "计算值:", mcu.x32(crc8c), "期望值:", mcu.x32(id[7]))
+        log.info("onewire_multi_app", "完整ROM ID:", id:query(0, 7):toHex())
+        return nil
+    end
+    
+    log.info("onewire_multi_app", "ROM ID校验成功:", id:query(0, 7):toHex())
+    
+    -- 通过MATCH ROM选择设备(确保只选中目标设备)
+    log.info("onewire_multi_app", "开始温度转换(通过ROM匹配选择设备)")
+    
+    -- 构建命令缓冲区:MATCH ROM(0x55) + 目标设备ROM ID + 温度转换命令(0x44)
+    -- 0x55是MATCH ROM命令,后面必须跟64位目标设备的ROM ID
+    tbuff:write(0x55)     -- MATCH ROM命令
+    tbuff:copy(nil, id)  -- 复制64位ROM ID(确保选择正确的设备)
+    tbuff:write(0xb8)
+    tbuff[tbuff:used() - 1] = 0x44  -- CONVERT T温度转换命令
+    
+    succ = onewire.tx(0, tbuff, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "发送温度转换命令失败")
+        return nil
+    end
+
+    -- 第三步:等待转换完成
+    log.info("onewire_multi_app", "等待温度转换完成")
+    
+    -- 等待一段时间让转换完成
+    sys.wait(750)
+    
+    -- 发送复位脉冲检查设备
+    succ = onewire.reset(0, true)
+    if not succ then
+        log.warn("onewire_multi_app", "等待转换完成时设备未响应")
+        return nil
+    end
+    
+    -- 检查转换是否完成
+    if onewire.bit(0) > 0 then
+        log.info("onewire_multi_app", "温度转换完成")
+    end
+    
+    -- 第四步:读取温度数据
+    log.info("onewire_multi_app", "读取温度数据")
+    
+    -- 构建读取命令:匹配ROM(0x55) + ROM ID + 读取暂存器命令(0xBE)
+    tbuff[tbuff:used() - 1] = 0xbe
+    succ = onewire.tx(0, tbuff, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "发送读取命令失败")
+        return nil
+    end
+    
+    -- 接收9字节温度数据
+    succ, rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
+    if not succ then
+        log.warn("onewire_multi_app", "温度数据接收失败")
+        return nil
+    end
+    
+    -- 第五步:CRC校验和温度计算
+    log.info("onewire_multi_app", "CRC校验和温度计算")
+    
+    -- CRC校验
+    crc8c = crypto.crc8(rbuff:toStr(0, 8), 0x31, 0, true)
+    if crc8c == rbuff[8] then
+        -- 计算温度值
+        range = (rbuff[4] >> 5) & 0x03
+        t = rbuff:query(0, 2, false, true)
+        t = t * (5000 >> range)
+        t = t / 10000
+        
+        -- 范围检查
+        if t >= -55.0 and t <= 125.0 then
+            log.info("onewire_multi_app", "温度读取成功:", string.format("%.2f°C", t))
+            return t
+        else
+            log.warn("onewire_multi_app", "温度值超出有效范围:", t)
+            return nil
+        end
+    else
+        log.warn("onewire_multi_app", "温度数据CRC校验不对", 
+                "计算值:", mcu.x32(crc8c), "期望值:", mcu.x32(rbuff[8]))
+        return nil
+    end
+end
+
+-- 简化版温度读取(用于快速测试)
+local function quick_read_ds18b20()
+    log.info("onewire_multi_app", "快速读取温度,引脚: " .. onewire_pin)
+    
+    -- 首先检测设备是否存在
+    if not detect_ds18b20_device() then
+        return nil
+    end
+    
+    -- 使用完整读取函数
+    return read_ds18b20_temperature()
+end
+
+-- 单总线分时复用主函数(同一条总线,分时访问不同设备)
+local function multi_sensor_app_main()
+    log.info("onewire_multi_app", "启动双传感器应用(引脚54和23)")
+    
+    -- 初始化硬件
+    if not init_hardware() then
+        log.error("onewire_multi_app", "硬件初始化失败,任务无法启动")
+        return
+    end
+    
+    -- 初始化OneWire总线
+    init_onewire_bus()
+    
+    -- 电源稳定延时:确保DS18B20内部电路就绪
+    power_stabilization_delay()
+    
+    -- 检测设备
+    local device_present = detect_ds18b20_device()
+    
+    if not device_present then
+        log.error("onewire_multi_app", "未检测到设备响应")
+        log.warn("onewire_multi_app", "硬件连接提示:")
+        log.warn("onewire_multi_app", "1. 传感器连接引脚54或23")
+        log.warn("onewire_multi_app", "2. 确保GPIO31/GPIO2已设置为高电平供电")
+        log.warn("onewire_multi_app", "3. 确保4.7kΩ上拉电阻正确安装")
+        log.warn("onewire_multi_app", "4. 检查传感器VDD、GND、DQ连接")
+        -- 关闭OneWire总线
+        onewire.deinit(0)
+        return
+    end
+    
+    log.info("onewire_multi_app", "开始双传感器连续监测...")
+    log.info("onewire_multi_app", "按PWR_KEY按键可切换引脚(54和23)")
+    
+  -- 主循环:按键切换设备,分时读取温度
+    local read_count = 0
+    local success_count = 0
+    
+    while true do
+        read_count = read_count + 1
+        
+        -- 检查按键状态
+        if pwr_key_pressed then
+            pwr_key_pressed = false
+            switch_onewire_pin()
+            
+            -- 重新初始化OneWire总线
+            init_onewire_bus()
+        end
+        
+        log.info("onewire_multi_app", "第" .. read_count .. "次读取,引脚:" .. onewire_pin)
+        
+        -- 尝试读取温度
+        local temperature = read_ds18b20_temperature()
+        
+        if temperature then
+            success_count = success_count + 1
+            log.info("onewire_multi_app", "引脚" .. onewire_pin .. "温度:", 
+                    string.format("%.2f°C", temperature), 
+                    "成功率:", string.format("%.1f%%", success_count/read_count*100))
+            
+            -- 简单的温度报警逻辑
+            if temperature > 30 then
+                log.warn("onewire_multi_app", "温度偏高:", string.format("%.2f°C", temperature))
+            elseif temperature < 10 then
+                log.warn("onewire_multi_app", "温度偏低:", string.format("%.2f°C", temperature))
+            end
+        else
+            log.warn("onewire_multi_app", "本次读取失败")
+            log.info("onewire_multi_app", "成功率:", string.format("%.1f%%", success_count/read_count*100))
+        end
+        
+        -- 等待下一次读取
+        sys.wait(2000)
+    end
+end
+log.info("onewire_multi_app", "双传感器应用模块加载完成(54和23切换)")
+-- 启动多传感器应用任务
+sys.taskInit(multi_sensor_app_main)
+

+ 0 - 108
module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/onewire_single_18b20/main.lua

@@ -1,108 +0,0 @@
-PROJECT = "onewiredemo"
-VERSION = "1.0.0"
-sys = require("sys")
-log.style(1)
-
---[[
-接线说明:
-   DS18B20    Air780EPM
-1. GND    -> GND
-2. VDD    -> 3.3V
-3. DATA    -> GPIO2
-
-注意:
-1. 3.3v在老版本的开发板上没有引脚, 所以需要外接, 一定要确保共地
-2. ONEWIRE功能支持在4个引脚使用, 但硬件通道只有一个, 默认是GPIO2
-3. 如需切换到其他脚, 参考如下切换逻辑, 选其中一种
-
-]]
-
-local function read_ds18b20(id)
-    local tbuff = zbuff.create(10)
-    local succ,crc8c,range,t
-    local rbuff = zbuff.create(9)
-    --如果有多个DS18B20,需要带上ID
-    tbuff:write(0x55)
-    tbuff:copy(nil, id)
-    tbuff:write(0xb8)
-    --如果只有1个DS18B20,就用无ID方式
-    --tbuff:write(0xcc,0xb8)
-    while true do
-        tbuff[tbuff:used() - 1] = 0x44
-        succ = onewire.tx(0, tbuff, false, true, true)
-        if not succ then
-            return
-        end
-        while true do
-            succ = onewire.reset(0, true)
-            if not succ then
-                return
-            end
-            if onewire.bit(0) > 0 then
-                log.info("温度转换完成")
-                break
-            end
-            sys.wait(10)
-        end
-        tbuff[tbuff:used() - 1] = 0xbe
-        succ = onewire.tx(0, tbuff, false, true, true)
-        if not succ then
-            return
-        end
-        succ,rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
-        crc8c = crypto.crc8(rbuff:toStr(0,8), 0x31, 0, true)
-        if crc8c == rbuff[8] then
-            range = (rbuff[4] >> 5) & 0x03
-            -- rbuff[0] = 0xF8
-            -- rbuff[1] = 0xFF
-            t = rbuff:query(0,2,false,true)
-            t = t * (5000 >> range)
-            t = t / 10000
-            log.info(t)
-        else
-            log.info("RAM DATA CRC校验不对",  mcu.x32(crc8c), mcu.x32(rbuff[8]))
-            return
-        end
-        sys.wait(500)
-    end
-end
-
-local function test_ds18b20()
-    local succ,rx_data
-    local id = zbuff.create(8)
-
-    local crc8c
-    onewire.init(0)
-    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
-    while true do
-        id:set() --清空id
-        succ,rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
-        if succ then
-            if id[0] == 0x28 then
-                crc8c = crypto.crc8(id:query(0,7), 0x31, 0, true)
-                if crc8c == id[7] then
-                    log.info("探测到DS18B20", id:query(0, 7):toHex())
-                    read_ds18b20(id)
-                    log.info("DS18B20离线,重新探测")
-                else
-                    log.info("ROM ID CRC校验不对",  mcu.x32(crc8c), mcu.x32(id[7]))
-                end
-            else
-                log.info("ROM ID不正确", mcu.x32(id[0]))
-            end
-        end
-        log.info("没有检测到DS18B20, 5秒后重试")
-        sys.wait(5000)
-
-    end
-    
-
-end
-
-if onewire then
-    sys.taskInit(test_ds18b20)
-else
-    log.info("no onewire")
-end
-
-sys.run()

+ 203 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/onewire_single_app.lua

@@ -0,0 +1,203 @@
+--[[
+@module  onewire_single_app
+@summary OneWire单DS18B20温度传感器应用演示模块(GPIO2默认模式)
+@version 1.0.0
+@date    2025.11.25
+@author  王棚嶙
+@usage
+本模块演示单DS18B20温度传感器的完整功能:
+1. 使用GPIO2默认OneWire功能
+2. 硬件通道0模式,无需引脚复用
+3. 优化的时序参数和错误处理
+4. 连续温度监测
+5. 完整的OneWire API接口演示
+]]
+
+
+log.info("onewire_single_app", "单传感器模块版本:1.0.0")
+
+-- 设置所有GPIO引脚电压为3.3V,确保DS18B20传感器正常供电
+pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
+
+-- DS18B20命令定义
+local CMD_CONVERT_T = 0x44
+local CMD_READ_SCRATCHPAD = 0xBE
+local CMD_SKIP_ROM = 0xCC
+local CMD_READ_ROM = 0x33
+
+-- 单传感器应用主函数
+local function single_sensor_app_main()
+    log.info("onewire_single_app", "启动单传感器应用")
+    
+    -- 初始化OneWire总线(使用硬件通道0模式)
+    log.info("onewire_single_app", "初始化OneWire总线...")
+    onewire.init(0)
+    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
+    log.info("onewire_single_app", "OneWire总线初始化完成,使用GPIO2默认引脚")
+    
+    
+    
+    -- 检测DS18B20设备
+    log.info("onewire_single_app", "检测DS18B20设备...")
+    
+    local succ, rx_data
+    local id = zbuff.create(8)
+    local crc8c
+    
+    -- 清空ID缓冲区
+    id:set()
+    
+    -- 读取设备ROM ID(使用手动配置的引脚)
+    succ, rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
+    
+    local detected = false
+    local device_id = nil
+    
+    if succ then
+        -- 检查家族码(DS18B20为0x28)
+        if id[0] == 0x28 then
+            -- CRC校验
+            crc8c = crypto.crc8(id:query(0,7), 0x31, 0, true)
+            if crc8c == id[7] then
+                log.info("onewire_single_app", "探测到DS18B20", id:query(0, 7):toHex())
+                detected = true
+                device_id = id
+            else
+                log.warn("onewire_single_app", "ROM ID CRC校验不对", mcu.x32(crc8c), mcu.x32(id[7]))
+            end
+        else
+            log.warn("onewire_single_app", "ROM ID不正确", mcu.x32(id[0]))
+        end
+    else
+        log.warn("onewire_single_app", "未检测到DS18B20设备,请检查硬件连接")
+        log.info("onewire_single_app", "硬件连接提示:")
+        log.info("onewire_single_app", "1. DS18B20 DATA引脚 -> GPIO2 (默认OneWire功能)")
+        log.info("onewire_single_app", "2. 确保上拉电阻4.7kΩ连接DATA到3.3V")
+        log.info("onewire_single_app", "3. 使用硬件通道0模式,无需引脚复用配置")
+    end
+    
+    if not detected then
+        log.warn("onewire_single_app", "设备检测失败,任务无法启动")
+        log.info("onewire_single_app", "单传感器应用启动完成")
+        onewire.deinit(0)
+        return
+    end
+    
+    log.info("onewire_single_app", "开始连续温度监测...")
+    
+    -- 读取DS18B20温度数据(单总线单设备模式)
+    -- 与多传感器模式的对比:
+    -- - 单传感器:使用SKIP ROM(0xCC)直接通信,无需ROM ID
+    -- - 多传感器:使用MATCH ROM(0x55)选择设备,需要目标ROM ID
+    -- 
+    -- 单设备读取流程:
+    -- 1. SKIP ROM:发送0xCC命令,跳过ROM ID识别
+    -- 2. 温度转换:发送CONVERT T(0x44)启动温度转换
+    -- 3. 读取数据:发送READ SCRATCHPAD(0xBE)读取温度数据
+    -- 4. CRC校验:验证数据完整性
+    -- 
+    -- 优势:通信简单高效,无需设备寻址
+    -- 限制:只能用于总线上只有一个设备的场景
+    local function read_temperature(dev_id)
+        local tbuff = zbuff.create(10)
+        local rbuff = zbuff.create(9)
+        local succ, crc8c, range, t
+        
+        -- 发送SKIP ROM命令(0xCC) - 跳过ROM识别,直接与设备通信
+        -- 工作原理:所有设备都会响应SKIP ROM命令,无需发送64位ROM ID
+        -- 适用场景:总线上只有一个设备,无需设备寻址和选择
+        -- 优势:通信效率高,无需传输ROM ID,简化通信流程
+        -- 风险:如果总线上有多个设备,所有设备会同时响应,造成冲突
+        tbuff:write(0xcc)
+        
+        -- 发送温度转换命令
+        tbuff[tbuff:used() - 1] = 0x44
+        succ = onewire.tx(0, tbuff, false, true, true)
+        if not succ then
+            log.warn("onewire_single_app", "发送温度转换命令失败")
+            return nil
+        end
+        
+        -- 等待转换完成(使用位检测)
+        local conversion_complete = false
+        local max_wait = 100
+        local wait_count = 0
+        
+        while wait_count < max_wait do
+            succ = onewire.reset(0, true)
+            if not succ then
+                log.warn("onewire_single_app", "等待转换完成时设备未响应")
+                return nil
+            end
+            if onewire.bit(0) > 0 then
+                log.info("onewire_single_app", "温度转换完成")
+                conversion_complete = true
+                break
+            end
+            sys.wait(10)
+            wait_count = wait_count + 1
+        end
+        
+        if not conversion_complete then
+            log.warn("onewire_single_app", "温度转换超时")
+            return nil
+        end
+        
+        -- 读取温度数据
+        tbuff[tbuff:used() - 1] = 0xBE
+        succ = onewire.tx(0, tbuff, false, true, true)
+        if not succ then
+            log.warn("onewire_single_app", "发送读取命令失败")
+            return nil
+        end
+        
+        succ, rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
+        if not succ or rbuff:used() ~= 9 then
+            log.warn("onewire_single_app", "温度数据读取失败")
+            return nil
+        end
+        
+        -- CRC校验
+        crc8c = crypto.crc8(rbuff:toStr(0,8), 0x31, 0, true)
+        if crc8c == rbuff[8] then
+            range = (rbuff[4] >> 5) & 0x03
+            t = rbuff:query(0,2,false,true)
+            t = t * (5000 >> range)
+            t = t / 10000
+            log.info("onewire_single_app", "温度读取成功:", string.format("%.2f°C", t))
+            return t
+        else
+            log.warn("onewire_single_app", "RAM DATA CRC校验不对", mcu.x32(crc8c), mcu.x32(rbuff[8]))
+            return nil
+        end
+    end
+    
+    -- 主循环 - 连续温度监测
+    while true do
+        local temperature = read_temperature(device_id)
+        
+        if temperature then
+            -- 简单的温度报警逻辑(示例)
+            if temperature > 30 then
+                log.warn("onewire_single_app", "温度偏高:", string.format("%.2f°C", temperature))
+            elseif temperature < 10 then
+                log.warn("onewire_single_app", "温度偏低:", string.format("%.2f°C", temperature))
+            else
+                log.info("onewire_single_app", "温度正常:", string.format("%.2f°C", temperature))
+            end
+        else
+            log.warn("onewire_single_app", "本次读取失败,继续下一次")
+        end
+        
+        -- 等待下一次读取
+        sys.wait(3000)
+    end
+    
+    log.info("onewire_single_app", "单传感器连续读取任务结束")
+    log.info("onewire_single_app", "单传感器应用启动完成")
+end
+
+log.info("onewire_single_app", "单传感器应用模块加载完成")
+
+-- 启动单传感器应用任务
+sys.taskInit(single_sensor_app_main)

+ 212 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/onewire/readme.md

@@ -0,0 +1,212 @@
+> 王棚嶙
+
+# OneWire综合演示项目
+
+## 功能模块介绍
+
+本demo演示了完整的DS18B20温度传感器OneWire单总线协议实现。项目采用模块化架构,分别实现单传感器和多传感器应用场景。
+
+1、main.lua:主程序入口 <br> 
+2、onewire_single_app.lua:演示单传感器功能模块(GPIO2默认OneWire功能,硬件通道0模式,3秒间隔连续监测)<br> 
+3、onewire_multi_app.lua:演示多传感器功能模块(引脚54/23切换,PWR_KEY按键控制,2秒间隔双路监测)<br> 
+
+## 演示功能概述
+
+###  主程序入口模块 (main.lua)
+
+- 初始化项目信息和版本号
+- 初始化看门狗,并定时喂狗
+- 启动一个循环定时器,每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
+- 加载onewire_single_app模块(通过require "onewire_single_app")
+- 加载onewire_multi_app模块(通过require "onewire_multi_app")
+
+### 单传感器模式 (onewire_single_app.lua)
+- 使用GPIO2默认OneWire功能,硬件通道0模式,无需引脚复用
+- 完整的CRC8数据校验机制,确保数据可靠性
+- 设备自动识别和ROM验证,支持设备类型检测
+- 3秒间隔连续温度监测,实时温度报警功能
+- zbuff缓冲区优化,提高数据传输效率
+
+
+### 多传感器模式 (onewire_multi_app.lua - 单总线多设备演示)
+
+
+**单总线多设备挂载原理**:
+1. **物理连接**:所有DS18B20的VDD、GND、DQ引脚分别并联到同一组单总线
+2. **设备识别**:每个DS18B20出厂时烧录了全球唯一的64位ROM ID
+3. **总线扫描**:主机发送SEARCH ROM(0xF0)命令发现总线上的所有设备
+4. **设备选择**:通过MATCH ROM(0x55)命令+目标设备ROM ID选择特定设备通信
+5. **分时操作**:每次只与一个设备通信,避免总线冲突
+
+**分时复用测试逻辑**(按键切换设备):
+- **引脚54**:连接设备A(ROM ID: 28-9F-C4-93-00-00-00-14)
+- **引脚23**:连接设备B(ROM ID: 28-59-F2-53-00-00-00-14)
+- **PWR_KEY按键**:按一次切换一个设备,实现同一条总线的分时使用
+
+**核心测试流程**:
+1. 初始化当前引脚的OneWire总线
+2. 发送SEARCH ROM命令扫描总线上的设备
+3. 读取并验证设备的64位ROM ID(家族码+序列号+CRC)
+4. 使用MATCH ROM(0x55)命令选择目标设备
+5. 发送温度转换命令(0x44)并等待完成
+6. 读取温度数据并进行CRC校验
+7. 输出设备ROM ID、温度值、读取成功率
+
+
+
+## 演示硬件环境
+1、Air780EPM核心板一块
+
+2、TYPE-C USB数据线一根
+
+3、ds18b20传感器两个
+
+4、Air780EPM核心板和数据线的硬件接线方式为
+
+- Air780EPM核心板通过TYPE-C USB口供电;(核心板USB旁边的开关拨到on一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+5、Air780EPM核心板和ds18b20传感器接线方式
+
+### 单传感器连接
+
+|   Air780EPM核心板     |    DS18B20传感器    |
+| --------------- | -------------------|
+|    VDD_EXT         |         VCC        |
+|    23/GPIO2     |         DQ         |
+|    GND          |         GND        |
+
+连接图:
+
+![image](https://docs.openluat.com/air780epm/luatos/app/driver/onewire/image/ce9d3b2c9b3a36c22388d710913668a7.jpg)
+
+### 多传感器连接
+
+
+|   Air780EPM核心板     |    DS18B20传感器1    |
+| --------------- | -------------------|
+|    VDD_EXT         |         VCC        |
+|    23/GPIO2     |         DQ         |
+|    任意GND          |         GND        |
+
+|   Air780EPM核心板     |    DS18B20传感器2    |
+| --------------- | -------------------|
+|    32/GPIO31         |         VCC        |
+|    54/CAM_MCLK     |         DQ         |
+|    任意GND          |         GND        |
+
+连接图:
+
+![image](https://docs.openluat.com/air780epm/luatos/app/driver/onewire/image/1957f5eb447654ec31894efb589e809b.jpg)
+
+## 演示软件环境
+
+1、Luatools下载调试工具: https://docs.openluat.com/air780epm/common/Luatools/
+
+2、内核固件版本:
+Air780EPM:https://docs.openluat.com/air780epm/luatos/firmware/version/
+
+## 演示核心步骤
+1、搭建好硬件环境
+
+2、通过Luatools将demo与固件烧录到核心板或开发板中
+
+3、烧录好后,板子开机将会在Luatools上看到如下打印:
+
+```lua
+(1)单传感器演示
+[2025-11-24 23:46:59.904][000000000.251] I/user.main onewire_demo 1.0.0
+[2025-11-24 23:46:59.932][000000000.258] I/user.onewire_single_app 单传感器模块版本: 002.002.000
+[2025-11-24 23:46:59.970][000000000.258] I/user.onewire_single_app 单传感器应用模块加载完成
+[2025-11-24 23:47:00.002][000000000.259] I/user.onewire_single_app 启动单传感器应用
+[2025-11-24 23:47:00.032][000000000.259] I/user.onewire_single_app 初始化OneWire总线...
+[2025-11-24 23:47:00.064][000000000.259] I/user.onewire_single_app OneWire总线初始化完成,使用GPIO2默认引脚
+[2025-11-24 23:47:00.096][000000000.259] I/user.onewire_single_app 检测DS18B20设备...
+[2025-11-24 23:47:00.124][000000000.267] I/user.onewire_single_app 探测到DS18B20 2859F253000000 14
+[2025-11-24 23:47:00.159][000000000.267] I/user.onewire_single_app 开始连续温度监测...
+[2025-11-24 23:47:00.186][000000000.276] I/user.onewire_single_app 温度转换完成
+[2025-11-24 23:47:00.216][000000000.289] I/user.onewire_single_app 温度读取成功: 85.00°C
+[2025-11-24 23:47:00.246][000000000.290] W/user.onewire_single_app 温度偏高: 85.00°C
+[2025-11-24 23:47:00.409][000000003.299] I/user.onewire_single_app 温度转换完成
+[2025-11-24 23:47:00.436][000000003.312] I/user.onewire_single_app 温度读取成功: 28.25°C
+[2025-11-24 23:47:00.465][000000003.313] I/user.onewire_single_app 温度正常: 28.25°C
+[2025-11-24 23:47:03.404][000000006.322] I/user.onewire_single_app 温度转换完成
+[2025-11-24 23:47:03.437][000000006.335] I/user.onewire_single_app 温度读取成功: 28.25°C
+[2025-11-24 23:47:03.469][000000006.335] I/user.onewire_single_app 温度正常: 28.25°C
+
+(2)双传感器演示
+[2025-11-24 23:49:45.699][000000000.260] I/user.onewire_multi_app 双传感器应用模块加载完成(54和23切换)
+[2025-11-24 23:49:45.732][000000000.261] I/user.onewire_multi_app 启动双传感器应用(引脚54和23)
+[2025-11-24 23:49:45.761][000000000.261] I/user.onewire_multi_app 初始化硬件配置...
+[2025-11-24 23:49:45.789][000000000.261] I/user.onewire_multi_app 硬件初始化完成
+[2025-11-24 23:49:45.822][000000000.262] I/user.onewire_multi_app 初始引脚: 引脚54 (ONEWIRE功能)
+[2025-11-24 23:49:45.859][000000000.262] I/user.onewire_multi_app 切换按键: PWR_KEY
+[2025-11-24 23:49:45.898][000000000.262] I/user.onewire_multi_app 支持引脚: 54 和 23 循环切换
+[2025-11-24 23:49:45.932][000000000.262] I/user.onewire_multi_app 电源控制: GPIO31/GPIO2 (已设置为高电平)
+[2025-11-24 23:49:45.963][000000000.262] I/user.onewire_multi_app 电源控制: 开启
+[2025-11-24 23:49:45.997][000000000.363] I/user.onewire_multi_app 初始化OneWire总线,通道: 0
+[2025-11-24 23:49:46.026][000000000.374] I/user.onewire_multi_app OneWire总线初始化完成,通道: 0,引脚:54
+[2025-11-24 23:49:46.059][000000000.573] I/user.onewire_multi_app 检测DS18B20设备,引脚: 54
+[2025-11-24 23:49:46.089][000000000.575] I/user.onewire_multi_app 检测到DS18B20设备响应
+[2025-11-24 23:49:46.117][000000000.575] I/user.onewire_multi_app 开始双传感器连续监测...
+[2025-11-24 23:49:46.148][000000000.575] I/user.onewire_multi_app 按PWR_KEY按键可切换引脚(54和23)
+[2025-11-24 23:49:46.181][000000000.575] I/user.onewire_multi_app 第1次读取,引脚:54
+[2025-11-24 23:49:46.212][000000000.575] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 54
+[2025-11-24 23:49:46.243][000000000.576] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-24 23:49:46.272][000000000.583] I/user.onewire_multi_app ROM ID校验成功: 289FC493000000 14
+[2025-11-24 23:49:46.297][000000000.583] I/user.onewire_multi_app 开始温度转换
+[2025-11-24 23:49:46.325][000000000.590] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-24 23:49:46.401][000000001.341] I/user.onewire_multi_app 温度转换完成
+[2025-11-24 23:49:46.433][000000001.342] I/user.onewire_multi_app 读取温度数据
+[2025-11-24 23:49:46.466][000000001.354] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-24 23:49:46.499][000000001.355] I/user.onewire_multi_app 温度读取成功: 27.44°C
+[2025-11-24 23:49:46.530][000000001.355] I/user.onewire_multi_app 引脚54温度: 27.44°C 成功率: 100.0%
+[2025-11-24 23:49:46.630][000000003.355] I/user.onewire_multi_app 第2次读取,引脚:54
+[2025-11-24 23:49:46.665][000000003.356] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 54
+[2025-11-24 23:49:46.698][000000003.356] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-24 23:49:46.727][000000003.363] I/user.onewire_multi_app ROM ID校验成功: 289FC493000000 14
+[2025-11-24 23:49:46.765][000000003.363] I/user.onewire_multi_app 开始温度转换
+[2025-11-24 23:49:46.799][000000003.371] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-24 23:49:46.982][000000004.122] I/user.onewire_multi_app 温度转换完成
+[2025-11-24 23:49:47.010][000000004.123] I/user.onewire_multi_app 读取温度数据
+[2025-11-24 23:49:47.043][000000004.135] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-24 23:49:47.077][000000004.136] I/user.onewire_multi_app 温度读取成功: 27.50°C
+[2025-11-24 23:49:47.110][000000004.137] I/user.onewire_multi_app 引脚54温度: 27.50°C 成功率: 100.0%
+[2025-11-24 23:49:48.999][000000006.137] I/user.onewire_multi_app 第3次读取,引脚:54
+[2025-11-24 23:49:49.030][000000006.138] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 54
+[2025-11-24 23:49:49.061][000000006.138] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-24 23:49:49.094][000000006.145] I/user.onewire_multi_app ROM ID校验成功: 289FC493000000 14
+[2025-11-24 23:49:49.124][000000006.145] I/user.onewire_multi_app 开始温度转换
+[2025-11-24 23:49:49.154][000000006.153] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-24 23:49:49.778][000000006.904] I/user.onewire_multi_app 温度转换完成
+[2025-11-24 23:49:49.806][000000006.905] I/user.onewire_multi_app 读取温度数据
+[2025-11-24 23:49:49.836][000000006.917] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-24 23:49:49.866][000000006.918] I/user.onewire_multi_app 温度读取成功: 27.50°C
+[2025-11-24 23:49:49.907][000000006.919] I/user.onewire_multi_app 引脚54温度: 27.50°C 成功率: 100.0%
+
+(3)双传感器按键切换演示
+[2025-11-24 23:49:51.147][000000008.278] I/user.onewire_multi_app 切换按键被按下
+[2025-11-24 23:49:51.783][000000008.919] I/user.onewire_multi_app 切换OneWire引脚...
+[2025-11-24 23:49:51.821][000000008.939] I/user.onewire_multi_app 将PAD54配置为GPIO3 true
+[2025-11-24 23:49:51.856][000000008.940] I/user.onewire_multi_app 将GPIO3设置为高电平输出 function: 0C7F4A10
+[2025-11-24 23:49:51.897][000000008.940] I/user.onewire_multi_app 切换到引脚23
+[2025-11-24 23:49:51.933][000000008.940] I/user.onewire_multi_app 当前使用引脚: 23
+[2025-11-24 23:49:51.965][000000008.941] I/user.onewire_multi_app 将引脚23配置为ONEWIRE功能 true
+[2025-11-24 23:49:51.994][000000008.961] I/user.onewire_multi_app 引脚切换完成,当前使用: 引脚23
+[2025-11-24 23:49:52.324][000000009.461] I/user.onewire_multi_app 初始化OneWire总线,通道: 0
+[2025-11-24 23:49:52.356][000000009.471] I/user.onewire_multi_app OneWire总线初始化完成,通道: 0,引脚:23
+[2025-11-24 23:49:52.431][000000009.571] I/user.onewire_multi_app 第4次读取,引脚:23
+[2025-11-24 23:49:52.474][000000009.571] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 23
+[2025-11-24 23:49:52.519][000000009.572] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-24 23:49:52.563][000000009.579] I/user.onewire_multi_app ROM ID校验成功: 2859F253000000 14
+[2025-11-24 23:49:52.593][000000009.579] I/user.onewire_multi_app 开始温度转换
+[2025-11-24 23:49:52.622][000000009.587] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-24 23:49:53.203][000000010.338] I/user.onewire_multi_app 温度转换完成
+[2025-11-24 23:49:53.239][000000010.339] I/user.onewire_multi_app 读取温度数据
+[2025-11-24 23:49:53.272][000000010.351] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-24 23:49:53.302][000000010.352] I/user.onewire_multi_app 温度读取成功: 27.81°C
+[2025-11-24 23:49:53.332][000000010.353] I/user.onewire_multi_app 引脚23温度: 27.81°C 成功率: 100.0%
+
+
+```

+ 366 - 291
module/Air780EHM_Air780EHV_Air780EGH/demo/tf_card/tfcard_app.lua

@@ -1,341 +1,416 @@
 --[[
-@module  tfcard_app
-@summary TF卡文件操作测试模块
-@version 1.0.0
-@date    2025.08.25
-@author  王棚嶙
+@module  onewire_multi_app
+@summary OneWire多DS18B20温度传感器应用演示模块(带切换功能)
+@version 002.002.000
+@date    2025.11.25
+@author  王棚嶙
 @usage
-本文件为TF卡的文件操作测试流程:
-1. 创建目录
-2. 创建并写入文件
-3. 检查文件是否存在
-4. 获取文件大小
-5. 读取文件内容
-6. 启动计数文件操作
-7. 文件追加测试
-8. 按行读取测试
-9. 读取后关闭文件
-10. 文件重命名
-11. 列举目录内容
-12. 删除文件
-13. 删除目录
-本文件没有对外接口,直接在main.lua中require "tfcard_app"就可以加载运行
-]] 
-
-
-
-
-
-local function tfcard_main_task() -- 开始进行主测试流程。
-    -- ##########  SPI初始化 ##########
-    -- 如果使用核心板演示环境,请打开33——36行代码,同时关闭38——43行的代码。
-    -- 如果使用开发板演示环境,请打开38——43行代码,同时关闭33——36行的代码。
-    -- 在Air780EHM/EHV/EGH核心板上TF卡的的pin_cs为gpio8,spi_id为0.请根据实际硬件修改
-    spi_id, pin_cs = 0, 8
-    spi.setup(spi_id, nil, 0, 0, 400 * 1000)
-    --初始化后拉高pin_cs,准备开始挂载TF卡
-    gpio.setup(pin_cs, 1)
-
-    -- Air780EHM/EHV/EGH开发板上的pin_cs为gpio16,spi_id为0.请根据实际硬件修改
-    -- spi_id, pin_cs = 0, 16
-    -- spi.setup(spi_id, nil, 0, 0, 400 * 1000)
-    --设置片选引脚同一spi总线上的所有从设备在初始化时必须要先拉高CS脚,防止从设备之间互相干扰。
-    -- 在Air780EHM/EHV/EGH开发板上,TF卡和ch390共用SPI0总线。
-    -- gpio.setup(pin_cs, 1)
-
-    -- ########## 开始进行tf卡挂载 ##########
-    -- 挂载失败默认格式化,
-    -- 如无需格式化应改为fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000, nil, 1, false),
-    -- 一般是在测试硬件是否有问题的时候把格式化取消掉
-    mount_ok, mount_err = fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)
-    if mount_ok then
-        log.info("fatfs.mount", "挂载成功", mount_err)
-    else
-        log.error("fatfs.mount", "挂载失败", mount_err)
-        goto resource_cleanup
-    end
-
-    -- ########## 获取SD卡的可用空间信息并打印。 ########## 
-    data, err = fatfs.getfree("/sd")
-    if data then
-        --打印SD卡的可用空间信息
-        log.info("fatfs", "getfree", json.encode(data))
-    else
-        --打印错误信息
-        log.info("fatfs", "getfree", "err", err)
-        goto resource_cleanup
+本模块演示多DS18B20温度传感器的完整功能:
+1. 多传感器切换控制
+2. 电源管理(GPIO控制)
+3. 按键切换传感器
+4. 多路温度同时监测
+5. 使用引脚复用功能(pins.setup)
+]]
+
+log.info("onewire_multi_app", "多传感器模块版本: 002.002.000")
+
+-- 设置所有GPIO引脚电压为3.3V,确保DS18B20传感器正常供电
+pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
+
+-- GPIO2和GPIO31控制电源,确保DS18B20供电正常
+gpio.setup(2, 1)  -- GPIO2输出高电平
+gpio.setup(31, 1) -- GPIO31输出高电平
+
+-- 传感器配置
+local sensor_config = {
+    {pin = 2,  channel = 0, name = "GPIO2(默认)"},    -- GPIO2使用通道0
+    {pin = 54, channel = 3, name = "引脚54(复用)"}    -- 引脚54使用通道3
+}
+
+local current_sensor = 1  -- 当前使用的传感器索引
+
+-- DS18B20命令定义
+local CMD_CONVERT_T = 0x44     -- 温度转换命令
+local CMD_READ_SCRATCHPAD = 0xBE  -- 读取暂存器命令
+local CMD_SKIP_ROM = 0xCC      -- 跳过ROM命令
+
+-- 全局状态变量
+local task_running = false
+local switch_pressed = false
+
+-- CRC8校验函数
+local function crc8_check(data)
+    if not data or #data == 0 then
+        return false
     end
-
-    -- 列出所有挂载点,如不需要,可注释掉。
-    data = io.lsmount()
-    log.info("fs", "lsmount", json.encode(data))
-
-    -- ########## 功能: 启用fatfs调试模式 ##########
-    -- fatfs.debug(1) -- 若挂载失败,可以尝试打开调试信息,查找原因.(设置调试模式)
-
-    -- 执行tfcard文件操作演示
-    log.info("文件操作", "===== 开始文件操作 =====")
-
-    dir_path = "/sd/io_test"
-
-    -- 1. 创建目录
-    if io.mkdir(dir_path) then
-        log.info("io.mkdir", "目录创建成功", "路径:" .. dir_path)
-    else
-        -- 检查是否目录已存在
-        if io.exists(dir_path) then
-            log.warn("io.mkdir", "目录已存在,跳过创建", "路径:" .. dir_path)
-        else
-            log.error("io.mkdir", "目录创建失败且目录不存在", "路径:" .. dir_path)
-            goto resource_cleanup
+    
+    local crc = 0
+    for i = 1, #data - 1 do
+        crc = crc ~ data:byte(i)
+        for j = 1, 8 do
+            if crc & 0x01 then
+                crc = (crc >> 1) ~ 0x8C
+            else
+                crc = crc >> 1
+            end
         end
     end
+    
+    return crc == data:byte(#data)
+end
 
-    -- 2. 创建并写入文件
-    file_path = dir_path .. "/boottime"
-    file = io.open(file_path, "wb")
-    if file then
-        file:write("这是io库API文档示例的测试内容")
-        file:close()
-        --在LuatOS文件操作中,执行file:close()是必须且关键的操作,它用于关闭文件句柄,释放资源,并确保数据被正确写入磁盘。
-        -- 如果不执行file:close(),可能会导致数据丢失、文件损坏或其他不可预测的问题。
-        log.info("文件创建", "文件写入成功", "路径:" .. file_path)
-    else
-        log.error("文件创建", "文件创建失败", "路径:" .. file_path)
-        goto resource_cleanup
+-- 计算温度值
+local function calculate_temperature(data)
+    if not data or #data < 2 then
+        log.warn("onewire_multi_app", "温度数据无效或长度不足")
+        return nil
     end
-
-    -- 3. 检查文件是否存在
-    if io.exists(file_path) then
-        log.info("io.exists", "文件存在", "路径:" .. file_path)
-    else
-        log.error("io.exists", "文件不存在", "路径:" .. file_path)
-        goto resource_cleanup
+    
+    local temp_raw = data:byte(1) + (data:byte(2) * 256)
+    
+    if temp_raw > 32767 then
+        temp_raw = temp_raw - 65536
     end
-
-    -- 4. 获取文件大小
-    file_size = io.fileSize(file_path)
-    if file_size then
-        log.info("io.fileSize", "文件大小:" .. file_size .. "字节", "路径:" .. file_path)
-    else
-        log.error("io.fileSize", "获取文件大小失败", "路径:" .. file_path)
-        goto resource_cleanup
+    
+    local temperature = temp_raw * 0.0625
+    
+    if temperature < -55.0 or temperature > 125.0 then
+        log.warn("onewire_multi_app", "温度值超出有效范围:", temperature)
+        return nil
     end
+    
+    return temperature
+end
 
-    -- 5. 读取文件内容
-    file = io.open(file_path, "rb")
-    if file then
-        content = file:read("*a")
-        log.info("文件读取", "路径:" .. file_path, "内容:" .. content)
-        file:close()
-    else
-        log.error("文件操作", "无法打开文件读取内容", "路径:" .. file_path)
-        goto resource_cleanup
+-- 电源控制函数
+local function power_control(on)
+    log.info("onewire_multi_app", "电源控制:", on and "开启" or "关闭")
+    if on then
+        sys.wait(100)  -- 等待电源稳定
     end
+    return true
+end
 
-    -- 6. 启动计数文件操作
-    count = 0
-    --以只读模式打开文件
-    file = io.open(file_path, "rb")
-    if file then
-        data = file:read("*a")
-        log.info("启动计数", "文件内容:", data, "十六进制:", data:toHex())
-        count = tonumber(data) or 0
-        file:close()
-    else
-        log.warn("启动计数", "文件不存在或无法打开")
-
+-- 初始化指定传感器
+local function init_sensor(sensor_idx)
+    local config = sensor_config[sensor_idx]
+    
+    log.info("onewire_multi_app", "初始化传感器:", config.name, "引脚:", config.pin, "通道:", config.channel)
+    
+    if config.pin == 54 then
+        pins.setup(config.pin, "ONEWIRE")
+        log.info("onewire_multi_app", "配置引脚54为ONEWIRE复用功能")
     end
+    
+    onewire.init(config.channel)
+    onewire.timing(config.channel, false, 0, 480, 70, 70, 410, 70, 10, 10, 15, 70)
+    
+    log.info("onewire_multi_app", "传感器初始化完成:", config.name)
+    return config.channel
+end
 
-    log.info("启动计数", "当前值:", count)
-    count=count + 1
-    log.info("启动计数", "更新值:", count)
-
-    file = io.open(file_path, "wb")
-    if file then
-        file:write(tostring(count))
-        file:close()
-        log.info("文件写入", "路径:" .. file_path, "内容:", count)
+-- 检测DS18B20设备是否存在
+local function detect_ds18b20_device(channel)
+    local present = onewire.reset(channel, true)
+    if present then
+        log.info("onewire_multi_app", "通道", channel, "检测到DS18B20设备")
     else
-        log.error("文件写入", "无法打开文件", "路径:" .. file_path)
-        goto resource_cleanup
+        log.warn("onewire_multi_app", "通道", channel, "未检测到DS18B20设备")
     end
+    return present
+end
 
-    -- 7. 文件追加测试
-    append_file = dir_path .. "/test_a"
-    -- 清理旧文件
-    os.remove(append_file)
+-- 初始化OneWire总线
+local function init_onewire_bus(channel)
+    log.info("onewire_multi_app", "初始化OneWire总线,通道:", channel)
+    onewire.init(channel)
+    onewire.timing(channel, false, 0, 480, 70, 70, 410, 70, 10, 10, 15, 70)
+    log.info("onewire_multi_app", "OneWire总线初始化完成,通道:", channel)
+    return true
+end
 
-    -- 创建并写入初始内容
-    file = io.open(append_file, "wb")
-    if file then
-        file:write("ABC")
-        file:close()
-        log.info("文件创建", "路径:" .. append_file, "初始内容:ABC")
-    else
-        log.error("文件创建", "无法创建文件", "路径:" .. append_file)
-        goto resource_cleanup
+-- 读取DS18B20温度
+local function read_ds18b20_temperature(channel)
+    log.info("onewire_multi_app", "读取DS18B20温度,通道:", channel)
+    
+    local tbuff = zbuff.create(2)
+    local rbuff = zbuff.create(9)
+    
+    local present = onewire.reset(channel, true)
+    if not present then
+        log.warn("onewire_multi_app", "通道", channel, "设备未响应")
+        return nil
     end
-
-    -- 追加内容
-    file = io.open(append_file, "a+")
-    if file then
-        file:write("def")
-        file:close()
-        log.info("文件追加", "路径:" .. append_file, "追加内容:def")
-    else
-        log.error("文件追加", "无法打开文件进行追加", "路径:" .. append_file)
-        goto resource_cleanup
-
+    
+    tbuff:set(0, CMD_SKIP_ROM)
+    tbuff:set(1, CMD_CONVERT_T)
+    local succ = onewire.tx(channel, tbuff, 0, 2, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "通道", channel, "发送温度转换命令失败")
+        return nil
     end
-
-    -- 验证追加结果
-    file = io.open(append_file, "r")
-    if file then
-        data = file:read("*a")
-        log.info("文件验证", "路径:" .. append_file, "内容:" .. data, "结果:",
-            data == "ABCdef" and "成功" or "失败")
-        file:close()
-    else
-        log.error("文件验证", "无法打开文件进行验证", "路径:" .. append_file)
-        goto resource_cleanup
+    
+    log.info("onewire_multi_app", "等待温度转换完成...")
+    sys.wait(750)
+    
+    local succ = onewire.reset(channel, true)
+    if not succ then
+        log.warn("onewire_multi_app", "通道", channel, "读取时设备未响应")
+        return nil
     end
-
-    -- 8. 按行读取测试
-    line_file = dir_path .. "/testline"
-    file = io.open(line_file, "w")
-    if file then
-        file:write("abc\n")
-        file:write("123\n")
-        file:write("wendal\n")
-        file:close()
-        log.info("文件创建", "路径:" .. line_file, "写入3行文本")
-    else
-        log.error("文件创建", "无法创建文件", "路径:" .. line_file)
-        goto resource_cleanup
+    
+    tbuff:set(0, CMD_SKIP_ROM)
+    tbuff:set(1, CMD_READ_SCRATCHPAD)
+    succ = onewire.tx(channel, tbuff, 0, 2, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "通道", channel, "发送读取命令失败")
+        return nil
     end
-
-    -- 按行读取文件
-    file = io.open(line_file, "r")
-    if file then
-        log.info("按行读取", "路径:" .. line_file, "第1行:", file:read("*l"))
-        log.info("按行读取", "路径:" .. line_file, "第2行:", file:read("*l"))
-        log.info("按行读取", "路径:" .. line_file, "第3行:", file:read("*l"))
-        file:close()
-    else
-        log.error("按行读取", "无法打开文件", "路径:" .. line_file)
-        goto resource_cleanup
+    
+    rbuff:resize(9)
+    local succ, rx_len = onewire.rx(channel, 9, nil, rbuff, 0, false, false, false)
+    if not succ or rx_len ~= 9 then
+        log.warn("onewire_multi_app", "通道", channel, "温度数据接收失败,长度:", rx_len or 0)
+        return nil
     end
-
-    -- 9. 文件重命名
-    old_path = append_file
-    new_path = dir_path .. "/renamed_file.txt"
-    success, err = os.rename(old_path, new_path)
-    if success then
-        log.info("os.rename", "文件重命名成功", "原路径:" .. old_path, "新路径:" .. new_path)
-
-        -- 验证重命名结果
-        if io.exists(new_path) and not io.exists(old_path) then
-            log.info("验证结果", "重命名验证成功", "新文件存在", "原文件不存在")
+    
+    local crc8c = crypto.crc8(rbuff:query(0,8), 0x31, 0, true)
+    if crc8c == rbuff[8] then
+        local temp_low = rbuff[0]
+        local temp_high = rbuff[1]
+        local temp_raw = temp_low + (temp_high * 256)
+        
+        if temp_raw > 32767 then
+            temp_raw = temp_raw - 65536
+        end
+        
+        local temperature = temp_raw * 0.0625
+        
+        if temperature >= -55.0 and temperature <= 125.0 then
+            log.info("onewire_multi_app", "温度读取成功,通道", channel, ":", string.format("%.2f°C", temperature))
+            return temperature
         else
-            log.error("验证结果", "重命名验证失败")
+            log.warn("onewire_multi_app", "温度值超出有效范围:", temperature)
+            return nil
         end
     else
-        log.error("os.rename", "重命名失败", "错误:" .. tostring(err), "原路径:" .. old_path)
-        goto resource_cleanup
+        log.warn("onewire_multi_app", "CRC校验失败,期望:", string.format("0x%02X", crc8c), 
+                 "实际:", string.format("0x%02X", rbuff[8]))
+        return nil
     end
+end
 
-    -- 10. 列举目录内容
-    log.info("目录操作", "===== 开始目录列举 =====")
+-- 切换传感器
+local function switch_onewire_sensor()
+    log.info("onewire_multi_app", "切换传感器...")
+    
+    local old_config = sensor_config[current_sensor]
+    onewire.deinit(old_config.channel)
+    
+    current_sensor = (current_sensor % #sensor_config) + 1
+    local new_config = sensor_config[current_sensor]
+    
+    local channel = init_sensor(current_sensor)
+    
+    log.info("onewire_multi_app", "切换到传感器:", new_config.name, "通道:", channel)
+    return channel
+end
 
-    ret, data = io.lsdir(dir_path, 50, 0) -- 50表示最多返回50个文件,0表示从目录开头开始
-    if ret then
-        log.info("fs", "lsdir", json.encode(data))
-    else
-        log.info("fs", "lsdir", "fail", ret, data)
-        goto resource_cleanup
+-- 初始化硬件
+local function init_hardware()
+    log.info("onewire_multi_app", "初始化硬件配置...")
+    
+    gpio.setup(gpio.PWR_KEY, function()
+        switch_pressed = true
+        log.info("onewire_multi_app", "切换按键被按下")
+    end, gpio.PULLUP, gpio.RISING)
+    
+    local channel = init_sensor(current_sensor)
+    local detected = detect_ds18b20_device(channel)
+    
+    if not detected then
+        log.warn("onewire_multi_app", "第一个传感器未检测到,尝试第二个传感器...")
+        current_sensor = 2
+        channel = init_sensor(current_sensor)
+        detected = detect_ds18b20_device(channel)
     end
+    
+    log.info("onewire_multi_app", "硬件初始化完成")
+    log.info("onewire_multi_app", "当前使用传感器:", sensor_config[current_sensor].name)
+    
+    return detected
+end
 
-    -- 11. 删除文件测试
-    -- 测试删除renamed_file.txt文件
-    if os.remove(new_path) then
-        log.info("os.remove", "文件删除成功", "路径:" .. new_path)
-
-        -- 验证renamed_file.txt删除结果
-        if not io.exists(new_path) then
-            log.info("验证结果", "renamed_file.txt文件删除验证成功")
-        else
-            log.error("验证结果", "renamed_file.txt文件删除验证失败")
-        end
-    else
-        log.error("io.remove", "renamed_file.txt文件删除失败", "路径:" .. new_path)
-        goto resource_cleanup
+-- 演示OneWire初始化功能
+local function demonstrate_onewire_init()
+    log.info("demo", "=== 1. 多通道onewire.init()演示 ===")
+    for i, config in ipairs(sensor_config) do
+        log.info("demo", "初始化通道", config.channel, "对应引脚", config.pin, config.name)
+        onewire.init(config.channel)
     end
+    log.info("demo", "OneWire初始化演示完成")
+end
 
-    -- 测试删除testline文件
-    if os.remove(line_file) then
-        log.info("os.remove", "testline文件删除成功", "路径:" .. line_file)
+-- 演示时序配置功能
+local function demonstrate_onewire_timing()
+    log.info("demo", "=== 2. 多通道onewire.timing()演示 ===")
+    for i, config in ipairs(sensor_config) do
+        log.info("demo", "配置通道", config.channel, "的DS18B20标准时序")
+        onewire.timing(config.channel, false, 0, 480, 70, 70, 410, 70, 10, 10, 15, 70)
+    end
+    log.info("demo", "时序配置演示完成")
+end
 
-        -- 验证删除结果
-        if not io.exists(line_file) then
-            log.info("验证结果", "testline文件删除验证成功")
-        else
-            log.error("验证结果", "testline文件删除验证失败")
-        end
-    else
-        log.error("io.remove", "testline文件删除失败", "路径:" .. line_file)
-        goto resource_cleanup
+-- 演示设备检测功能
+local function demonstrate_device_detection()
+    log.info("demo", "=== 3. 多通道设备检测演示 ===")
+    for i, config in ipairs(sensor_config) do
+        local present = onewire.reset(config.channel, true)
+        log.info("demo", "通道", config.channel, config.name, "检测状态:", present and "检测到设备" or "未检测到设备")
     end
+    log.info("demo", "设备检测演示完成")
+end
 
-    if os.remove(file_path) then
-        log.info("os.remove", "文件删除成功", "路径:" .. file_path)
+-- 演示电源控制功能
+local function demonstrate_power_control()
+    log.info("demo", "=== 4. 电源控制演示 ===")
+    power_control(true)
+    power_control(false)
+    log.info("demo", "电源控制演示完成")
+end
 
-        -- 验证删除结果
-        if not io.exists(file_path) then
-            log.info("验证结果", "boottime文件删除验证成功")
-        else
-            log.error("验证结果", "boottime文件删除验证失败")
-        end
-    else
-        log.error("io.remove", "boottime文件删除失败", "路径:" .. file_path)
-        goto resource_cleanup
+-- 演示传感器切换功能
+local function demonstrate_sensor_switching()
+    log.info("demo", "=== 5. 传感器切换功能演示 ===")
+    local original_sensor = current_sensor
+    
+    for i = 1, #sensor_config do
+        local channel = switch_onewire_sensor()
+        local detected = detect_ds18b20_device(channel)
+        log.info("demo", "切换到传感器", current_sensor, "检测状态:", detected and "成功" or "失败")
+        sys.wait(500)
     end
+    
+    current_sensor = original_sensor
+    init_sensor(current_sensor)
+    log.info("demo", "传感器切换演示完成,恢复原始传感器")
+end
 
-    -- 12. 删除目录(不能删除非空目录,所以在删除目录前要确保目录内没有文件或子目录)
-    if io.rmdir(dir_path) then
-        log.info("io.rmdir", "目录删除成功", "路径:" .. dir_path)
-
-        -- 验证删除结果
-        if not io.exists(dir_path) then
-            log.info("验证结果", "目录删除验证成功")
-        else
-            log.error("验证结果", "目录删除验证失败")
-        end
-    else
-        log.error("io.rmdir", "目录删除失败", "路径:" .. dir_path)
-        goto resource_cleanup
+-- 演示位操作功能
+local function demonstrate_bit_operations()
+    log.info("demo", "=== 6. 位操作功能演示 ===")
+    for i, config in ipairs(sensor_config) do
+        onewire.bit(config.channel, 1)
+        log.info("demo", "通道", config.channel, "发送位: 1")
+        
+        local bit_value = onewire.bit(config.channel)
+        log.info("demo", "通道", config.channel, "读取位值:", bit_value)
     end
+    log.info("demo", "位操作演示完成")
+end
+
+-- 演示调试模式功能
+local function demonstrate_debug_mode()
+    log.info("demo", "=== 7. 调试模式演示 ===")
+    log.info("demo", "调试模式当前状态: 关闭(可通过取消注释开启)")
+    -- for i, config in ipairs(sensor_config) do
+    --     onewire.debug(config.channel, true)
+    --     log.info("demo", "开启通道", config.channel, "的调试模式")
+    -- end
+    log.info("demo", "调试模式演示完成")
+end
 
-    log.info("文件操作", "===== 文件操作完成 =====")
+-- 演示资源释放功能
+local function demonstrate_resource_cleanup()
+    log.info("demo", "=== 8. 资源释放演示 ===")
+    for i, config in ipairs(sensor_config) do
+        onewire.deinit(config.channel)
+        log.info("demo", "释放通道", config.channel, "的资源")
+    end
+    log.info("demo", "资源释放演示完成")
+    
+    -- 重新初始化所有通道
+    for i, config in ipairs(sensor_config) do
+        init_sensor(i)
+    end
+end
 
-    -- ########## 功能: 收尾功能演示##########
-    -- 卸载文件系统和关闭SPI
-    ::resource_cleanup::
+-- 演示多传感器OneWire API接口
+local function demonstrate_multi_onewire_apis()
+    log.info("onewire_multi_app", "开始演示多传感器OneWire API接口...")
+    
+    demonstrate_onewire_init()
+    demonstrate_onewire_timing()
+    demonstrate_device_detection()
+    demonstrate_power_control()
+    demonstrate_sensor_switching()
+    demonstrate_bit_operations()
+    demonstrate_debug_mode()
+    demonstrate_resource_cleanup()
+    
+    log.info("onewire_multi_app", "多传感器API演示完成")
+end
 
-    log.info("结束", "开始执行关闭操作...")  
-    -- 如已挂载需先卸载文件系统,未挂载直接关闭SPI
-    if mount_ok then
-        if fatfs.unmount("/sd") then
-            log.info("文件系统", "卸载成功")
+-- 多传感器应用主函数
+local function multi_sensor_app_main()
+    log.info("onewire_multi_app", "启动多传感器应用")
+    
+    if not init_hardware() then
+        log.error("onewire_multi_app", "所有传感器都未检测到,请检查硬件连接")
+        log.warn("onewire_multi_app", "硬件连接提示:")
+        log.warn("onewire_multi_app", "1. 传感器1连接GPIO2 (默认OneWire功能)")
+        log.warn("onewire_multi_app", "2. 传感器2连接引脚54 (复用为GPIO3/ONEWIRE)")
+        log.warn("onewire_multi_app", "3. 确保GPIO31/GPIO2已设置为高电平供电")
+        return
+    end
+    
+    task_running = true
+    log.info("onewire_multi_app", "开始多传感器连续监测...")
+    log.info("onewire_multi_app", "按PWR_KEY按键可切换传感器")
+    
+    while task_running do
+        if switch_pressed then
+            switch_pressed = false
+            switch_onewire_sensor()
+        end
+        
+        local config = sensor_config[current_sensor]
+        local temperature = read_ds18b20_temperature(config.channel)
+        
+        if temperature then
+            log.info("onewire_multi_app", 
+                "传感器" .. current_sensor .. "(" .. config.name .. "):", 
+                string.format("%.2f°C", temperature))
         else
-            log.error("文件系统", "卸载失败")
+            log.warn("onewire_multi_app", "传感器" .. current_sensor .. "读取失败")
         end
+        
+        sys.wait(1000)
+    end
+    
+    for i, config in ipairs(sensor_config) do
+        onewire.deinit(config.channel)
     end
+    
+    log.info("onewire_multi_app", "多传感器应用结束")
+end
 
-    -- 2. 关闭SPI接口
-    spi.close(spi_id)
-    log.info("SPI接口", "已关闭")
+-- 启动多传感器应用任务
+local function start_multi_sensor_app()
+    sys.taskInit(function()
+        demonstrate_multi_onewire_apis()
+        multi_sensor_app_main()
+    end)
+end
 
+-- 模块初始化函数
+local function init_module()
+    log.info("onewire_multi_app", "多传感器应用模块初始化")
+    start_multi_sensor_app()
 end
 
-sys.taskInit(tfcard_main_task)
+-- 启动模块
+init_module()
+
+log.info("onewire_multi_app", "多传感器应用模块加载完成")

+ 65 - 0
module/Air780EPM/demo/onewire/main.lua

@@ -0,0 +1,65 @@
+--[[
+@module  main
+@summary OneWire综合演示项目主文件(单传感器 + 多传感器)
+@version 001.000.000
+@date    2025.11.25
+@author  王棚嶙
+@usage
+本演示项目整合单DS18B20和多DS18B20传感器功能:
+1. 单传感器模式:GPIO2默认OneWire功能、硬件通道0模式、CRC校验、3秒间隔连续监测
+2. 多传感器模式:引脚54/23切换、PWR_KEY按键控制、电源管理、2秒间隔双路监测
+3. 完整的OneWire API接口演示、错误处理、设备检测、温度报警
+]]
+
+-- 项目信息
+PROJECT = "onewire_demo"
+VERSION = "001.000.000"
+
+
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+--添加硬狗防止程序卡死
+if wdt then
+    wdt.init(9000)--初始化watchdog设置为9s
+    sys.timerLoopStart(wdt.feed, 3000)--3s喂一次狗
+end
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 在加载以下两个功能的时候建议分别打开,避免同时初始化OneWire总线,导致资源冲突
+-- 单设备模式:使用GPIO2默认OneWire功能
+-- 双设备模式:GPIO2默认 + 引脚54复用
+
+-- 加载单传感器应用模块
+-- require("onewire_single_app")
+
+-- 加载多传感器应用模块
+require("onewire_multi_app")
+
+
+-- 启动系统主循环
+sys.run()

+ 0 - 9
module/Air780EPM/demo/onewire/onewire_multi_18b20_swich_read/main.lua

@@ -1,9 +0,0 @@
-PROJECT = "DS18B20_GPIO_SWITCH_READ_temperature"
-VERSION = "1.0.0"
-sys = require("sys")
-log.style(1)
-
-
-require"switch_read"
-
-sys.run()

+ 0 - 125
module/Air780EPM/demo/onewire/onewire_multi_18b20_swich_read/switch_read.lua

@@ -1,125 +0,0 @@
-
-pm.ioVol(pm.IOVOL_ALL_GPIO, 3300) -- 所有GPIO高电平输出3.3V(方便使用VDD_EXT给18B20供电)
-gpio.setup(2, 1) --GPIO2控制780EPM开发板V1.3版本camera电源打开和关闭
-gpio.setup(31, 1) -- GPIO31控制780EPM开发板V1.4版本camera电源打开和关闭
-
-local onewire_pin = 54 --18B20接的pin 54脚
-
-
-pins.setup(onewire_pin, "ONEWIRE") -- PAD54脚既是GPIO3也是cam_mclk 
-
---读取当前pin脚上的18B20温度
-local function read_ds18b20(id)
-log.info("读取温度",id)
-    local tbuff = zbuff.create(10)
-    local succ, crc8c, range, t
-    local rbuff = zbuff.create(9)
-    tbuff:write(0x55)
-    tbuff:copy(nil, id)
-    tbuff:write(0xb8)
-  
-    tbuff[tbuff:used() - 1] = 0x44
-    succ = onewire.tx(0, tbuff, false, true, true)
-    if not succ then
-        return
-    end
-
-    succ = onewire.reset(0, true)
-    if not succ then
-        return
-    end
-    if onewire.bit(0) > 0 then
-        log.info("温度转换完成")
-    end
-    tbuff[tbuff:used() - 1] = 0xbe
-    succ = onewire.tx(0, tbuff, false, true, true)
-    if not succ then
-        return
-    end
-    succ, rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
-    crc8c = crypto.crc8(rbuff:toStr(0, 8), 0x31, 0, true)
-    if crc8c == rbuff[8] then
-        range = (rbuff[4] >> 5) & 0x03
-        -- rbuff[0] = 0xF8
-        -- rbuff[1] = 0xFF
-        t = rbuff:query(0, 2, false, true)
-        t = t * (5000 >> range)
-        t = t / 10000
-        log.info("当前温度", t,"原始值为",mcu.x32(rbuff[8]))
-    else
-        log.info("RAM DATA CRC校验不对", mcu.x32(crc8c), mcu.x32(rbuff[8]))
-        return
-    end
-
-end
-
---初始化当前pin脚上的18B20,并读取18B20的唯一识别ID
-local function test_ds18b20()
-    local succ, rx_data
-    local id = zbuff.create(8)
-    local crc8c
-    onewire.init(0) -- 初始化单总线
-    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
-    id:set() -- 清空id
-    succ, rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
-    if succ then
-        if id[0] == 0x28 then
-            crc8c = crypto.crc8(id:query(0, 7), 0x31, 0, true)
-            if crc8c == id[7] then
-                log.info("探测到DS18B20", "18B20对应唯一ID为", id:query(0, 7):toHex())
-                read_ds18b20(id)
-                -- log.info("DS18B20离线,重新探测")
-            else
-                log.info("ROM ID CRC校验不对", mcu.x32(crc8c), mcu.x32(id[7]))
-            end
-        else
-            log.info("ROM ID不正确", mcu.x32(id[0]))
-        end
-    else
-        log.info("没有检测到DS18B20")
-    end
-end
-
-local switchover_pin = gpio.PWR_KEY --选择powerkey作为切换18B20读数的信号脚
-
-gpio.debounce(switchover_pin, 100) --设置防抖
-
-
---设置powerkey按下和抬起的功能(18B20温度的取值再54pin和56pin之间切换)
---如果客户需要其他pin(22/54/56/78)则改动switchover_pin为用户需要的即可
---注意,由于powerkey不能做单边沿触发,所以一次按下和抬起的动作,会触发两次切换pin
---看效果的话,可以先一直按住powerkey几秒,再松开
-gpio.setup(switchover_pin, function()
-    log.info("Touch_pin", switchover_pin, "被触发")
-    log.info("当前单总线pad", onewire_pin)
-    if onewire_pin == 54 then
-        log.info("给PAD" .. onewire_pin .. "配置到GPIO3上去", pins.setup(onewire_pin, "GPIO3"))
-        log.info("给GPIO3设置为高电平输出模式",gpio.setup(3,1))--一定要执行gpio.setup,至于是哪种模式,用户根据自身需求
-        onewire_pin = 56
-    else
-        log.info("给PAD" .. onewire_pin .. "配置到GPIO7上去", pins.setup(onewire_pin, "GPIO7"))
-        log.info("给GPIO7设置为高电平输出模式",gpio.setup(7,1))
-
-        onewire_pin = 54
-    end
-    log.info("设置后单总线pad", onewire_pin)
-
-    onewire.deinit(0) -- 切换的时候一定要先关闭单总线,在读取的时候重新初始化
-
-    log.info("给" .. onewire_pin .. "配置到onewire上去", pins.setup(onewire_pin, "ONEWIRE"))
-    sys.publish("powerkey被按下")
-end, gpio.PULLUP, gpio.RISING)
-
-sys.taskInit(function()
-    while 1 do
-        -- sys.waitUntil("powerkey被按下")
-        log.info("1S后读取"..onewire_pin.."脚上的18B20")
-        sys.wait(1000)
-        log.info("开始读取18B20")
-        test_ds18b20()
-    end
-end)
-
-
-
-

+ 367 - 0
module/Air780EPM/demo/onewire/onewire_multi_app.lua

@@ -0,0 +1,367 @@
+--[[
+@module  onewire_multi_app
+@summary OneWire多DS18B20温度传感器应用演示模块(54和23切换版本)
+@version 1.0.0
+@date    2025.11.25
+@author  王棚嶙
+@usage
+本模块演示多DS18B20温度传感器的完整功能:
+1. 双传感器切换控制(引脚54和23)
+2. 电源管理(GPIO控制)
+3. 按键切换传感器
+4. 双路温度同时监测
+5. 使用引脚复用功能(pins.setup)
+]]
+
+log.info("onewire_multi_app", "多传感器模块版本: 1.0.0")
+
+-- 设置所有GPIO引脚电压为3.3V,确保DS18B20传感器正常供电
+pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
+
+-- 和GPIO31控制传感器电源使能,确保DS18B20供电正常
+gpio.setup(31, 1)
+
+-- 硬件配置(双设备模式:支持引脚54和23切换)
+local onewire_pin = 54
+local switchover_pin = gpio.PWR_KEY
+
+-- DS18B20命令定义
+local CMD_CONVERT_T = 0x44
+local CMD_READ_SCRATCHPAD = 0xBE
+local CMD_READ_ROM = 0x33
+
+-- 全局状态变量
+local pwr_key_pressed = false
+
+-- PWR_KEY按键中断处理函数
+-- 功能:处理引脚切换按键事件,设置标志位供主循环查询
+local function handle_pwr_key_interrupt()
+    pwr_key_pressed = true
+    log.info("onewire_multi_app", "切换按键被按下")
+end
+
+-- 初始化硬件配置
+local function init_hardware()
+    log.info("onewire_multi_app", "初始化硬件配置...")
+    
+    -- 配置PWR_KEY按键,使用上升沿触发并添加防抖
+    gpio.debounce(switchover_pin, 100)
+    gpio.setup(switchover_pin, handle_pwr_key_interrupt, gpio.PULLUP, gpio.RISING)
+    
+    -- 初始配置当前引脚为ONEWIRE功能
+    pins.setup(onewire_pin, "ONEWIRE")
+    
+    log.info("onewire_multi_app", "硬件初始化完成")
+    log.info("onewire_multi_app", "初始引脚: 引脚" .. onewire_pin .. " (ONEWIRE功能)")
+    log.info("onewire_multi_app", "切换按键: PWR_KEY")
+    log.info("onewire_multi_app", "支持引脚: 54 和 23 循环切换")
+    log.info("onewire_multi_app", "电源控制: GPIO31/GPIO2 (已设置为高电平)")
+    
+    return true
+end
+
+
+-- 时序要求:DS18B20上电后需要稳定时间,100ms延时确保电源稳定
+-- 技术背景:DS18B20在电源切换后需要tREC(恢复时间)完成内部初始化
+-- 实际测试:无延时可能导致设备检测失败或温度读取异常
+-- 建议值:最小50ms,推荐100ms以确保可靠性
+local function power_stabilization_delay()
+    log.info("onewire_multi_app", "电源稳定延时(确保DS18B20内部电路就绪)")
+    sys.wait(100)  -- DS18B20 tREC恢复时间,最小50ms,推荐100ms
+end
+
+-- 单总线分时使用引脚切换(同一条总线,分时复用)
+-- 核心逻辑:使用GPIO54和GPIO23两个引脚连接同一条OneWire总线,实现分时复用
+-- 应用场景:当需要在同一总线上分时访问不同设备时使用
+-- 技术原理:通过切换总线连接引脚,实现同一条物理总线的分时使用
+-- 切换效果:
+-- - GPIO54:当前时间段连接设备A(ROM ID: 28-9F-C4-93-00-00-00-14)
+-- - GPIO23:切换到时间段连接设备B(ROM ID: 28-59-F2-53-00-00-00-14)
+-- 注意:这不是多总线并行,而是单总线的分时复用策略
+local function switch_onewire_pin()
+    log.info("onewire_multi_app", "切换OneWire引脚...")
+    
+    -- 关闭当前OneWire总线
+    onewire.deinit(0)
+    
+    
+    -- 分时复用切换逻辑
+    -- 技术原理:将当前不使用的引脚配置为GPIO功能并输出高电平
+    -- 目的:确保非活动设备处于高阻态,避免干扰当前连接的设备
+    -- 电气特性:GPIO设置为开漏输出模式,高电平由上拉电阻提供
+    if onewire_pin == 54 then
+        -- 从54切换到23
+        -- 将PIN54配置为GPIO3功能,不再作为OneWire使用
+        log.info("onewire_multi_app", "将PIN54配置为GPIO3", pins.setup(54, "GPIO3"))
+        -- 设置GPIO3为高电平输出(开漏模式,高电平由上拉电阻提供)
+        log.info("onewire_multi_app", "将GPIO3设置为高电平输出(OneWire总线空闲状态)", gpio.setup(3, 1))
+        onewire_pin = 23
+        log.info("onewire_multi_app", "切换到引脚23")
+    else
+        -- 从23切换到54
+        -- 将PIN23配置为GPIO2功能,不再作为OneWire使用
+        log.info("onewire_multi_app", "将PIN23配置为GPIO2", pins.setup(23, "GPIO2"))
+        -- 设置GPIO2为高电平输出(开漏模式,高电平由上拉电阻提供)
+        log.info("onewire_multi_app", "将GPIO2设置为高电平输出(OneWire总线空闲状态)", gpio.setup(2, 1))
+        onewire_pin = 54
+        log.info("onewire_multi_app", "切换到引脚54")
+    end
+    
+    log.info("onewire_multi_app", "当前使用引脚:", onewire_pin)
+    
+    -- 配置新引脚为ONEWIRE功能
+    -- 分时复用原理:将选中的引脚配置为OneWire功能,连接到对应设备
+    -- 连接过程:先断开之前的设备连接,再连接新的设备
+    -- 电气特性:确保当前连接的设备具有完整的OneWire通信能力
+    log.info("onewire_multi_app", "将引脚" .. onewire_pin .. "配置为ONEWIRE功能", pins.setup(onewire_pin, "ONEWIRE"))
+    
+    log.info("onewire_multi_app", "引脚切换完成,当前使用: 引脚" .. onewire_pin)
+end
+
+-- 初始化OneWire总线
+local function init_onewire_bus()
+    log.info("onewire_multi_app", "初始化OneWire总线,通道: 0")
+    
+    -- 配置当前引脚
+    pins.setup(onewire_pin, "ONEWIRE")
+    
+    -- 初始化OneWire总线
+    onewire.init(0)
+    
+    -- 配置DS18B20标准时序参数
+    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
+    
+    log.info("onewire_multi_app", "OneWire总线初始化完成,通道: 0,引脚:" .. onewire_pin)
+    
+    return true
+end
+
+-- 检测DS18B20设备是否存在(分时复用场景)
+-- 分时逻辑:在当前连接的引脚上发送复位脉冲,检测该设备响应
+-- 单总线场景:只有当前连接的引脚上的设备会响应复位脉冲
+-- 返回值:true表示当前引脚连接的设备响应,false表示无设备响应
+local function detect_ds18b20_device()
+    log.info("onewire_multi_app", "检测DS18B20设备,引脚: " .. onewire_pin)
+    
+    -- 发送复位脉冲并检测设备
+    local present = onewire.reset(0, true)
+    
+    if present then
+        log.info("onewire_multi_app", "检测到DS18B20设备响应")
+        return true
+    else
+        log.warn("onewire_multi_app", "未检测到DS18B20设备响应")
+        return false
+    end
+end
+
+-- 读取DS18B20温度(单总线分时复用)
+-- 核心流程:读ROM ID → 选设备 → 温度转换 → 读数据 → CRC校验
+local function read_ds18b20_temperature()
+    log.info("onewire_multi_app", "开始读取DS18B20温度,引脚: " .. onewire_pin)
+    
+    local tbuff = zbuff.create(10)
+    local succ, crc8c, range, t
+    local rbuff = zbuff.create(9)
+    
+    -- 读取设备ROM ID(每个设备唯一)
+    log.info("onewire_multi_app", "读取设备ROM ID(64位唯一标识)")
+    
+    local id = zbuff.create(8)
+    id:set()
+    
+    succ, rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "读取ROM ID失败")
+        return nil
+    end
+    
+    -- 检查设备类型码(DS18B20应为0x28)
+    if id[0] ~= 0x28 then
+        log.warn("onewire_multi_app", "非DS18B20设备,类型码:", mcu.x32(id[0]))
+        return nil
+    end
+    
+    -- CRC校验设备ID
+    crc8c = crypto.crc8(id:query(0, 7), 0x31, 0, true)
+    if crc8c ~= id[7] then
+        log.warn("onewire_multi_app", "ROM ID CRC校验不对", 
+                "计算值:", mcu.x32(crc8c), "期望值:", mcu.x32(id[7]))
+        log.info("onewire_multi_app", "完整ROM ID:", id:query(0, 7):toHex())
+        return nil
+    end
+    
+    log.info("onewire_multi_app", "ROM ID校验成功:", id:query(0, 7):toHex())
+    
+    -- 通过MATCH ROM选择设备(确保只选中目标设备)
+    log.info("onewire_multi_app", "开始温度转换(通过ROM匹配选择设备)")
+    
+    -- 构建命令缓冲区:MATCH ROM(0x55) + 目标设备ROM ID + 温度转换命令(0x44)
+    -- 0x55是MATCH ROM命令,后面必须跟64位目标设备的ROM ID
+    tbuff:write(0x55)     -- MATCH ROM命令
+    tbuff:copy(nil, id)  -- 复制64位ROM ID(确保选择正确的设备)
+    tbuff:write(0xb8)
+    tbuff[tbuff:used() - 1] = 0x44  -- CONVERT T温度转换命令
+    
+    succ = onewire.tx(0, tbuff, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "发送温度转换命令失败")
+        return nil
+    end
+
+    -- 第三步:等待转换完成
+    log.info("onewire_multi_app", "等待温度转换完成")
+    
+    -- 等待一段时间让转换完成
+    sys.wait(750)
+    
+    -- 发送复位脉冲检查设备
+    succ = onewire.reset(0, true)
+    if not succ then
+        log.warn("onewire_multi_app", "等待转换完成时设备未响应")
+        return nil
+    end
+    
+    -- 检查转换是否完成
+    if onewire.bit(0) > 0 then
+        log.info("onewire_multi_app", "温度转换完成")
+    end
+    
+    -- 第四步:读取温度数据
+    log.info("onewire_multi_app", "读取温度数据")
+    
+    -- 构建读取命令:匹配ROM(0x55) + ROM ID + 读取暂存器命令(0xBE)
+    tbuff[tbuff:used() - 1] = 0xbe
+    succ = onewire.tx(0, tbuff, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "发送读取命令失败")
+        return nil
+    end
+    
+    -- 接收9字节温度数据
+    succ, rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
+    if not succ then
+        log.warn("onewire_multi_app", "温度数据接收失败")
+        return nil
+    end
+    
+    -- 第五步:CRC校验和温度计算
+    log.info("onewire_multi_app", "CRC校验和温度计算")
+    
+    -- CRC校验
+    crc8c = crypto.crc8(rbuff:toStr(0, 8), 0x31, 0, true)
+    if crc8c == rbuff[8] then
+        -- 计算温度值
+        range = (rbuff[4] >> 5) & 0x03
+        t = rbuff:query(0, 2, false, true)
+        t = t * (5000 >> range)
+        t = t / 10000
+        
+        -- 范围检查
+        if t >= -55.0 and t <= 125.0 then
+            log.info("onewire_multi_app", "温度读取成功:", string.format("%.2f°C", t))
+            return t
+        else
+            log.warn("onewire_multi_app", "温度值超出有效范围:", t)
+            return nil
+        end
+    else
+        log.warn("onewire_multi_app", "温度数据CRC校验不对", 
+                "计算值:", mcu.x32(crc8c), "期望值:", mcu.x32(rbuff[8]))
+        return nil
+    end
+end
+
+-- 简化版温度读取(用于快速测试)
+local function quick_read_ds18b20()
+    log.info("onewire_multi_app", "快速读取温度,引脚: " .. onewire_pin)
+    
+    -- 首先检测设备是否存在
+    if not detect_ds18b20_device() then
+        return nil
+    end
+    
+    -- 使用完整读取函数
+    return read_ds18b20_temperature()
+end
+
+-- 单总线分时复用主函数(同一条总线,分时访问不同设备)
+local function multi_sensor_app_main()
+    log.info("onewire_multi_app", "启动双传感器应用(引脚54和23)")
+    
+    -- 初始化硬件
+    if not init_hardware() then
+        log.error("onewire_multi_app", "硬件初始化失败,任务无法启动")
+        return
+    end
+    
+    -- 初始化OneWire总线
+    init_onewire_bus()
+    
+    -- 电源稳定延时:确保DS18B20内部电路就绪
+    power_stabilization_delay()
+    
+    -- 检测设备
+    local device_present = detect_ds18b20_device()
+    
+    if not device_present then
+        log.error("onewire_multi_app", "未检测到设备响应")
+        log.warn("onewire_multi_app", "硬件连接提示:")
+        log.warn("onewire_multi_app", "1. 传感器连接引脚54或23")
+        log.warn("onewire_multi_app", "2. 确保GPIO31/GPIO2已设置为高电平供电")
+        log.warn("onewire_multi_app", "3. 确保4.7kΩ上拉电阻正确安装")
+        log.warn("onewire_multi_app", "4. 检查传感器VDD、GND、DQ连接")
+        -- 关闭OneWire总线
+        onewire.deinit(0)
+        return
+    end
+    
+    log.info("onewire_multi_app", "开始双传感器连续监测...")
+    log.info("onewire_multi_app", "按PWR_KEY按键可切换引脚(54和23)")
+    
+  -- 主循环:按键切换设备,分时读取温度
+    local read_count = 0
+    local success_count = 0
+    
+    while true do
+        read_count = read_count + 1
+        
+        -- 检查按键状态
+        if pwr_key_pressed then
+            pwr_key_pressed = false
+            switch_onewire_pin()
+            
+            -- 重新初始化OneWire总线
+            init_onewire_bus()
+        end
+        
+        log.info("onewire_multi_app", "第" .. read_count .. "次读取,引脚:" .. onewire_pin)
+        
+        -- 尝试读取温度
+        local temperature = read_ds18b20_temperature()
+        
+        if temperature then
+            success_count = success_count + 1
+            log.info("onewire_multi_app", "引脚" .. onewire_pin .. "温度:", 
+                    string.format("%.2f°C", temperature), 
+                    "成功率:", string.format("%.1f%%", success_count/read_count*100))
+            
+            -- 简单的温度报警逻辑
+            if temperature > 30 then
+                log.warn("onewire_multi_app", "温度偏高:", string.format("%.2f°C", temperature))
+            elseif temperature < 10 then
+                log.warn("onewire_multi_app", "温度偏低:", string.format("%.2f°C", temperature))
+            end
+        else
+            log.warn("onewire_multi_app", "本次读取失败")
+            log.info("onewire_multi_app", "成功率:", string.format("%.1f%%", success_count/read_count*100))
+        end
+        
+        -- 等待下一次读取
+        sys.wait(2000)
+    end
+end
+log.info("onewire_multi_app", "双传感器应用模块加载完成(54和23切换)")
+-- 启动多传感器应用任务
+sys.taskInit(multi_sensor_app_main)
+

+ 0 - 107
module/Air780EPM/demo/onewire/onewire_single_18b20/main.lua

@@ -1,107 +0,0 @@
-PROJECT = "onewiredemo"
-VERSION = "1.0.0"
-sys = require("sys")
-log.style(1)
-
---[[
-接线说明:
-   DS18B20    Air780EPM
-1. GND    -> GND
-2. VDD    -> 3.3V
-3. DATA    -> GPIO2
-
-注意:
-1. 3.3v在老版本的开发板上没有引脚, 所以需要外接, 一定要确保共地
-2. ONEWIRE功能支持在4个引脚使用, 但硬件通道只有一个, 默认是GPIO2
-3. 如需切换到其他脚, 参考上一级目录下的onewire_multi_18b20_swich_read
-]]
-
-local function read_ds18b20(id)
-    local tbuff = zbuff.create(10)
-    local succ,crc8c,range,t
-    local rbuff = zbuff.create(9)
-    --如果有多个DS18B20,需要带上ID
-    tbuff:write(0x55)
-    tbuff:copy(nil, id)
-    tbuff:write(0xb8)
-    --如果只有1个DS18B20,就用无ID方式
-    --tbuff:write(0xcc,0xb8)
-    while true do
-        tbuff[tbuff:used() - 1] = 0x44
-        succ = onewire.tx(0, tbuff, false, true, true)
-        if not succ then
-            return
-        end
-        while true do
-            succ = onewire.reset(0, true)
-            if not succ then
-                return
-            end
-            if onewire.bit(0) > 0 then
-                log.info("温度转换完成")
-                break
-            end
-            sys.wait(10)
-        end
-        tbuff[tbuff:used() - 1] = 0xbe
-        succ = onewire.tx(0, tbuff, false, true, true)
-        if not succ then
-            return
-        end
-        succ,rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
-        crc8c = crypto.crc8(rbuff:toStr(0,8), 0x31, 0, true)
-        if crc8c == rbuff[8] then
-            range = (rbuff[4] >> 5) & 0x03
-            -- rbuff[0] = 0xF8
-            -- rbuff[1] = 0xFF
-            t = rbuff:query(0,2,false,true)
-            t = t * (5000 >> range)
-            t = t / 10000
-            log.info(t)
-        else
-            log.info("RAM DATA CRC校验不对",  mcu.x32(crc8c), mcu.x32(rbuff[8]))
-            return
-        end
-        sys.wait(500)
-    end
-end
-
-local function test_ds18b20()
-    local succ,rx_data
-    local id = zbuff.create(8)
-
-    local crc8c
-    onewire.init(0)
-    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
-    while true do
-        id:set() --清空id
-        succ,rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
-        if succ then
-            if id[0] == 0x28 then
-                crc8c = crypto.crc8(id:query(0,7), 0x31, 0, true)
-                if crc8c == id[7] then
-                    log.info("探测到DS18B20", id:query(0, 7):toHex())
-                    read_ds18b20(id)
-                    log.info("DS18B20离线,重新探测")
-                else
-                    log.info("ROM ID CRC校验不对",  mcu.x32(crc8c), mcu.x32(id[7]))
-                end
-            else
-                log.info("ROM ID不正确", mcu.x32(id[0]))
-            end
-        end
-        log.info("没有检测到DS18B20, 5秒后重试")
-        sys.wait(5000)
-
-    end
-    
-
-end
-
-if onewire then
-    sys.taskInit(test_ds18b20)
-else
-    log.info("no onewire")
-end
-
-sys.run()

+ 203 - 0
module/Air780EPM/demo/onewire/onewire_single_app.lua

@@ -0,0 +1,203 @@
+--[[
+@module  onewire_single_app
+@summary OneWire单DS18B20温度传感器应用演示模块(GPIO2默认模式)
+@version 1.0.0
+@date    2025.11.25
+@author  王棚嶙
+@usage
+本模块演示单DS18B20温度传感器的完整功能:
+1. 使用GPIO2默认OneWire功能
+2. 硬件通道0模式,无需引脚复用
+3. 优化的时序参数和错误处理
+4. 连续温度监测
+5. 完整的OneWire API接口演示
+]]
+
+
+log.info("onewire_single_app", "单传感器模块版本:1.0.0")
+
+-- 设置所有GPIO引脚电压为3.3V,确保DS18B20传感器正常供电
+pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
+
+-- DS18B20命令定义
+local CMD_CONVERT_T = 0x44
+local CMD_READ_SCRATCHPAD = 0xBE
+local CMD_SKIP_ROM = 0xCC
+local CMD_READ_ROM = 0x33
+
+-- 单传感器应用主函数
+local function single_sensor_app_main()
+    log.info("onewire_single_app", "启动单传感器应用")
+    
+    -- 初始化OneWire总线(使用硬件通道0模式)
+    log.info("onewire_single_app", "初始化OneWire总线...")
+    onewire.init(0)
+    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
+    log.info("onewire_single_app", "OneWire总线初始化完成,使用GPIO2默认引脚")
+    
+    
+    
+    -- 检测DS18B20设备
+    log.info("onewire_single_app", "检测DS18B20设备...")
+    
+    local succ, rx_data
+    local id = zbuff.create(8)
+    local crc8c
+    
+    -- 清空ID缓冲区
+    id:set()
+    
+    -- 读取设备ROM ID(使用手动配置的引脚)
+    succ, rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
+    
+    local detected = false
+    local device_id = nil
+    
+    if succ then
+        -- 检查家族码(DS18B20为0x28)
+        if id[0] == 0x28 then
+            -- CRC校验
+            crc8c = crypto.crc8(id:query(0,7), 0x31, 0, true)
+            if crc8c == id[7] then
+                log.info("onewire_single_app", "探测到DS18B20", id:query(0, 7):toHex())
+                detected = true
+                device_id = id
+            else
+                log.warn("onewire_single_app", "ROM ID CRC校验不对", mcu.x32(crc8c), mcu.x32(id[7]))
+            end
+        else
+            log.warn("onewire_single_app", "ROM ID不正确", mcu.x32(id[0]))
+        end
+    else
+        log.warn("onewire_single_app", "未检测到DS18B20设备,请检查硬件连接")
+        log.info("onewire_single_app", "硬件连接提示:")
+        log.info("onewire_single_app", "1. DS18B20 DATA引脚 -> GPIO2 (默认OneWire功能)")
+        log.info("onewire_single_app", "2. 确保上拉电阻4.7kΩ连接DATA到3.3V")
+        log.info("onewire_single_app", "3. 使用硬件通道0模式,无需引脚复用配置")
+    end
+    
+    if not detected then
+        log.warn("onewire_single_app", "设备检测失败,任务无法启动")
+        log.info("onewire_single_app", "单传感器应用启动完成")
+        onewire.deinit(0)
+        return
+    end
+    
+    log.info("onewire_single_app", "开始连续温度监测...")
+    
+    -- 读取DS18B20温度数据(单总线单设备模式)
+    -- 与多传感器模式的对比:
+    -- - 单传感器:使用SKIP ROM(0xCC)直接通信,无需ROM ID
+    -- - 多传感器:使用MATCH ROM(0x55)选择设备,需要目标ROM ID
+    -- 
+    -- 单设备读取流程:
+    -- 1. SKIP ROM:发送0xCC命令,跳过ROM ID识别
+    -- 2. 温度转换:发送CONVERT T(0x44)启动温度转换
+    -- 3. 读取数据:发送READ SCRATCHPAD(0xBE)读取温度数据
+    -- 4. CRC校验:验证数据完整性
+    -- 
+    -- 优势:通信简单高效,无需设备寻址
+    -- 限制:只能用于总线上只有一个设备的场景
+    local function read_temperature(dev_id)
+        local tbuff = zbuff.create(10)
+        local rbuff = zbuff.create(9)
+        local succ, crc8c, range, t
+        
+        -- 发送SKIP ROM命令(0xCC) - 跳过ROM识别,直接与设备通信
+        -- 工作原理:所有设备都会响应SKIP ROM命令,无需发送64位ROM ID
+        -- 适用场景:总线上只有一个设备,无需设备寻址和选择
+        -- 优势:通信效率高,无需传输ROM ID,简化通信流程
+        -- 风险:如果总线上有多个设备,所有设备会同时响应,造成冲突
+        tbuff:write(0xcc)
+        
+        -- 发送温度转换命令
+        tbuff[tbuff:used() - 1] = 0x44
+        succ = onewire.tx(0, tbuff, false, true, true)
+        if not succ then
+            log.warn("onewire_single_app", "发送温度转换命令失败")
+            return nil
+        end
+        
+        -- 等待转换完成(使用位检测)
+        local conversion_complete = false
+        local max_wait = 100
+        local wait_count = 0
+        
+        while wait_count < max_wait do
+            succ = onewire.reset(0, true)
+            if not succ then
+                log.warn("onewire_single_app", "等待转换完成时设备未响应")
+                return nil
+            end
+            if onewire.bit(0) > 0 then
+                log.info("onewire_single_app", "温度转换完成")
+                conversion_complete = true
+                break
+            end
+            sys.wait(10)
+            wait_count = wait_count + 1
+        end
+        
+        if not conversion_complete then
+            log.warn("onewire_single_app", "温度转换超时")
+            return nil
+        end
+        
+        -- 读取温度数据
+        tbuff[tbuff:used() - 1] = 0xBE
+        succ = onewire.tx(0, tbuff, false, true, true)
+        if not succ then
+            log.warn("onewire_single_app", "发送读取命令失败")
+            return nil
+        end
+        
+        succ, rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
+        if not succ or rbuff:used() ~= 9 then
+            log.warn("onewire_single_app", "温度数据读取失败")
+            return nil
+        end
+        
+        -- CRC校验
+        crc8c = crypto.crc8(rbuff:toStr(0,8), 0x31, 0, true)
+        if crc8c == rbuff[8] then
+            range = (rbuff[4] >> 5) & 0x03
+            t = rbuff:query(0,2,false,true)
+            t = t * (5000 >> range)
+            t = t / 10000
+            log.info("onewire_single_app", "温度读取成功:", string.format("%.2f°C", t))
+            return t
+        else
+            log.warn("onewire_single_app", "RAM DATA CRC校验不对", mcu.x32(crc8c), mcu.x32(rbuff[8]))
+            return nil
+        end
+    end
+    
+    -- 主循环 - 连续温度监测
+    while true do
+        local temperature = read_temperature(device_id)
+        
+        if temperature then
+            -- 简单的温度报警逻辑(示例)
+            if temperature > 30 then
+                log.warn("onewire_single_app", "温度偏高:", string.format("%.2f°C", temperature))
+            elseif temperature < 10 then
+                log.warn("onewire_single_app", "温度偏低:", string.format("%.2f°C", temperature))
+            else
+                log.info("onewire_single_app", "温度正常:", string.format("%.2f°C", temperature))
+            end
+        else
+            log.warn("onewire_single_app", "本次读取失败,继续下一次")
+        end
+        
+        -- 等待下一次读取
+        sys.wait(3000)
+    end
+    
+    log.info("onewire_single_app", "单传感器连续读取任务结束")
+    log.info("onewire_single_app", "单传感器应用启动完成")
+end
+
+log.info("onewire_single_app", "单传感器应用模块加载完成")
+
+-- 启动单传感器应用任务
+sys.taskInit(single_sensor_app_main)

+ 212 - 0
module/Air780EPM/demo/onewire/readme.md

@@ -0,0 +1,212 @@
+> 王棚嶙
+
+# OneWire综合演示项目
+
+## 功能模块介绍
+
+本demo演示了完整的DS18B20温度传感器OneWire单总线协议实现。项目采用模块化架构,分别实现单传感器和多传感器应用场景。
+
+1、main.lua:主程序入口 <br> 
+2、onewire_single_app.lua:演示单传感器功能模块(GPIO2默认OneWire功能,硬件通道0模式,3秒间隔连续监测)<br> 
+3、onewire_multi_app.lua:演示多传感器功能模块(引脚54/23切换,PWR_KEY按键控制,2秒间隔双路监测)<br> 
+
+## 演示功能概述
+
+###  主程序入口模块 (main.lua)
+
+- 初始化项目信息和版本号
+- 初始化看门狗,并定时喂狗
+- 启动一个循环定时器,每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
+- 加载onewire_single_app模块(通过require "onewire_single_app")
+- 加载onewire_multi_app模块(通过require "onewire_multi_app")
+
+### 单传感器模式 (onewire_single_app.lua)
+- 使用GPIO2默认OneWire功能,硬件通道0模式,无需引脚复用
+- 完整的CRC8数据校验机制,确保数据可靠性
+- 设备自动识别和ROM验证,支持设备类型检测
+- 3秒间隔连续温度监测,实时温度报警功能
+- zbuff缓冲区优化,提高数据传输效率
+
+
+### 多传感器模式 (onewire_multi_app.lua - 单总线多设备演示)
+
+
+**单总线多设备挂载原理**:
+1. **物理连接**:所有DS18B20的VDD、GND、DQ引脚分别并联到同一组单总线
+2. **设备识别**:每个DS18B20出厂时烧录了全球唯一的64位ROM ID
+3. **总线扫描**:主机发送SEARCH ROM(0xF0)命令发现总线上的所有设备
+4. **设备选择**:通过MATCH ROM(0x55)命令+目标设备ROM ID选择特定设备通信
+5. **分时操作**:每次只与一个设备通信,避免总线冲突
+
+**分时复用测试逻辑**(按键切换设备):
+- **引脚54**:连接设备A(ROM ID: 28-9F-C4-93-00-00-00-14)
+- **引脚23**:连接设备B(ROM ID: 28-59-F2-53-00-00-00-14)
+- **PWR_KEY按键**:按一次切换一个设备,实现同一条总线的分时使用
+
+**核心测试流程**:
+1. 初始化当前引脚的OneWire总线
+2. 发送SEARCH ROM命令扫描总线上的设备
+3. 读取并验证设备的64位ROM ID(家族码+序列号+CRC)
+4. 使用MATCH ROM(0x55)命令选择目标设备
+5. 发送温度转换命令(0x44)并等待完成
+6. 读取温度数据并进行CRC校验
+7. 输出设备ROM ID、温度值、读取成功率
+
+
+
+## 演示硬件环境
+1、Air780EPM核心板一块
+
+2、TYPE-C USB数据线一根
+
+3、ds18b20传感器两个
+
+4、Air780EPM核心板和数据线的硬件接线方式为
+
+- Air780EPM核心板通过TYPE-C USB口供电;(核心板USB旁边的开关拨到on一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+5、Air780EPM核心板和ds18b20传感器接线方式
+
+### 单传感器连接
+
+|   Air780EPM核心板     |    DS18B20传感器    |
+| --------------- | -------------------|
+|    VDD_EXT         |         VCC        |
+|    23/GPIO2     |         DQ         |
+|    GND          |         GND        |
+
+连接图:
+
+![image](https://docs.openluat.com/air780epm/luatos/app/driver/onewire/image/ce9d3b2c9b3a36c22388d710913668a7.jpg)
+
+### 多传感器连接
+
+
+|   Air780EPM核心板     |    DS18B20传感器1    |
+| --------------- | -------------------|
+|    VDD_EXT         |         VCC        |
+|    23/GPIO2     |         DQ         |
+|    任意GND          |         GND        |
+
+|   Air780EPM核心板     |    DS18B20传感器2    |
+| --------------- | -------------------|
+|    32/GPIO31         |         VCC        |
+|    54/CAM_MCLK     |         DQ         |
+|    任意GND          |         GND        |
+
+连接图:
+
+![image](https://docs.openluat.com/air780epm/luatos/app/driver/onewire/image/1957f5eb447654ec31894efb589e809b.jpg)
+
+## 演示软件环境
+
+1、Luatools下载调试工具: https://docs.openluat.com/air780epm/common/Luatools/
+
+2、内核固件版本:
+Air780EPM:https://docs.openluat.com/air780epm/luatos/firmware/version/
+
+## 演示核心步骤
+1、搭建好硬件环境
+
+2、通过Luatools将demo与固件烧录到核心板或开发板中
+
+3、烧录好后,板子开机将会在Luatools上看到如下打印:
+
+```lua
+(1)单传感器演示
+[2025-11-24 23:46:59.904][000000000.251] I/user.main onewire_demo 1.0.0
+[2025-11-24 23:46:59.932][000000000.258] I/user.onewire_single_app 单传感器模块版本: 002.002.000
+[2025-11-24 23:46:59.970][000000000.258] I/user.onewire_single_app 单传感器应用模块加载完成
+[2025-11-24 23:47:00.002][000000000.259] I/user.onewire_single_app 启动单传感器应用
+[2025-11-24 23:47:00.032][000000000.259] I/user.onewire_single_app 初始化OneWire总线...
+[2025-11-24 23:47:00.064][000000000.259] I/user.onewire_single_app OneWire总线初始化完成,使用GPIO2默认引脚
+[2025-11-24 23:47:00.096][000000000.259] I/user.onewire_single_app 检测DS18B20设备...
+[2025-11-24 23:47:00.124][000000000.267] I/user.onewire_single_app 探测到DS18B20 2859F253000000 14
+[2025-11-24 23:47:00.159][000000000.267] I/user.onewire_single_app 开始连续温度监测...
+[2025-11-24 23:47:00.186][000000000.276] I/user.onewire_single_app 温度转换完成
+[2025-11-24 23:47:00.216][000000000.289] I/user.onewire_single_app 温度读取成功: 85.00°C
+[2025-11-24 23:47:00.246][000000000.290] W/user.onewire_single_app 温度偏高: 85.00°C
+[2025-11-24 23:47:00.409][000000003.299] I/user.onewire_single_app 温度转换完成
+[2025-11-24 23:47:00.436][000000003.312] I/user.onewire_single_app 温度读取成功: 28.25°C
+[2025-11-24 23:47:00.465][000000003.313] I/user.onewire_single_app 温度正常: 28.25°C
+[2025-11-24 23:47:03.404][000000006.322] I/user.onewire_single_app 温度转换完成
+[2025-11-24 23:47:03.437][000000006.335] I/user.onewire_single_app 温度读取成功: 28.25°C
+[2025-11-24 23:47:03.469][000000006.335] I/user.onewire_single_app 温度正常: 28.25°C
+
+(2)双传感器演示
+[2025-11-24 23:49:45.699][000000000.260] I/user.onewire_multi_app 双传感器应用模块加载完成(54和23切换)
+[2025-11-24 23:49:45.732][000000000.261] I/user.onewire_multi_app 启动双传感器应用(引脚54和23)
+[2025-11-24 23:49:45.761][000000000.261] I/user.onewire_multi_app 初始化硬件配置...
+[2025-11-24 23:49:45.789][000000000.261] I/user.onewire_multi_app 硬件初始化完成
+[2025-11-24 23:49:45.822][000000000.262] I/user.onewire_multi_app 初始引脚: 引脚54 (ONEWIRE功能)
+[2025-11-24 23:49:45.859][000000000.262] I/user.onewire_multi_app 切换按键: PWR_KEY
+[2025-11-24 23:49:45.898][000000000.262] I/user.onewire_multi_app 支持引脚: 54 和 23 循环切换
+[2025-11-24 23:49:45.932][000000000.262] I/user.onewire_multi_app 电源控制: GPIO31/GPIO2 (已设置为高电平)
+[2025-11-24 23:49:45.963][000000000.262] I/user.onewire_multi_app 电源控制: 开启
+[2025-11-24 23:49:45.997][000000000.363] I/user.onewire_multi_app 初始化OneWire总线,通道: 0
+[2025-11-24 23:49:46.026][000000000.374] I/user.onewire_multi_app OneWire总线初始化完成,通道: 0,引脚:54
+[2025-11-24 23:49:46.059][000000000.573] I/user.onewire_multi_app 检测DS18B20设备,引脚: 54
+[2025-11-24 23:49:46.089][000000000.575] I/user.onewire_multi_app 检测到DS18B20设备响应
+[2025-11-24 23:49:46.117][000000000.575] I/user.onewire_multi_app 开始双传感器连续监测...
+[2025-11-24 23:49:46.148][000000000.575] I/user.onewire_multi_app 按PWR_KEY按键可切换引脚(54和23)
+[2025-11-24 23:49:46.181][000000000.575] I/user.onewire_multi_app 第1次读取,引脚:54
+[2025-11-24 23:49:46.212][000000000.575] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 54
+[2025-11-24 23:49:46.243][000000000.576] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-24 23:49:46.272][000000000.583] I/user.onewire_multi_app ROM ID校验成功: 289FC493000000 14
+[2025-11-24 23:49:46.297][000000000.583] I/user.onewire_multi_app 开始温度转换
+[2025-11-24 23:49:46.325][000000000.590] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-24 23:49:46.401][000000001.341] I/user.onewire_multi_app 温度转换完成
+[2025-11-24 23:49:46.433][000000001.342] I/user.onewire_multi_app 读取温度数据
+[2025-11-24 23:49:46.466][000000001.354] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-24 23:49:46.499][000000001.355] I/user.onewire_multi_app 温度读取成功: 27.44°C
+[2025-11-24 23:49:46.530][000000001.355] I/user.onewire_multi_app 引脚54温度: 27.44°C 成功率: 100.0%
+[2025-11-24 23:49:46.630][000000003.355] I/user.onewire_multi_app 第2次读取,引脚:54
+[2025-11-24 23:49:46.665][000000003.356] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 54
+[2025-11-24 23:49:46.698][000000003.356] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-24 23:49:46.727][000000003.363] I/user.onewire_multi_app ROM ID校验成功: 289FC493000000 14
+[2025-11-24 23:49:46.765][000000003.363] I/user.onewire_multi_app 开始温度转换
+[2025-11-24 23:49:46.799][000000003.371] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-24 23:49:46.982][000000004.122] I/user.onewire_multi_app 温度转换完成
+[2025-11-24 23:49:47.010][000000004.123] I/user.onewire_multi_app 读取温度数据
+[2025-11-24 23:49:47.043][000000004.135] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-24 23:49:47.077][000000004.136] I/user.onewire_multi_app 温度读取成功: 27.50°C
+[2025-11-24 23:49:47.110][000000004.137] I/user.onewire_multi_app 引脚54温度: 27.50°C 成功率: 100.0%
+[2025-11-24 23:49:48.999][000000006.137] I/user.onewire_multi_app 第3次读取,引脚:54
+[2025-11-24 23:49:49.030][000000006.138] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 54
+[2025-11-24 23:49:49.061][000000006.138] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-24 23:49:49.094][000000006.145] I/user.onewire_multi_app ROM ID校验成功: 289FC493000000 14
+[2025-11-24 23:49:49.124][000000006.145] I/user.onewire_multi_app 开始温度转换
+[2025-11-24 23:49:49.154][000000006.153] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-24 23:49:49.778][000000006.904] I/user.onewire_multi_app 温度转换完成
+[2025-11-24 23:49:49.806][000000006.905] I/user.onewire_multi_app 读取温度数据
+[2025-11-24 23:49:49.836][000000006.917] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-24 23:49:49.866][000000006.918] I/user.onewire_multi_app 温度读取成功: 27.50°C
+[2025-11-24 23:49:49.907][000000006.919] I/user.onewire_multi_app 引脚54温度: 27.50°C 成功率: 100.0%
+
+(3)双传感器按键切换演示
+[2025-11-24 23:49:51.147][000000008.278] I/user.onewire_multi_app 切换按键被按下
+[2025-11-24 23:49:51.783][000000008.919] I/user.onewire_multi_app 切换OneWire引脚...
+[2025-11-24 23:49:51.821][000000008.939] I/user.onewire_multi_app 将PAD54配置为GPIO3 true
+[2025-11-24 23:49:51.856][000000008.940] I/user.onewire_multi_app 将GPIO3设置为高电平输出 function: 0C7F4A10
+[2025-11-24 23:49:51.897][000000008.940] I/user.onewire_multi_app 切换到引脚23
+[2025-11-24 23:49:51.933][000000008.940] I/user.onewire_multi_app 当前使用引脚: 23
+[2025-11-24 23:49:51.965][000000008.941] I/user.onewire_multi_app 将引脚23配置为ONEWIRE功能 true
+[2025-11-24 23:49:51.994][000000008.961] I/user.onewire_multi_app 引脚切换完成,当前使用: 引脚23
+[2025-11-24 23:49:52.324][000000009.461] I/user.onewire_multi_app 初始化OneWire总线,通道: 0
+[2025-11-24 23:49:52.356][000000009.471] I/user.onewire_multi_app OneWire总线初始化完成,通道: 0,引脚:23
+[2025-11-24 23:49:52.431][000000009.571] I/user.onewire_multi_app 第4次读取,引脚:23
+[2025-11-24 23:49:52.474][000000009.571] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 23
+[2025-11-24 23:49:52.519][000000009.572] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-24 23:49:52.563][000000009.579] I/user.onewire_multi_app ROM ID校验成功: 2859F253000000 14
+[2025-11-24 23:49:52.593][000000009.579] I/user.onewire_multi_app 开始温度转换
+[2025-11-24 23:49:52.622][000000009.587] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-24 23:49:53.203][000000010.338] I/user.onewire_multi_app 温度转换完成
+[2025-11-24 23:49:53.239][000000010.339] I/user.onewire_multi_app 读取温度数据
+[2025-11-24 23:49:53.272][000000010.351] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-24 23:49:53.302][000000010.352] I/user.onewire_multi_app 温度读取成功: 27.81°C
+[2025-11-24 23:49:53.332][000000010.353] I/user.onewire_multi_app 引脚23温度: 27.81°C 成功率: 100.0%
+
+
+```

+ 275 - 0
module/Air8000/demo/fota/ble_fota/ble_file_fota.lua

@@ -0,0 +1,275 @@
+--[[
+@module  ble_file_fota
+@summary 蓝牙FOTA升级功能模块(文件写入方式)
+@version 1.0
+@date    2025.12.08
+@author  孟伟
+@usage
+-- 蓝牙FOTA升级功能(文件写入方式)
+-- 提供通过蓝牙低功耗(BLE)接收升级包数据进行固件升级的功能
+
+本文件为FOTA业务逻辑处理模块,核心业务逻辑为:
+1. 处理接收到的BLE写入请求数据
+2. 实现FOTA升级流程的控制
+3. 管理升级状态和文件操作
+
+本文件的对外接口有1个:
+1. ble_file_fota.proc(service_uuid, char_uuid, data): 处理接收到的BLE写入请求数据
+
+依赖模块:
+- ble_main: 用于提供BLE服务和事件处理
+]]
+
+local ble_file_fota = {}
+
+-- 升级状态管理
+local upgrade_state = {
+    is_upgrading = false,          -- 是否正在升级
+    total_size = 0,                -- 总文件大小(字节)
+    received_size = 0,             -- 已接收大小(字节)
+    upgrade_file = "/ble_fota.bin" -- 临时升级文件路径
+}
+
+-- 配置参数
+local config = {
+    service_uuid = "F000",   -- FOTA服务UUID(短格式)
+    char_uuid_cmd = "F001",  -- 命令特征值UUID
+    char_uuid_data = "F002", -- 数据特征值UUID
+    max_packet_size = 200    -- BLE数据包最大长度(字节)
+}
+local function ble_reboot()
+    -- 完成FOTA流程并重启
+    fota.finish(true)
+    log.info("FOTA_CMD", "正在重启设备...")
+    rtos.reboot()
+end
+-- 处理FOTA命令
+-- @param cmd_data 命令数据,格式:[命令码(1字节)] 或 [命令码(1字节) + 文件大小(4字节)]
+local function handle_command(cmd_data)
+    log.info("FOTA_CMD", "收到命令数据:", cmd_data:toHex(), "长度:", #cmd_data)
+
+    -- 检查命令数据是否有效
+    if #cmd_data < 1 then
+        log.error("FOTA_CMD", "命令数据为空")
+        return
+    end
+
+    -- 解析命令码(第一个字节)
+    local cmd = cmd_data:byte(1)
+    log.info("FOTA_CMD", "解析命令码:", cmd, string.format("(0x%02X)", cmd))
+
+    -- 命令0x01:开始升级
+    if cmd == 0x01 then
+        log.info("FOTA_CMD", "处理开始升级命令")
+
+        -- 检查命令格式:需要至少5字节(1字节命令码 + 4字节文件大小)
+        if #cmd_data >= 5 then
+            -- 解析文件大小(小端序,从第2字节开始)
+            local total_size = string.unpack("<I4", cmd_data, 2)
+            log.info("FOTA_CMD", "文件总大小:", total_size, "字节")
+
+            -- 初始化FOTA子系统
+            log.info("FOTA_CMD", "初始化FOTA子系统...")
+            if fota.init() then
+                log.info("FOTA_CMD", "FOTA初始化成功")
+
+                -- 等待FOTA底层准备就绪
+                log.info("FOTA_CMD", "等待FOTA底层准备...")
+                -- 等待FOTA底层准备就绪,最多等待10秒
+                local wait_count = 0
+                local wait_ok = false
+                while wait_count < 100 do -- 最多轮询100次,每次100ms,共10秒
+                    if fota.wait() then
+                        wait_ok = true
+                        break
+                    end
+                    sys.wait(100)
+                    wait_count = wait_count + 1
+                end
+
+                if wait_ok then
+                    log.info("FOTA_CMD", "FOTA底层准备就绪")
+
+                    -- 删除旧的临时文件(如果存在)
+                    if os.remove(upgrade_state.upgrade_file) then
+                        log.info("FOTA_CMD", "已清理旧临时文件")
+                    end
+
+                    -- 更新升级状态
+                    upgrade_state.is_upgrading = true
+                    upgrade_state.total_size = total_size
+                    upgrade_state.received_size = 0
+
+                    log.info("FOTA_CMD", "升级状态已设置",
+                        "总大小:", upgrade_state.total_size,
+                        "临时文件:", upgrade_state.upgrade_file)
+                    log.info("FOTA_CMD", "准备接收固件数据...")
+                else
+                    log.error("FOTA_CMD", "FOTA底层准备超时")
+                    fota.finish(false)
+                    upgrade_state.is_upgrading = false
+                end
+            else
+                log.error("FOTA_CMD", "FOTA初始化失败")
+            end
+        else
+            log.error("FOTA_CMD", "开始命令格式错误,长度不足")
+        end
+
+        -- 命令0x02:结束升级
+    elseif cmd == 0x02 then
+        log.info("FOTA_CMD", "处理结束升级命令")
+
+        -- 检查是否处于升级状态
+        if not upgrade_state.is_upgrading then
+            log.warn("FOTA_CMD", "未处于升级状态,忽略结束命令")
+            return
+        end
+
+        -- 验证文件完整性
+        log.info("FOTA_CMD", "验证文件完整性...")
+        log.info("FOTA_CMD", "已接收:", upgrade_state.received_size, "字节")
+        log.info("FOTA_CMD", "应接收:", upgrade_state.total_size, "字节")
+
+        if upgrade_state.received_size == upgrade_state.total_size then
+            log.info("FOTA_CMD", "文件完整性验证通过")
+
+            -- 执行FOTA升级
+            log.info("FOTA_CMD", "开始执行FOTA升级...")
+            local result, isDone = fota.file(upgrade_state.upgrade_file)
+            log.info("FOTA_CMD", "FOTA升级结果:", "result:", result, "isDone:", isDone)
+
+            if result and isDone then
+                log.info("FOTA_CMD", " FOTA升级成功!")
+
+                -- 延迟重启,给用户一些反应时间
+                log.info("FOTA_CMD", "2秒后设备将自动重启...,重启后通过日志判断最终是否升级成功")
+
+                -- 延迟2秒后重启设备
+                sys.timerStart(ble_reboot, 2000)
+            else
+                log.error("FOTA_CMD", "FOTA升级失败")
+            end
+        else
+            log.error("FOTA_CMD", "文件不完整,升级失败")
+        end
+
+        -- 清理升级状态(无论成功还是失败)
+        log.info("FOTA_CMD", "清理升级状态...")
+        upgrade_state.is_upgrading = false
+
+        -- 删除临时文件
+        if upgrade_state.upgrade_file then
+            if os.remove(upgrade_state.upgrade_file) then
+                log.info("FOTA_CMD", "已删除临时文件")
+            else
+                log.warn("FOTA_CMD", "删除临时文件失败")
+            end
+        end
+
+        -- 结束FOTA流程
+        fota.finish(false)
+        log.info("FOTA_CMD", "升级流程结束")
+    else
+        log.warn("FOTA_CMD", "未知命令码:", cmd, string.format("(0x%02X)", cmd))
+    end
+end
+
+-- 处理FOTA数据
+-- @param data 固件数据块
+local function handle_data(data)
+    log.info("FOTA_DATA", "收到数据包,长度:", #data, "字节")
+
+    -- 检查是否处于升级状态
+    if not upgrade_state.is_upgrading then
+        log.warn("FOTA_DATA", "未处于升级状态,忽略数据")
+        return
+    end
+
+    -- 保存数据到临时文件
+    log.info("FOTA_DATA", "写入文件:", upgrade_state.upgrade_file)
+    local file = io.open(upgrade_state.upgrade_file, "ab")
+    if file then
+        -- 写入数据
+        file:write(data)
+        file:close()
+
+        -- 更新接收状态
+        upgrade_state.received_size = upgrade_state.received_size + #data
+
+        -- 计算并显示进度
+        local progress = math.floor((upgrade_state.received_size / upgrade_state.total_size) * 100)
+
+        -- 每50个数据包或完成时打印进度
+        if upgrade_state.received_size % (config.max_packet_size * 50) == 0 or
+            upgrade_state.received_size >= upgrade_state.total_size then
+            log.info("FOTA_DATA", "升级进度:", progress, "%",
+                "(", upgrade_state.received_size, "/", upgrade_state.total_size, ")")
+        end
+
+        log.info("FOTA_DATA", "数据写入成功,当前总计:", upgrade_state.received_size, "字节")
+    else
+        log.error("FOTA_DATA", "打开文件失败:", upgrade_state.upgrade_file)
+
+        -- 文件操作失败,终止升级
+        upgrade_state.is_upgrading = false
+        fota.finish(false)
+    end
+end
+
+-- 处理接收到的BLE写入请求数据
+-- @param service_uuid 服务UUID
+-- @param char_uuid 特征值UUID
+-- @param data 写入的数据
+function ble_file_fota.proc(service_uuid, char_uuid, data)
+    log.info("ble_file_fota", "处理写入数据", service_uuid, char_uuid, data:toHex())
+
+    -- 简化的UUID匹配逻辑:检查UUID是否包含我们的短UUID
+    local is_service_match = string.find(service_uuid:lower(), config.service_uuid:lower())
+    local is_cmd_match = string.find(char_uuid:lower(), config.char_uuid_cmd:lower())
+    local is_data_match = string.find(char_uuid:lower(), config.char_uuid_data:lower())
+
+    log.info("ble_file_fota", "UUID匹配结果:",
+        "服务匹配:", is_service_match,
+        "命令匹配:", is_cmd_match,
+        "数据匹配:", is_data_match)
+
+    if is_service_match then
+        if is_cmd_match then
+            -- 命令特征值:处理FOTA命令
+            log.info("ble_file_fota", "命令特征值匹配,处理命令")
+            handle_command(data)
+        elseif is_data_match then
+            -- 数据特征值:处理FOTA数据
+            log.info("ble_file_fota", "数据特征值匹配,处理数据")
+            handle_data(data)
+        else
+            log.warn("ble_file_fota", "未知的特征值UUID:", char_uuid)
+        end
+    else
+        log.warn("ble_file_fota", "未知的服务UUID:", service_uuid)
+    end
+end
+
+
+function ble_file_fota.proc_disconnect()
+    log.info("ble_file_fota", "处理连接断开事件")
+
+    -- 如果正在升级,连接断开则终止升级
+    if upgrade_state.is_upgrading then
+        log.error("ble_file_fota", "升级过程中连接断开,终止升级")
+        upgrade_state.is_upgrading = false
+
+        -- 删除临时文件
+        if upgrade_state.upgrade_file then
+            os.remove(upgrade_state.upgrade_file)
+        end
+
+        -- 结束FOTA流程
+        fota.finish(false)
+    end
+end
+
+
+
+return ble_file_fota

BIN
module/Air8000/demo/fota/ble_fota/ble_fota.bin


+ 246 - 0
module/Air8000/demo/fota/ble_fota/ble_fota_tool.py

@@ -0,0 +1,246 @@
+#!/usr/bin/env python3
+import asyncio
+import struct
+import time
+from bleak import BleakScanner, BleakClient
+
+# 完整UUID定义
+FOTA_SERVICE_UUID = "0000f000-0000-1000-8000-00805f9b34fb"  # 完整服务UUID
+FOTA_CMD_CHAR_UUID = "0000f001-0000-1000-8000-00805f9b34fb"  # 完整命令特征UUID
+FOTA_DATA_CHAR_UUID = "0000f002-0000-1000-8000-00805f9b34fb"  # 完整数据特征UUID
+
+# Command definitions
+CMD_START_UPGRADE = 0x01
+CMD_END_UPGRADE = 0x02
+
+# 每包数据大小
+MAX_PACKET_SIZE = 200
+
+class SimpleFotaTool:
+    def __init__(self, device_name, firmware_path):
+        self.device_name = device_name
+        self.firmware_path = firmware_path
+        self.client = None
+        self.firmware_data = None
+        self.total_size = 0
+        self.target_device = None
+
+    async def load_firmware(self):
+        """Load firmware file into memory"""
+        try:
+            with open(self.firmware_path, 'rb') as f:
+                self.firmware_data = f.read()
+            self.total_size = len(self.firmware_data)
+            print(f"   固件加载完成,大小: {self.total_size} 字节")
+            return True
+        except Exception as e:
+            print(f"   加载固件失败: {e}")
+            return False
+
+    async def scan_device(self):
+        """Scan for the target device"""
+        print("\n2. 扫描目标设备...")
+        print("   正在扫描,请等待...")
+
+        try:
+            devices = await BleakScanner.discover(timeout=10.0)
+
+            found_devices = []
+            for device in devices:
+                if device.name and self.device_name in device.name:
+                    found_devices.append(device)
+                    print(f"   找到匹配设备: {device.name} (地址: {device.address})")
+
+            if not found_devices:
+                print(f"   未找到设备: {self.device_name}")
+                return None
+
+            # 选择第一个匹配的设备
+            self.target_device = found_devices[0]
+            print(f"   选择设备: {self.target_device.name} (地址: {self.target_device.address})")
+            return self.target_device
+
+        except Exception as e:
+            print(f"   扫描失败: {e}")
+            return None
+
+    async def connect_device(self, device):
+        """Connect to the target device"""
+        print("\n3. 建立BLE连接...")
+        try:
+            self.client = BleakClient(device.address)
+            await self.client.connect(timeout=30.0)
+            print(f"   连接成功,状态: {self.client.is_connected}")
+
+            # 调试:打印所有服务和特征值
+            print("\n4. 发现服务和特征值...")
+
+            # 兼容不同版本的Bleak库
+            try:
+                # 新版本Bleak
+                services = self.client.services
+            except AttributeError:
+                # 旧版本Bleak
+                services = await self.client.get_services()
+
+            fota_service_found = False
+            for service in services:
+                if service.uuid.lower() == FOTA_SERVICE_UUID.lower():
+                    fota_service_found = True
+                    print(f"   找到FOTA服务: {service.uuid}")
+                    for char in service.characteristics:
+                        print(f"     特征值: {char.uuid} - 属性: {char.properties}")
+                        if char.uuid.lower() == FOTA_CMD_CHAR_UUID.lower():
+                            print(f"       -> 命令特征值 (可写)")
+                        elif char.uuid.lower() == FOTA_DATA_CHAR_UUID.lower():
+                            print(f"       -> 数据特征值 (可写)")
+
+            if not fota_service_found:
+                print("   警告: 未找到FOTA服务,但继续尝试...")
+
+            return True
+        except Exception as e:
+            print(f"   连接失败: {e}")
+            return False
+
+    async def write_characteristic(self, uuid, data):
+        """写入特征值"""
+        try:
+            await self.client.write_gatt_char(uuid, data, response=True)
+
+            # 正确提取短UUID(从完整UUID中提取f001/f002部分)
+            # 完整UUID格式: "0000f001-0000-1000-8000-00805f9b34fb"
+            # 我们想要提取 "f001" 部分
+            short_uuid = uuid.split('-')[0][-4:]
+            print(f"   写入特征值 {short_uuid},数据长度: {len(data)} 字节")
+            return True
+        except Exception as e:
+            print(f"   写入特征值失败: {e}")
+            return False
+
+    async def send_start_command(self):
+        """发送开始升级命令"""
+        print("\n5. 发送开始升级命令...")
+
+        # 连接成功后短暂延时
+        print("   连接成功,等待1秒...")
+        await asyncio.sleep(1)
+
+        # 发送开始升级命令
+        start_cmd = struct.pack("<BI", CMD_START_UPGRADE, self.total_size)
+        if not await self.write_characteristic(FOTA_CMD_CHAR_UUID, start_cmd):
+            return False
+
+        print("   开始命令发送完成")
+        await asyncio.sleep(1)  # 等待设备准备
+        return True
+
+    async def send_firmware_data(self):
+        """Send firmware data in chunks with optimized delay"""
+        print("\n6. 分块传输固件数据...")
+        sent_bytes = 0
+        start_time = time.time()
+        packet_count = 0
+
+        # 优化延时:减少到100ms以提高速度
+        PACKET_DELAY = 0.1
+
+        while sent_bytes < self.total_size:
+            chunk_size = min(MAX_PACKET_SIZE, self.total_size - sent_bytes)
+            chunk = self.firmware_data[sent_bytes:sent_bytes + chunk_size]
+
+            if not await self.write_characteristic(FOTA_DATA_CHAR_UUID, chunk):
+                return False
+
+            sent_bytes += chunk_size
+            packet_count += 1
+
+            # 短暂延时,避免数据丢失
+            await asyncio.sleep(PACKET_DELAY)
+
+            # 每20个数据包显示一次进度
+            if packet_count % 20 == 0 or sent_bytes >= self.total_size:
+                progress = (sent_bytes / self.total_size) * 100
+                elapsed = time.time() - start_time
+                speed = sent_bytes / elapsed / 1024 if elapsed > 0 else 0
+                remaining_time = (self.total_size - sent_bytes) / (sent_bytes / elapsed) if sent_bytes > 0 else 0
+                print(f"   进度: {progress:.1f}% - {speed:.1f} KB/s - 已发送 {packet_count} 包 - 预计剩余: {remaining_time:.1f}s")
+
+        total_time = time.time() - start_time
+        avg_speed = self.total_size / total_time / 1024
+        print(f"   数据传输完成! 总时间: {total_time:.1f}s, 平均速度: {avg_speed:.1f} KB/s")
+        return True
+
+    async def end_upgrade(self):
+        """Send end upgrade command"""
+        print("\n7. 发送结束升级命令...")
+        end_cmd = struct.pack("B", CMD_END_UPGRADE)
+        if not await self.write_characteristic(FOTA_CMD_CHAR_UUID, end_cmd):
+            return False
+
+        print("   结束命令发送完成")
+
+        # 等待设备处理
+        print("\n8. 等待设备处理升级...")
+        await asyncio.sleep(5)  # 给设备足够时间处理
+        return True
+
+    async def run(self):
+        """Main execution flow"""
+
+        # 1. 加载固件文件
+        print("\n1. 加载固件文件...")
+        if not await self.load_firmware():
+            return False
+
+        # 2. 扫描目标设备
+        device = await self.scan_device()
+        if not device:
+            return False
+
+        # 3. 连接设备
+        if not await self.connect_device(device):
+            return False
+
+        try:
+            # 4. 发送开始命令
+            if not await self.send_start_command():
+                return False
+
+            # 5. 发送固件数据
+            if not await self.send_firmware_data():
+                return False
+
+            # 6. 结束升级
+            if not await self.end_upgrade():
+                return False
+
+            print("\n" + "="*50)
+            print("升级流程完成! 设备应该正在重启...")
+            print("="*50)
+            return True
+
+        except Exception as e:
+            print(f"   升级过程中出现错误: {e}")
+            return False
+        finally:
+            # 断开连接
+            if self.client and self.client.is_connected:
+                await self.client.disconnect()
+                print("   已断开连接")
+
+async def main():
+    import argparse
+    parser = argparse.ArgumentParser(description="蓝牙FOTA升级工具")
+    parser.add_argument("-f", "--firmware", required=True, help="固件文件路径")
+    parser.add_argument("-d", "--device", default="Air8000_FOTA", help="设备名称")
+
+    args = parser.parse_args()
+
+    tool = SimpleFotaTool(args.device, args.firmware)
+    success = await tool.run()
+    return success
+
+if __name__ == "__main__":
+    success = asyncio.run(main())
+    exit(0 if success else 1)

+ 236 - 0
module/Air8000/demo/fota/ble_fota/ble_main.lua

@@ -0,0 +1,236 @@
+--[[
+@module  ble_main
+@summary BLE服务主功能模块
+@version 1.0
+@date    2025.12.08
+@author  孟伟
+@usage
+-- BLE服务主功能模块
+-- 提供BLE服务的初始化、配置和事件处理
+-- 不包含FOTA业务逻辑,仅处理BLE相关功能
+
+依赖模块:
+- ble_file_fota: 用于处理FOTA相关业务逻辑(文件写入方式)
+- ble_packet_fota: 用于处理FOTA相关业务逻辑(分段写入方式)
+]]
+
+-- 选择FOTA升级方式:"file" 或 "packet"
+-- 1. file方式:将升级包数据先写入本地文件,然后调用fota.file()进行升级
+-- 2. packet方式:直接使用fota.packet()处理分段数据,不写入文件,适合差分升级
+local fota_mode = "packet" -- 默认使用file方式
+
+-- 根据选择加载对应的FOTA模块
+local ble_fota_main
+if fota_mode == "file" then
+    ble_fota_main = require "ble_file_fota"
+else
+    ble_fota_main = require "ble_packet_fota"
+end
+
+-- ble_main的任务名
+local TASK_NAME = "BLE_MAIN"
+
+-- 配置参数
+config = {
+    device_name = "Air8000_FOTA", -- 设备广播名称
+    service_uuid = "F000",        -- FOTA服务UUID(短格式)
+    char_uuid_cmd = "F001",       -- 命令特征值UUID
+    char_uuid_data = "F002",      -- 数据特征值UUID
+    max_packet_size = 20          -- BLE数据包最大长度(字节)
+}
+
+local bluetooth_device = nil
+local ble_device = nil
+local adv_create = nil
+local gatt_create = nil
+
+-- GATT服务数据库定义
+-- 这里定义了BLE设备提供的服务和特征值
+local att_db = {
+    string.fromHex(config.service_uuid), -- Service UUID
+    {
+        string.fromHex(config.char_uuid_cmd),
+        ble.WRITE | ble.WRITE_CMD
+    },
+    {
+        string.fromHex(config.char_uuid_data),
+        ble.WRITE | ble.WRITE_CMD
+    }
+}
+
+-- BLE事件回调函数
+local function ble_event_cb(ble_dev, event, param)
+    log.info("BLE_EVENT", "收到BLE事件:", event)
+
+    -- 根据LuatOS BLE事件枚举处理不同事件
+    if event == ble.EVENT_CONN then
+        -- 连接成功事件
+        log.info("BLE_EVENT", "设备已连接", "地址:", param.addr and param.addr:toHex() or "未知")
+        sys.sendMsg(TASK_NAME, "BLE_EVENT", "CONNECT", param)
+    elseif event == ble.EVENT_DISCONN then
+        -- 连接断开事件
+        log.info("BLE_EVENT", "设备已断开连接", "原因:", param.reason or "未知")
+        sys.sendMsg(TASK_NAME, "BLE_EVENT", "DISCONNECTED", param.reason)
+    elseif event == ble.EVENT_WRITE then
+        -- 写入事件 - 这是关键事件!
+        log.info("BLE_EVENT", "处理写入事件")
+
+        -- 检查参数是否完整
+        if not param or not param.uuid_service or not param.uuid_characteristic or not param.data then
+            log.error("BLE_EVENT", "写入事件参数不完整")
+            return
+        end
+
+        -- 获取服务UUID和特征值UUID
+        local service_uuid = param.uuid_service:toHex()
+        local char_uuid = param.uuid_characteristic:toHex()
+        local data = param.data
+
+        log.info("BLE_EVENT", "服务UUID:", service_uuid)
+        log.info("BLE_EVENT", "特征值UUID:", char_uuid)
+        log.info("BLE_EVENT", "数据长度:", #data, "字节")
+        sys.sendMsg(TASK_NAME, "BLE_EVENT", "WRITE_REQ", param)
+    elseif event == ble.EVENT_READ then
+        -- 读取事件 - 外围设备收到主设备读请求
+        log.info("BLE_EVENT", "处理读取事件")
+    elseif event == ble.EVENT_READ_VALUE then
+        -- 读取操作完成事件 - 中心设备读取特征值完成
+        log.info("BLE_EVENT", "读取操作完成", "数据:", param.data and param.data:toHex() or "无数据")
+    elseif event == ble.EVENT_SCAN_REPORT then
+        -- 扫描报告事件 - 中心设备扫描到其他BLE设备
+        log.info("BLE_EVENT", "扫描报告", "RSSI:", param.rssi, "地址:", param.adv_addr and param.adv_addr:toHex() or "未知")
+    elseif event == ble.EVENT_SCAN_STOP then
+        -- 扫描停止事件
+        log.info("BLE_EVENT", "扫描停止")
+    else
+        -- 其他事件
+        log.info("BLE_EVENT", "其他事件类型:", event)
+        if param then
+            -- 尝试打印参数的基本信息,避免直接打印table导致错误
+            if type(param) == "table" then
+                log.info("BLE_EVENT", "事件参数为table,包含字段:", #param)
+                for k, v in pairs(param) do
+                    if type(v) == "string" then
+                        log.info("BLE_EVENT", "参数字段:", k, "值:", v:toHex())
+                    else
+                        log.info("BLE_EVENT", "参数字段:", k, "类型:", type(v))
+                    end
+                end
+            else
+                log.info("BLE_EVENT", "事件参数类型:", type(param))
+            end
+        end
+    end
+end
+
+-- 初始化BLE功能
+local function ble_init()
+    log.info("BLE_INIT", "开始初始化BLE...")
+
+    -- 初始化蓝牙核心
+    bluetooth_device = bluetooth.init()
+    if not bluetooth_device then
+        log.error("BLE_INIT", "蓝牙核心初始化失败")
+        return false
+    end
+    log.info("BLE_INIT", "蓝牙核心初始化成功")
+
+    -- 初始化BLE功能
+    ble_device = bluetooth_device:ble(ble_event_cb)
+    if not ble_device then
+        log.error("BLE_INIT", "BLE功能初始化失败")
+        return false
+    end
+    log.info("BLE_INIT", "BLE功能初始化成功")
+
+    -- 创建GATT服务
+    gatt_create = ble_device:gatt_create(att_db)
+    if not gatt_create then
+        log.error("BLE_INIT", "GATT服务创建失败")
+        return false
+    end
+    log.info("BLE_INIT", "GATT服务创建成功")
+
+    -- 配置广播数据
+    log.info("BLE_INIT", "配置广播数据...")
+    adv_create = ble_device:adv_create({
+        addr_mode = ble.PUBLIC,
+        channel_map = ble.CHNLS_ALL,
+        intv_min = 120,
+        intv_max = 120,
+        adv_data = {
+            { ble.FLAGS,               string.char(0x06) },  -- BLE标志
+            { ble.COMPLETE_LOCAL_NAME, config.device_name }, -- 设备名称
+        }
+    })
+
+    if not adv_create then
+        log.error("BLE_INIT", "广播配置失败")
+        return false
+    end
+    log.info("BLE_INIT", "广播配置成功")
+
+    -- 开始广播
+    ble_device:adv_start()
+    log.info("BLE_INIT", "BLE广播已启动,设备名称:", config.device_name)
+
+    return true
+end
+
+-- 主任务处理函数
+local function ble_main_task_func()
+    local result, msg
+
+    while true do
+        result = ble_init()
+        if not result then
+            log.error("ble_main_task_func", "ble_init error")
+            goto EXCEPTION_PROC
+        end
+
+        while true do
+            msg = sys.waitMsg(TASK_NAME, "BLE_EVENT")
+
+            if not msg then
+                log.error("ble_main_task_func", "waitMsg timeout")
+                goto EXCEPTION_PROC
+            end
+
+            if msg[2] == "CONNECT" then
+                local conn_param = msg[3]
+                log.info("BLE", "设备连接成功: " .. conn_param.addr:toHex())
+            elseif msg[2] == "DISCONNECTED" then
+                log.info("BLE", "设备断开连接,原因: " .. msg[3])
+                -- 通知FOTA模块连接断开
+                ble_fota_main.proc_disconnect()
+                break
+            -- 收到中心设备的写请求,将写的数据发给ble_fota_main模块处理
+            elseif msg[2] == "WRITE_REQ" then
+                local ble_param = msg[3]
+                local service_uuid = ble_param.uuid_service:toHex()
+                local char_uuid = ble_param.uuid_characteristic:toHex()
+                local data = ble_param.data
+
+                log.info("BLE", "收到写请求: " .. service_uuid .. " " .. char_uuid .. " " .. data:toHex())
+                ble_fota_main.proc(service_uuid, char_uuid, data)
+            end
+        end
+
+        ::EXCEPTION_PROC::
+        log.error("ble_main_task_func", "异常退出, 5秒后重新开启广播")
+
+        -- 停止广播
+        if ble_device then
+            ble_device:adv_stop()
+        end
+
+        -- 清空此task绑定的消息队列中的未处理的消息
+        sys.cleanMsg(TASK_NAME)
+
+        -- 5秒后跳转到循环体开始位置,自动重试
+        sys.wait(5000)
+    end
+end
+
+-- 启动主任务
+sys.taskInitEx(ble_main_task_func, TASK_NAME)

+ 270 - 0
module/Air8000/demo/fota/ble_fota/ble_packet_fota.lua

@@ -0,0 +1,270 @@
+--[[
+@module  ble_packet_fota
+@summary 蓝牙FOTA升级功能模块(分段写入方式)
+@version 1.0
+@date    2025.12.08
+@author  孟伟
+@usage
+-- 蓝牙FOTA升级功能(分段写入方式)
+-- 提供通过蓝牙低功耗(BLE)接收升级包数据进行固件升级的功能
+
+本文件为FOTA业务逻辑处理模块,核心业务逻辑为:
+1. 处理接收到的BLE写入请求数据
+2. 实现FOTA升级流程的控制(分段写入方式)
+3. 管理升级状态和分段数据操作
+
+本文件的对外接口有1个:
+1. ble_packet_fota.proc(service_uuid, char_uuid, data): 处理接收到的BLE写入请求数据
+
+依赖模块:
+- ble_main: 用于提供BLE服务和事件处理
+]]
+
+local ble_packet_fota = {}
+
+-- 升级状态管理
+local upgrade_state = {
+    is_upgrading = false, -- 是否正在升级
+    total_size = 0,       -- 总文件大小(字节)
+    received_size = 0,    -- 已接收大小(字节)
+    upgrade_packet = 0    -- 升级包计数器
+}
+
+-- 配置参数
+local config = {
+    service_uuid = "F000",   -- FOTA服务UUID(短格式)
+    char_uuid_cmd = "F001",  -- 命令特征值UUID
+    char_uuid_data = "F002", -- 数据特征值UUID
+    max_packet_size = 200    -- BLE数据包最大长度(字节)
+}
+local function ble_reboot()
+    -- 完成FOTA流程并重启
+    fota.finish(true)
+    log.info("FOTA_CMD", "正在重启设备...")
+    rtos.reboot()
+end
+-- 处理FOTA命令
+-- @param cmd_data 命令数据,格式:[命令码(1字节)] 或 [命令码(1字节) + 文件大小(4字节)]
+local function handle_command(cmd_data)
+    log.info("FOTA_CMD", "收到命令数据:", cmd_data:toHex(), "长度:", #cmd_data)
+
+    -- 检查命令数据是否有效
+    if #cmd_data < 1 then
+        log.error("FOTA_CMD", "命令数据为空")
+        return
+    end
+
+    -- 解析命令码(第一个字节)
+    local cmd = cmd_data:byte(1)
+    log.info("FOTA_CMD", "解析命令码:", cmd, string.format("(0x%02X)", cmd))
+
+    -- 命令0x01:开始升级
+    if cmd == 0x01 then
+        log.info("FOTA_CMD", "处理开始升级命令")
+
+        -- 检查命令格式:需要至少5字节(1字节命令码 + 4字节文件大小)
+        if #cmd_data >= 5 then
+            -- 解析文件大小(小端序,从第2字节开始)
+            local total_size = string.unpack("<I4", cmd_data, 2)
+            log.info("FOTA_CMD", "文件总大小:", total_size, "字节")
+
+            -- 初始化FOTA子系统
+            log.info("FOTA_CMD", "初始化FOTA子系统...")
+            if fota.init() then
+                log.info("FOTA_CMD", "FOTA初始化成功")
+
+                -- 等待FOTA底层准备就绪
+                log.info("FOTA_CMD", "等待FOTA底层准备...")
+                -- 等待FOTA底层准备就绪,最多等待10秒
+                local wait_count = 0
+                local wait_ok = false
+                while wait_count < 100 do -- 最多轮询100次,每次100ms,共10秒
+                    if fota.wait() then
+                        wait_ok = true
+                        break
+                    end
+                    sys.wait(100)
+                    wait_count = wait_count + 1
+                end
+
+                if wait_ok then
+                    log.info("FOTA_CMD", "FOTA底层准备就绪")
+
+                    -- 更新升级状态
+                    upgrade_state.is_upgrading = true
+                    upgrade_state.total_size = total_size
+                    upgrade_state.received_size = 0
+                    upgrade_state.upgrade_packet = 0
+
+                    log.info("FOTA_CMD", "升级状态已设置",
+                        "总大小:", upgrade_state.total_size)
+                    log.info("FOTA_CMD", "准备接收固件数据...")
+                else
+                    log.error("FOTA_CMD", "FOTA底层准备超时")
+                    fota.finish(false)
+                    upgrade_state.is_upgrading = false
+                end
+            else
+                log.error("FOTA_CMD", "FOTA初始化失败")
+            end
+        else
+            log.error("FOTA_CMD", "开始命令格式错误,长度不足")
+        end
+
+        -- 命令0x02:结束升级(通知升级包发完)
+    elseif cmd == 0x02 then
+        log.info("FOTA_CMD", "处理结束升级命令")
+
+        -- 检查是否处于升级状态
+        if not upgrade_state.is_upgrading then
+            log.warn("FOTA_CMD", "未处于升级状态,忽略结束命令")
+            return
+        end
+
+        -- 验证文件完整性
+        log.info("FOTA_CMD", "验证文件完整性...")
+        log.info("FOTA_CMD", "已接收:", upgrade_state.received_size, "字节")
+        log.info("FOTA_CMD", "应接收:", upgrade_state.total_size, "字节")
+
+        if upgrade_state.received_size == upgrade_state.total_size then
+            log.info("FOTA_CMD", "文件完整性验证通过")
+            log.info("FOTA_CMD", "升级数据已全部接收,等待升级完成...")
+
+            -- 等待底层校验结束
+            local success = false
+            for i = 1, 30 do -- 最多等待3秒
+                sys.wait(100)
+                local succ, fotaDone = fota.isDone()
+                if not succ then
+                    log.error("FOTA_CMD", "校验过程出错")
+                    fota.finish(false)
+                    upgrade_state.is_upgrading = false
+                    break
+                end
+                if fotaDone then
+                    log.info("FOTA_CMD", "FOTA升级成功!")
+
+                    -- 延迟重启,给用户一些反应时间
+                    log.info("FOTA_CMD", "2秒后设备将自动重启...,重启后通过日志判断最终是否升级成功")
+
+                    -- 延迟2秒后重启设备
+                    sys.timerStart(ble_reboot, 2000)
+                    success = true
+                    break
+                end
+            end
+
+            if not success then
+                log.error("FOTA_CMD", "校验超时")
+                fota.finish(false)
+                upgrade_state.is_upgrading = false
+            end
+        else
+            log.error("FOTA_CMD", "文件不完整,升级失败")
+            -- 清理升级状态
+            upgrade_state.is_upgrading = false
+            fota.finish(false)
+        end
+
+        log.info("FOTA_CMD", "结束升级命令处理完成")
+    else
+        log.warn("FOTA_CMD", "未知命令码:", cmd, string.format("(0x%02X)", cmd))
+    end
+end
+
+-- 处理FOTA数据
+-- @param data 固件数据块
+local function handle_data(data)
+    log.info("FOTA_DATA", "收到数据包,长度:", #data, "字节")
+
+    -- 检查是否处于升级状态
+    if not upgrade_state.is_upgrading then
+        log.warn("FOTA_DATA", "未处于升级状态,忽略数据")
+        return
+    end
+
+    -- 直接使用fota.run()处理分段数据,不写入文件
+    log.info("FOTA_DATA", "处理分段数据,包序号:", upgrade_state.upgrade_packet)
+    local result, isDone = fota.run(data)
+    log.info("FOTA_DATA", "分段写入结果:", "result:", result, "isDone:", isDone)
+
+    if result then
+        -- 更新接收状态
+        upgrade_state.received_size = upgrade_state.received_size + #data
+        upgrade_state.upgrade_packet = upgrade_state.upgrade_packet + 1
+
+        -- 计算并显示进度
+        local progress = math.floor((upgrade_state.received_size / upgrade_state.total_size) * 100)
+
+        -- 每50个数据包或完成时打印进度
+        if upgrade_state.received_size % (config.max_packet_size * 50) == 0 or
+            upgrade_state.received_size >= upgrade_state.total_size then
+            log.info("FOTA_DATA", "升级进度:", progress, "%",
+                "(", upgrade_state.received_size, "/", upgrade_state.total_size, ")")
+        end
+
+        log.info("FOTA_DATA", "数据写入成功,当前总计:", upgrade_state.received_size, "字节")
+
+        -- 如果所有数据都已接收,检查升级是否完成
+        if upgrade_state.received_size >= upgrade_state.total_size then
+            log.info("FOTA_DATA", "所有数据已接收,等待升级完成...")
+        end
+    else
+        log.error("FOTA_DATA", "分段写入失败")
+
+        -- 分段写入失败,终止升级
+        upgrade_state.is_upgrading = false
+        fota.finish(false)
+    end
+end
+
+-- 处理接收到的BLE写入请求数据
+-- @param service_uuid 服务UUID
+-- @param char_uuid 特征值UUID
+-- @param data 写入的数据
+function ble_packet_fota.proc(service_uuid, char_uuid, data)
+    log.info("ble_packet_fota", "处理写入数据", service_uuid, char_uuid, data:toHex())
+
+    -- 简化的UUID匹配逻辑:检查UUID是否包含我们的短UUID
+    local is_service_match = string.find(service_uuid:lower(), config.service_uuid:lower())
+    local is_cmd_match = string.find(char_uuid:lower(), config.char_uuid_cmd:lower())
+    local is_data_match = string.find(char_uuid:lower(), config.char_uuid_data:lower())
+
+    log.info("ble_packet_fota", "UUID匹配结果:",
+        "服务匹配:", is_service_match,
+        "命令匹配:", is_cmd_match,
+        "数据匹配:", is_data_match)
+
+    if is_service_match then
+        if is_cmd_match then
+            -- 命令特征值:处理FOTA命令
+            log.info("ble_packet_fota", "命令特征值匹配,处理命令")
+            handle_command(data)
+        elseif is_data_match then
+            -- 数据特征值:处理FOTA数据
+            log.info("ble_packet_fota", "数据特征值匹配,处理数据")
+            handle_data(data)
+        else
+            log.warn("ble_packet_fota", "未知的特征值UUID:", char_uuid)
+        end
+    else
+        log.warn("ble_packet_fota", "未知的服务UUID:", service_uuid)
+    end
+end
+
+-- 处理BLE连接断开事件
+-- @return nil
+function ble_packet_fota.proc_disconnect()
+    log.info("ble_packet_fota", "处理连接断开事件")
+
+    -- 如果正在升级,连接断开则终止升级
+    if upgrade_state.is_upgrading then
+        log.error("ble_packet_fota", "升级过程中连接断开,终止升级")
+        upgrade_state.is_upgrading = false
+
+        -- 结束FOTA流程
+        fota.finish(false)
+    end
+end
+
+return ble_packet_fota

+ 92 - 0
module/Air8000/demo/fota/ble_fota/main.lua

@@ -0,0 +1,92 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.10.24
+@author  孟伟
+@usage
+Air8000模块的蓝牙FOTA升级功能演示;
+
+通过蓝牙低功耗(BLE)方式实现固件升级的应用场景:
+
+1、蓝牙分段升级:通过BLE连接将升级包文件分多个片段发送,每个片段接收并写入,代码演示通过蓝牙特征值传输固件数据进行升级;
+
+2、无需有线连接:利用BLE无线通信特性,实现设备固件的无线远程升级;
+
+适用场景:
+    - 蓝牙设备固件升级 -> 智能硬件、可穿戴设备等蓝牙连接设备
+    - 自定义升级流程 -> 通过BLE特征值实现命令控制和数据传输
+
+更多说明参考本目录下的readme.md文件
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+PROJECT = "fota_test"
+VERSION = "001.000.000" --不同于使用libfota2扩展库来升级必须是xxx.xxx.xxx的格式,这里可以自定义版本号格式。
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+
+
+-- 循环打印版本号, 方便看版本号变化, 非必须
+function print_version()
+    while 1 do
+        sys.wait(3000)
+        log.info("fota", "version", VERSION)
+        -- log.info("hezhou ble_fota test")
+    end
+end
+sys.taskInit(print_version)
+
+-- 加载蓝牙FOTA功能模块
+require "ble_main"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 1297 - 0
module/Air8000/demo/fota/ble_fota/readme.md

@@ -0,0 +1,1297 @@
+## 功能模块介绍
+
+1、main.lua:主程序入口,负责初始化系统和启动蓝牙FOTA服务;
+
+2、ble_main.lua:BLE服务主功能模块,负责蓝牙连接管理、事件处理和数据传输;支持配置选择FOTA升级方式;
+
+3、ble_file_fota.lua:FOTA业务逻辑模块(文件写入方式),负责升级流程控制、命令处理和文件操作;
+
+4、ble_packet_fota.lua:FOTA业务逻辑模块(分段写入方式),负责升级流程控制、命令处理和分段数据操作;
+
+5、ble_fota_tool.py:Python脚本工具,用于通过蓝牙发送升级包,演示ble升级的完整流程;
+
+6、ble_fota.bin:演示蓝牙FOTA升级的升级包文件,用于测试升级功能的完整性;内容只升级了版本号,加了个打印
+
+## 演示功能概述
+
+本demo演示的核心功能为:
+
+Air8000系列模块通过BLE(蓝牙低功耗)进行固件远程升级(FOTA)的完整实现方案,支持两种升级方式:
+
+1. **文件写入方式**:将接收到的升级包数据先保存到本地临时文件,完成数据接收后再执行升级
+   - 优点:适合完整固件升级,逻辑清晰,易于调试
+   - 缺点:需要额外的文件系统分区存储空间保存完整固件
+   - 适用场景:存储空间充足的设备
+
+2. **分段写入方式**:直接将接收到的升级包数据通过fota.run()函数处理,无需保存到本地文件
+   - 优点:节省存储空间,适合差分升级
+   - 缺点:对内存要求较高,需要足够的内存缓冲区实时处理数据
+   - 适用场景:存储空间有限的设备
+
+适用场景:
+- 智能硬件、可穿戴设备等蓝牙连接设备的固件升级
+- 需要自定义升级流程的应用场景
+- 无网络环境下的设备固件更新
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air8000/luatos/common/hwenv/image/Air8000_core_board1.jpg)
+
+1、Air8000 核心板一块
+
+2、TYPE-C USB数据线一根 ,Air8000 核心板和数据线的硬件接线方式为:
+
+- Air8000 核心板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8000 V2016版本固件)](https://docs.openluat.com/air8000/luatos/firmware/)
+
+3、Python 3 环境(用于运行ble_fota_tool.py发送升级包)
+      注意需要安装bleak库,可通过pip安装:
+
+      ```bash
+         pip install bleak
+      ```
+
+## 蓝牙FOTA升级工具使用说明
+
+### 工具功能
+- 连接Air8000设备并发送升级包
+- 显示升级进度和状态信息
+- 支持自定义升级包文件名
+
+### 使用方法
+1. 确保Air8000设备处于蓝牙广播状态,设备名称为"Air8000_FOTA"
+2. 在命令行中运行ble_fota_tool.py脚本,指定升级包文件名
+   ```bash
+   python ble_fota_tool.py -f 升级包文件名
+   ```
+
+### 参数说明
+
+- -d 或 --device - 指定目标设备名称(默认值为"Air8000_FOTA")
+
+- -f 或 --firmware - 指定固件文件路径(必需参数)
+
+### 工作流程
+当运行脚本后,它会自动执行以下步骤:
+
+1. 加载指定的固件文件
+2. 扫描并发现蓝牙设备
+3. 连接到目标设备
+4. 发送开始升级命令
+5. 发送固件数据到设备
+6. 发送结束升级命令
+7. 等待设备处理升级
+8. 断开连接并完成升级过程
+
+脚本会在每个步骤显示详细的日志信息,包括连接状态、数据传输进度等,方便用户监控整个升级过程。
+
+## 蓝牙GATT服务设计
+
+### 服务与特征值定义
+| 类型 |UUID | 属性 | 功能描述 |
+|------|------------|------|----------|
+| Service | F000 | - | FOTA服务主UUID,用于标识升级服务 |
+| Characteristic | F001 | Write Without Response | 命令特征值,用于发送升级控制命令 |
+| Characteristic | F002 | Write Without Response | 数据特征值,用于发送升级包数据 |
+
+### 命令报文格式
+
+#### 开始升级命令
+- **命令码**:0x01
+- **格式**:`[命令码(1字节)] + [文件大小(4字节)]`
+- **示例**:`01 75 15 00 00` 表示开始升级,固件大小为5493字节
+- **说明**:
+  - 命令码固定为0x01
+  - 文件大小为4字节小端序
+  - 设备收到此命令后,初始化FOTA子系统并准备接收数据
+
+#### 结束升级命令
+- **命令码**:0x02
+- **格式**:`[命令码(1字节)]`
+- **示例**:`02`
+- **说明**:
+  - 命令码固定为0x02
+  - 设备收到此命令后,验证固件完整性并执行升级
+
+### 数据报文格式
+
+- **格式**:`[固件数据(n字节)]`
+- **默认长度**:200字节(可通过ble_fota_tool.py配置调整,最大256)
+- **说明**:
+  - 固件数据为二进制格式
+  - 每包数据大小不超过BLE数据包最大长度:Air8000系列模组硬件支持最大256字节,考虑到稳定性建议保持默认的200字节
+  - 设备收到数据后,根据所选的FOTA方式进行处理:
+    - 文件方式:将数据写入临时文件
+    - 分段方式:直接通过fota.run()处理数据
+
+### 升级流程
+
+#### 用法:
+1. 先把脚本和固件烧录到Air8000模块中,并确认设备正常启动
+2. 模块启动后会自动开启BLE广播,广播名称为"Air8000_FOTA"
+3. 在电脑端操作:运行ble_fota_tool.py脚本连接设备并发送升级固件
+    注意:确保升级文件名为正确格式,并且与ble_fota_tool.py在同一目录下
+4. 观察日志输出确认升级进度
+5. 模块接收并验证固件成功后,会自动重启并应用新固件
+
+#### BLE通讯过程说明
+蓝牙FOTA升级通过BLE特征值进行命令控制和数据传输:
+协议流程:
+1. 上位机通过BLE扫描并连接名为"Air8000_FOTA"的设备
+2. 上位机向命令特征值(F001)发送开始升级命令(0x01)和固件大小
+3. 设备初始化FOTA功能并准备接收数据
+4. 上位机向数据特征值(F002)分包发送固件数据
+5. 设备接收并保存数据到临时文件
+6. 上位机发送结束升级命令(0x02)
+7. 设备验证固件完整性并执行FOTA升级流程
+8. 升级成功后设备自动重启
+
+
+## 演示操作步骤
+
+1、搭建好演示硬件环境
+
+2、根据ble_main.lua中的fota_mode选择FOTA升级方式:"file" 或 "packet",默认是"packet"方式。
+   ```lua
+   -- 选择FOTA升级方式,"file" 或 "packet"
+   local fota_mode = "packet"
+   ```
+
+3、测试的时候可以直接使用demo目录下已经制作好的升级包,也可以自己制作升级包。
+
+   自己制作升级包时使用Luatools制作升级包,流程参考[升级包制作流程](https://docs.openluat.com/osapi/core/fota/#51)。将制作好的升级包放在ble_fota_tool.py同级目录下
+
+4、使用Luatools烧录内核固件和demo脚本代码,烧录成功后,自动开机运行。
+
+5、确认设备处于蓝牙广播状态(设备名称为"Air8000_FOTA")
+
+6、运行Python脚本发送升级包:
+
+```bash
+python ble_fota_tool.py -f ble_fota.bin
+```
+
+7、脚本会自动扫描并连接到目标设备,发送升级命令并传输固件文件
+
+8、设备接收并验证升级包,校验升级包没问题后会启动升级并重启,重启后项目版本号会从1.0.0更新为1.0.1
+
+9、可以看到如下日志:
+
+   蓝牙FOTA升级日志解读:
+
+   1. 设备启动,蓝牙服务初始化成功
+
+   2. 收到开始升级命令,初始化FOTA分区和缓冲区
+
+   3. 开始分段接收固件数据
+
+   4. 所有数据接收完成,验证固件完整性
+
+   5. 升级完成,设备重启
+
+   6. 重启后新版本正常运行,确认升级成功
+
+file方式升级日志如下:
+```lua
+[2025-12-09 14:43:26.113][000000000.416] I/user.BLE_INIT 开始初始化BLE...
+[2025-12-09 14:43:26.144][000000000.416] D/drv.bt 执行luat_bluetooth_init
+[2025-12-09 14:43:26.171][000000000.416] I/user.BLE_INIT 蓝牙核心初始化成功
+[2025-12-09 14:43:26.196][000000000.416] I/user.BLE_INIT BLE功能初始化成功
+[2025-12-09 14:43:26.241][000000000.417] I/user.BLE_INIT GATT服务创建成功
+[2025-12-09 14:43:26.259][000000000.417] I/user.BLE_INIT 配置广播数据...
+[2025-12-09 14:43:26.276][000000000.418] I/user.BLE_INIT 广播配置成功
+[2025-12-09 14:43:26.294][000000000.418] I/user.BLE_INIT BLE广播已启动,设备名称: Air8000_FOTA
+[2025-12-09 14:43:26.322][000000000.448] I/user.BLE_EVENT 收到BLE事件: 1
+[2025-12-09 14:43:26.350][000000000.448] I/user.BLE_EVENT 其他事件类型: 1
+[2025-12-09 14:43:26.386][000000000.474] I/user.BLE_EVENT 收到BLE事件: 3
+[2025-12-09 14:43:26.416][000000000.475] I/user.BLE_EVENT 其他事件类型: 3
+[2025-12-09 14:43:26.451][000000000.519] I/user.BLE_EVENT 收到BLE事件: 4
+[2025-12-09 14:43:26.490][000000000.520] I/user.BLE_EVENT 其他事件类型: 4
+[2025-12-09 14:43:26.805][000000001.663] D/mobile cid1, state0
+[2025-12-09 14:43:26.831][000000001.663] D/mobile bearer act 0, result 0
+[2025-12-09 14:43:26.851][000000001.664] D/mobile NETIF_LINK_ON -> IP_READY
+[2025-12-09 14:43:26.867][000000001.707] D/mobile TIME_SYNC 0
+[2025-12-09 14:43:28.031][000000003.383] I/user.fota version 001.000.000
+[2025-12-09 14:43:31.021][000000006.383] I/user.fota version 001.000.000
+[2025-12-09 14:43:34.027][000000009.383] I/user.fota version 001.000.000
+[2025-12-09 14:43:37.019][000000012.383] I/user.fota version 001.000.000
+[2025-12-09 14:43:40.018][000000015.383] I/user.fota version 001.000.000
+[2025-12-09 14:43:43.022][000000018.383] I/user.fota version 001.000.000
+[2025-12-09 14:43:46.031][000000021.383] I/user.fota version 001.000.000
+[2025-12-09 14:43:46.419][000000021.770] I/user.BLE_EVENT 收到BLE事件: 12
+[2025-12-09 14:43:46.430][000000021.771] I/user.BLE_EVENT 设备已连接 地址: 141333C76D92
+[2025-12-09 14:43:46.439][000000021.772] I/user.BLE 设备连接成功: 141333C76D92
+[2025-12-09 14:43:49.026][000000024.383] I/user.fota version 001.000.000
+[2025-12-09 14:43:49.135][000000024.495] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:49.145][000000024.495] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:49.155][000000024.496] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:49.165][000000024.496] I/user.BLE_EVENT 特征值UUID: F001
+[2025-12-09 14:43:49.174][000000024.496] I/user.BLE_EVENT 数据长度: 5 字节
+[2025-12-09 14:43:49.186][000000024.497] I/user.BLE 收到写请求: F000 F001 01871A0000
+[2025-12-09 14:43:49.200][000000024.498] I/user.ble_file_fota 处理写入数据 F000 F001 01871A0000 10
+[2025-12-09 14:43:49.207][000000024.498] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: 1 数据匹配: nil
+[2025-12-09 14:43:49.222][000000024.499] I/user.ble_file_fota 命令特征值匹配,处理命令
+[2025-12-09 14:43:49.233][000000024.499] I/user.FOTA_CMD 收到命令数据: 01871A0000 长度: 5
+[2025-12-09 14:43:49.244][000000024.500] I/user.FOTA_CMD 解析命令码: 1 (0x01)
+[2025-12-09 14:43:49.254][000000024.500] I/user.FOTA_CMD 处理开始升级命令
+[2025-12-09 14:43:49.266][000000024.500] I/user.FOTA_CMD 文件总大小: 6791 字节
+[2025-12-09 14:43:49.272][000000024.500] I/user.FOTA_CMD 初始化FOTA子系统...
+[2025-12-09 14:43:49.282][000000024.515] I/user.FOTA_CMD FOTA初始化成功
+[2025-12-09 14:43:49.292][000000024.516] I/user.FOTA_CMD 等待FOTA底层准备...
+[2025-12-09 14:43:49.300][000000024.516] I/user.FOTA_CMD FOTA底层准备就绪
+[2025-12-09 14:43:49.310][000000024.520] I/user.FOTA_CMD 升级状态已设置 总大小: 6791 临时文件: /ble_fota.bin
+[2025-12-09 14:43:49.319][000000024.520] I/user.FOTA_CMD 准备接收固件数据...
+[2025-12-09 14:43:50.194][000000025.555] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:50.203][000000025.556] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:50.212][000000025.556] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:50.222][000000025.556] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:50.233][000000025.556] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:50.246][000000025.558] I/user.BLE 收到写请求: F000 F002 1872C3EA2C25123600000000000000000000000000000000000000000000000000000000000000000000000000000000000000002B1A00000000000024342525E2585E1B996AB9E251859848000000000000000000000000000000001872C3EA071A0000005C00000E4148EF5A243F7A94C524D49C393BE10000040000E05400056600000400FD19000000008109C7536188201E4070E4F8F94AA2C68473C100329390791CEB9BE96EC35E8E58408C2AC66F005FE972F833C5F3DA9A10A24DFFE32A2D8B93D3FA34
+[2025-12-09 14:43:50.258][000000025.558] I/user.ble_file_fota 处理写入数据 F000 F002 1872C3EA2C25123600000000000000000000000000000000000000000000000000000000000000000000000000000000000000002B1A00000000000024342525E2585E1B996AB9E251859848000000000000000000000000000000001872C3EA071A0000005C00000E4148EF5A243F7A94C524D49C393BE10000040000E05400056600000400FD19000000008109C7536188201E4070E4F8F94AA2C68473C100329390791CEB9BE96EC35E8E58408C2AC66F005FE972F833C5F3DA9A10A24DFFE32A2D8B93D3FA34 400
+[2025-12-09 14:43:50.268][000000025.559] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:50.277][000000025.559] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:50.287][000000025.560] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:50.297][000000025.560] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:50.309][000000025.565] I/user.FOTA_DATA 数据写入成功,当前总计: 200 字节
+[2025-12-09 14:43:50.382][000000025.732] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:50.394][000000025.732] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:50.404][000000025.733] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:50.412][000000025.733] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:50.428][000000025.733] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:50.437][000000025.734] I/user.BLE 收到写请求: F000 F002 578326A8397A8359CFCDB2A5DF2D0F4B55688C2923877D02A65E6A9BEF12A5670C81644C22535802647B3D9DFA35855E2FBF332AA3D80A94E8999A561C368B017A27D3AE1A0CA7C12880C29DADDA628F33041F6E683EAFB72C42A510AB7F5F8DBE623F348D0BD82A12E5A73AE737E3F3AD3B200F768C91297D22E99F748C7E9DD6DE3A692DED4F8B2A0D70AF92ECD2AFD865E60404CBF122A59A5590EF494E4E9F4A9C703C976B1A63F6ECB8CB31146389BFFD8321D48D07B0DF4C3BE1CA40C1B6D6E7F3956BFC27
+[2025-12-09 14:43:50.454][000000025.735] I/user.ble_file_fota 处理写入数据 F000 F002 578326A8397A8359CFCDB2A5DF2D0F4B55688C2923877D02A65E6A9BEF12A5670C81644C22535802647B3D9DFA35855E2FBF332AA3D80A94E8999A561C368B017A27D3AE1A0CA7C12880C29DADDA628F33041F6E683EAFB72C42A510AB7F5F8DBE623F348D0BD82A12E5A73AE737E3F3AD3B200F768C91297D22E99F748C7E9DD6DE3A692DED4F8B2A0D70AF92ECD2AFD865E60404CBF122A59A5590EF494E4E9F4A9C703C976B1A63F6ECB8CB31146389BFFD8321D48D07B0DF4C3BE1CA40C1B6D6E7F3956BFC27 400
+[2025-12-09 14:43:50.468][000000025.736] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:50.482][000000025.736] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:50.494][000000025.736] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:50.502][000000025.736] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:50.514][000000025.802] I/user.FOTA_DATA 数据写入成功,当前总计: 400 字节
+[2025-12-09 14:43:50.585][000000025.950] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:50.594][000000025.950] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:50.602][000000025.951] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:50.611][000000025.951] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:50.621][000000025.951] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:50.633][000000025.953] I/user.BLE 收到写请求: F000 F002 BFAE6FA013B4867390CDA0870A6B7A3FB7F27C519343384418178F72CA8DD59C03C100DC9D5164426EAA229591718358C05796458854B37880CC94AE917BC8E803BB404BC29BB67C7D7079AACDEA8BE9F128332B4082C33022B5E9A381DC3C8E1DAFFDA7DADCEFFBB24E917E2300454A251E2BCDC42C37976E1E67CE95E1296DCC28E9647783D3BB69FD0D2FA8E21AB1A0C273801D4FA26ABE48B2DF7E2EBFACF919F76F7B16A7AEA63B5940D38DA7247DC55368F3FECA9C622E340C70A6E384959B0262F57BEB47
+[2025-12-09 14:43:50.643][000000025.953] I/user.ble_file_fota 处理写入数据 F000 F002 BFAE6FA013B4867390CDA0870A6B7A3FB7F27C519343384418178F72CA8DD59C03C100DC9D5164426EAA229591718358C05796458854B37880CC94AE917BC8E803BB404BC29BB67C7D7079AACDEA8BE9F128332B4082C33022B5E9A381DC3C8E1DAFFDA7DADCEFFBB24E917E2300454A251E2BCDC42C37976E1E67CE95E1296DCC28E9647783D3BB69FD0D2FA8E21AB1A0C273801D4FA26ABE48B2DF7E2EBFACF919F76F7B16A7AEA63B5940D38DA7247DC55368F3FECA9C622E340C70A6E384959B0262F57BEB47 400
+[2025-12-09 14:43:50.654][000000025.954] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:50.664][000000025.954] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:50.670][000000025.954] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:50.680][000000025.955] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:50.689][000000025.971] I/user.FOTA_DATA 数据写入成功,当前总计: 600 字节
+[2025-12-09 14:43:50.770][000000026.125] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:50.779][000000026.126] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:50.794][000000026.126] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:50.807][000000026.126] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:50.815][000000026.126] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:50.829][000000026.128] I/user.BLE 收到写请求: F000 F002 9F91D745FD569A414C89ABA1FADC28AEDBFC7914B989922EC7C2DD96B671A2EA0469B6E0D0C551667CBFB86181535A9B22FFC681501D9782AA99F4ACF3E2226B517EF94FCF225CA046FC4EBA70E295A124859F973CBD2BC54006E0A1C25C0CBF1429405D9A55BF8256947328E167EE7FD1189695C70F6570317586F61573509FE11BE46BAF25E84C9E8C635556B4E36D5E8577A0613B21E506CA71CCD0972C7339FD71B5038C1CDB183D2C7072123C0539CBBD7658B09A6678D79CBEBCC00B3349911BF90AC528E1
+[2025-12-09 14:43:50.840][000000026.128] I/user.ble_file_fota 处理写入数据 F000 F002 9F91D745FD569A414C89ABA1FADC28AEDBFC7914B989922EC7C2DD96B671A2EA0469B6E0D0C551667CBFB86181535A9B22FFC681501D9782AA99F4ACF3E2226B517EF94FCF225CA046FC4EBA70E295A124859F973CBD2BC54006E0A1C25C0CBF1429405D9A55BF8256947328E167EE7FD1189695C70F6570317586F61573509FE11BE46BAF25E84C9E8C635556B4E36D5E8577A0613B21E506CA71CCD0972C7339FD71B5038C1CDB183D2C7072123C0539CBBD7658B09A6678D79CBEBCC00B3349911BF90AC528E1 400
+[2025-12-09 14:43:50.851][000000026.129] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:50.860][000000026.129] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:50.872][000000026.130] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:50.880][000000026.130] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:50.892][000000026.142] I/user.FOTA_DATA 数据写入成功,当前总计: 800 字节
+[2025-12-09 14:43:50.943][000000026.299] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:50.956][000000026.299] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:50.967][000000026.300] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:50.977][000000026.300] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:50.987][000000026.300] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:50.997][000000026.302] I/user.BLE 收到写请求: F000 F002 E223812664DD4D928A3E8EDA95352501825004DA6A79B02F259CCCCD81E7EB6999E56EF07D72F0C89F93DE646E91FDD4F508ADEA6AAFC39653592D1135F981883C2B14DFCE97DFA8C63D8C99A78C46EFA6B7A2F07D3D372A025C2E4F7355FADCB664ED0C63ABF28DA2C73A5DB4693A296E2E0A0AEF8B0091EF1EB5B13B2EF9D662A5FC8B4DB84A4BB0229A1E82F9376A79D1DBD0EF770708EB8A4A6637EDB112DF402AA83089A33DC90E9911FA0C81AF39450ECE80C4B7BA580661286900635BE4A1F81583331816
+[2025-12-09 14:43:51.010][000000026.302] I/user.ble_file_fota 处理写入数据 F000 F002 E223812664DD4D928A3E8EDA95352501825004DA6A79B02F259CCCCD81E7EB6999E56EF07D72F0C89F93DE646E91FDD4F508ADEA6AAFC39653592D1135F981883C2B14DFCE97DFA8C63D8C99A78C46EFA6B7A2F07D3D372A025C2E4F7355FADCB664ED0C63ABF28DA2C73A5DB4693A296E2E0A0AEF8B0091EF1EB5B13B2EF9D662A5FC8B4DB84A4BB0229A1E82F9376A79D1DBD0EF770708EB8A4A6637EDB112DF402AA83089A33DC90E9911FA0C81AF39450ECE80C4B7BA580661286900635BE4A1F81583331816 400
+[2025-12-09 14:43:51.022][000000026.303] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:51.032][000000026.303] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:51.040][000000026.303] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:51.049][000000026.304] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:51.058][000000026.315] I/user.FOTA_DATA 数据写入成功,当前总计: 1000 字节
+[2025-12-09 14:43:51.114][000000026.477] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:51.125][000000026.477] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:51.135][000000026.478] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:51.144][000000026.478] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:51.155][000000026.478] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:51.167][000000026.479] I/user.BLE 收到写请求: F000 F002 E873D77EDC3E717CAA4CB2A7085406D8B4F680F9CF38F4193308A241CFE7F59AD54E7A9A5EB5D818D0C4D193C20318EB3AA118A3F485FB83183D1711B43B59672410A3838F7564CA912FAF597744A170AE45380D29773CDC72330AF7792D3A58E9395F3226B70462F739677B77347338CB374A4411CC3AD721931228728A3082E06B9742903999708C366C3EA2E35CCDF1E59F3B4BB94139B229A775E54D470FA10BC45DB934F73D36DFBE1E076247EDDA39A17A7F281C6B405EE564D144879021AE75435DA61B0B
+[2025-12-09 14:43:51.176][000000026.480] I/user.ble_file_fota 处理写入数据 F000 F002 E873D77EDC3E717CAA4CB2A7085406D8B4F680F9CF38F4193308A241CFE7F59AD54E7A9A5EB5D818D0C4D193C20318EB3AA118A3F485FB83183D1711B43B59672410A3838F7564CA912FAF597744A170AE45380D29773CDC72330AF7792D3A58E9395F3226B70462F739677B77347338CB374A4411CC3AD721931228728A3082E06B9742903999708C366C3EA2E35CCDF1E59F3B4BB94139B229A775E54D470FA10BC45DB934F73D36DFBE1E076247EDDA39A17A7F281C6B405EE564D144879021AE75435DA61B0B 400
+[2025-12-09 14:43:51.190][000000026.481] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:51.199][000000026.481] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:51.205][000000026.481] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:51.217][000000026.482] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:51.229][000000026.494] I/user.FOTA_DATA 数据写入成功,当前总计: 1200 字节
+[2025-12-09 14:43:51.302][000000026.652] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:51.310][000000026.653] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:51.322][000000026.653] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:51.330][000000026.653] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:51.339][000000026.653] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:51.349][000000026.655] I/user.BLE 收到写请求: F000 F002 410FFED6705B09EA71340D2EE49489909141E21CF9FCC15B26AE57779C8ADA469F3544578C2EB7C2AEB55B759AB3539322B1A3CA5670FAB659E5DEF3AC982485982C93F540C526F944C421DFC15E0246A5428FA23FD4EA607077CE1355FC00ED2522B0AB448A43A136D8FEDA5C756EF4D37D707E6CBC01046AA33B12B57AB1058AFF6ED30B6B86F664834CB287D62C1651EAB588EBEB903596CE12DD9E36E282E0F4B3BB2C17AA0B0E220B3088FF1542C89E18F4D621E438884D1A48F425888FC986F6589FF41553
+[2025-12-09 14:43:51.359][000000026.655] I/user.ble_file_fota 处理写入数据 F000 F002 410FFED6705B09EA71340D2EE49489909141E21CF9FCC15B26AE57779C8ADA469F3544578C2EB7C2AEB55B759AB3539322B1A3CA5670FAB659E5DEF3AC982485982C93F540C526F944C421DFC15E0246A5428FA23FD4EA607077CE1355FC00ED2522B0AB448A43A136D8FEDA5C756EF4D37D707E6CBC01046AA33B12B57AB1058AFF6ED30B6B86F664834CB287D62C1651EAB588EBEB903596CE12DD9E36E282E0F4B3BB2C17AA0B0E220B3088FF1542C89E18F4D621E438884D1A48F425888FC986F6589FF41553 400
+[2025-12-09 14:43:51.368][000000026.656] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:51.377][000000026.656] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:51.386][000000026.657] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:51.394][000000026.658] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:51.403][000000026.674] I/user.FOTA_DATA 数据写入成功,当前总计: 1400 字节
+[2025-12-09 14:43:51.474][000000026.825] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:51.487][000000026.826] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:51.497][000000026.826] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:51.511][000000026.826] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:51.525][000000026.826] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:51.534][000000026.828] I/user.BLE 收到写请求: F000 F002 AF8909FB5E3F2931D6AEA8AC8570EBDAE58D62286B0CB3D6EF5536EEA795DC4D7F116520A50DE34925E8253CD9476B67E8B9C7D3A7ED4EF074FA573E087A3F1DD0A7EE0241854D002016D33F56A54F56ED6CBB4207061FD64AF40AECCE0B33C96FDF83303E00A4413B1305516FF04C7052FA4B354EDFC9C8151DBCE50A56171CB112F11864F50045F8C70F0788ECF30AC28C67347A518CB032A2E9D7C10A8FCEA12190E46E3D11F896F18B8EB38CEE94FE8A32C44A14F827EA6E352BEA31D786943436AE5396CFE2
+[2025-12-09 14:43:51.544][000000026.828] I/user.ble_file_fota 处理写入数据 F000 F002 AF8909FB5E3F2931D6AEA8AC8570EBDAE58D62286B0CB3D6EF5536EEA795DC4D7F116520A50DE34925E8253CD9476B67E8B9C7D3A7ED4EF074FA573E087A3F1DD0A7EE0241854D002016D33F56A54F56ED6CBB4207061FD64AF40AECCE0B33C96FDF83303E00A4413B1305516FF04C7052FA4B354EDFC9C8151DBCE50A56171CB112F11864F50045F8C70F0788ECF30AC28C67347A518CB032A2E9D7C10A8FCEA12190E46E3D11F896F18B8EB38CEE94FE8A32C44A14F827EA6E352BEA31D786943436AE5396CFE2 400
+[2025-12-09 14:43:51.560][000000026.829] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:51.569][000000026.829] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:51.578][000000026.829] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:51.590][000000026.830] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:51.601][000000026.856] I/user.FOTA_DATA 数据写入成功,当前总计: 1600 字节
+[2025-12-09 14:43:51.644][000000026.999] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:51.656][000000026.999] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:51.667][000000027.000] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:51.678][000000027.000] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:51.689][000000027.000] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:51.697][000000027.001] I/user.BLE 收到写请求: F000 F002 EDB607310E11E77F72F5F13F50651C339B3A0AE817EE467BA4E6ED338114AF1854D051FB48D13A814239B33805F0EADB89BF82F2D207B9AEB59DB2BE8BD9F4F0EE3FFB28B2E8B28534844A1C4274AE4728329F676E25414207226BCEC0F197FBCBC5C139C7086682A4511E542923B4EB79DCB32DE59784DF5DF8F13FD2F84E550CA85703945F317B1012F4364BC3128819E37F22DB62B91F2CF6297CD569A28A638E77B0063DBEBDE6740677B1D118FFA4859F8D50087E0BBFAC077E262AC204A5F2C80D74BD8FEE
+[2025-12-09 14:43:51.709][000000027.002] I/user.ble_file_fota 处理写入数据 F000 F002 EDB607310E11E77F72F5F13F50651C339B3A0AE817EE467BA4E6ED338114AF1854D051FB48D13A814239B33805F0EADB89BF82F2D207B9AEB59DB2BE8BD9F4F0EE3FFB28B2E8B28534844A1C4274AE4728329F676E25414207226BCEC0F197FBCBC5C139C7086682A4511E542923B4EB79DCB32DE59784DF5DF8F13FD2F84E550CA85703945F317B1012F4364BC3128819E37F22DB62B91F2CF6297CD569A28A638E77B0063DBEBDE6740677B1D118FFA4859F8D50087E0BBFAC077E262AC204A5F2C80D74BD8FEE 400
+[2025-12-09 14:43:51.719][000000027.003] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:51.729][000000027.003] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:51.738][000000027.003] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:51.747][000000027.003] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:51.760][000000027.018] I/user.FOTA_DATA 数据写入成功,当前总计: 1800 字节
+[2025-12-09 14:43:51.816][000000027.174] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:51.824][000000027.174] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:51.835][000000027.175] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:51.853][000000027.175] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:51.867][000000027.175] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:51.881][000000027.177] I/user.BLE 收到写请求: F000 F002 94E91700D87B64F42F5066347F9AA5F5D03C35E7FD6AC34265D5183B77DFB8B9ABC25C778AEB347F9D6BA4D99BA6CAA424383D4CF341160B4B0D4D80CB74B495B481381D21D6774E367A43D2121CF399774973F46FDED2FF379DDB14D6A2E2E4DA79783AC902C2039E1DDB8F14640CEA0A23DCD23FF5ACAB0129F935CC43822A2370E150BE3232E079449AC2438E541DD0A2B95742507017B7645BEAE7804D12989706640D4DFDEF4679ADA908B88AC01D00DB28837D18B9A3391181661ADAAA79F2B6DFE121C14A
+[2025-12-09 14:43:51.899][000000027.177] I/user.ble_file_fota 处理写入数据 F000 F002 94E91700D87B64F42F5066347F9AA5F5D03C35E7FD6AC34265D5183B77DFB8B9ABC25C778AEB347F9D6BA4D99BA6CAA424383D4CF341160B4B0D4D80CB74B495B481381D21D6774E367A43D2121CF399774973F46FDED2FF379DDB14D6A2E2E4DA79783AC902C2039E1DDB8F14640CEA0A23DCD23FF5ACAB0129F935CC43822A2370E150BE3232E079449AC2438E541DD0A2B95742507017B7645BEAE7804D12989706640D4DFDEF4679ADA908B88AC01D00DB28837D18B9A3391181661ADAAA79F2B6DFE121C14A 400
+[2025-12-09 14:43:51.911][000000027.178] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:51.922][000000027.178] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:51.935][000000027.178] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:51.947][000000027.179] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:51.962][000000027.194] I/user.FOTA_DATA 数据写入成功,当前总计: 2000 字节
+[2025-12-09 14:43:51.986][000000027.350] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:51.995][000000027.351] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:52.004][000000027.351] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:52.022][000000027.351] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:52.034][000000027.351] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:52.045][000000027.353] I/user.BLE 收到写请求: F000 F002 3834A5479DF6DDDC5EBADFFF3C8BB9808F67A25A9916D0E5CF17A49FDD72A49C13D258E1E955462C882A05FDC65C7B25E32C6EC8DB5104C3C44D9F50F588DD4A574D93A7C1A55AE21100CA17AB225AF060DA2E9FC234D544D6C499B3E2C967E8AA8F88086C24A7C7688D9C3651200721DE5309CA1E9AEC2EBAD606B5B5BCCA5659DC6EF24B9A886C29F27870882873C76AF5D9728CDFD82307382516962F202EDC31B1CFE82CD3AA5239567AE9B1E44B00AB9CB0112000C6977B621AFEE42BDD42A023014474D2E8
+[2025-12-09 14:43:52.058][000000027.353] I/user.ble_file_fota 处理写入数据 F000 F002 3834A5479DF6DDDC5EBADFFF3C8BB9808F67A25A9916D0E5CF17A49FDD72A49C13D258E1E955462C882A05FDC65C7B25E32C6EC8DB5104C3C44D9F50F588DD4A574D93A7C1A55AE21100CA17AB225AF060DA2E9FC234D544D6C499B3E2C967E8AA8F88086C24A7C7688D9C3651200721DE5309CA1E9AEC2EBAD606B5B5BCCA5659DC6EF24B9A886C29F27870882873C76AF5D9728CDFD82307382516962F202EDC31B1CFE82CD3AA5239567AE9B1E44B00AB9CB0112000C6977B621AFEE42BDD42A023014474D2E8 400
+[2025-12-09 14:43:52.069][000000027.354] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:52.078][000000027.354] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:52.088][000000027.354] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:52.097][000000027.355] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:52.104][000000027.373] I/user.FOTA_DATA 数据写入成功,当前总计: 2200 字节
+[2025-12-09 14:43:52.117][000000027.383] I/user.fota version 001.000.000
+[2025-12-09 14:43:52.177][000000027.525] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:52.191][000000027.525] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:52.199][000000027.526] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:52.211][000000027.526] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:52.222][000000027.526] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:52.231][000000027.527] I/user.BLE 收到写请求: F000 F002 63663F8E2E57002B58CD4812D54992E3DC35630D911747925F0525FE2146969453955C12F6ECF3F086401F7B400842562866F63775646ECEC3EB75A592697C3353B30BDA7D5E7417259AC51EB15D4C9630DA0DAD132232C969B0A5738E613556CBF6C194E11EFE106E0AC67ECF088CE3F0F0005E6A4E88CD7DE728D8F7677025033E575C7C26128CD97E3CF9861BDADE2DFA95C1DAF7AF765E6F5436A58AB9EF5FC46D24D6010D204A63DEAABD64E86FFF7C266259E7281177FC7DE905D83AD76EB54D4E4F42DBE9
+[2025-12-09 14:43:52.241][000000027.528] I/user.ble_file_fota 处理写入数据 F000 F002 63663F8E2E57002B58CD4812D54992E3DC35630D911747925F0525FE2146969453955C12F6ECF3F086401F7B400842562866F63775646ECEC3EB75A592697C3353B30BDA7D5E7417259AC51EB15D4C9630DA0DAD132232C969B0A5738E613556CBF6C194E11EFE106E0AC67ECF088CE3F0F0005E6A4E88CD7DE728D8F7677025033E575C7C26128CD97E3CF9861BDADE2DFA95C1DAF7AF765E6F5436A58AB9EF5FC46D24D6010D204A63DEAABD64E86FFF7C266259E7281177FC7DE905D83AD76EB54D4E4F42DBE9 400
+[2025-12-09 14:43:52.249][000000027.529] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:52.258][000000027.529] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:52.267][000000027.529] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:52.276][000000027.529] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:52.288][000000027.592] I/user.FOTA_DATA 数据写入成功,当前总计: 2400 字节
+[2025-12-09 14:43:52.344][000000027.700] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:52.356][000000027.700] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:52.366][000000027.701] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:52.378][000000027.701] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:52.387][000000027.701] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:52.397][000000027.702] I/user.BLE 收到写请求: F000 F002 CDF7739542AF28D7E4A0C93AE2AFD9FB4F8F025ED1D352BFA14FF85085D795CBC8087C3912E010319D728434CCD90531248A0EA2C4DC65936324E7DABB15C5A7774DA6C761DDBF31CB3242B17CED11DC18BFA1E5663CACF3ECEA84D6E49798742CD98C9F61F7ED4513B69449DEEA1ADBD300C9B2E6DB5920133424E25ECD3809D7C59D4A3AB15BC57EC72554059E953C6172F46557E20FE1C3C25D9AE36380EBC5BE3FC29835D7AB7F5B1844B7DA5D37630C7F3474664ED04FBBD4FA95CC43FAFBE4E32CA76FE988
+[2025-12-09 14:43:52.408][000000027.703] I/user.ble_file_fota 处理写入数据 F000 F002 CDF7739542AF28D7E4A0C93AE2AFD9FB4F8F025ED1D352BFA14FF85085D795CBC8087C3912E010319D728434CCD90531248A0EA2C4DC65936324E7DABB15C5A7774DA6C761DDBF31CB3242B17CED11DC18BFA1E5663CACF3ECEA84D6E49798742CD98C9F61F7ED4513B69449DEEA1ADBD300C9B2E6DB5920133424E25ECD3809D7C59D4A3AB15BC57EC72554059E953C6172F46557E20FE1C3C25D9AE36380EBC5BE3FC29835D7AB7F5B1844B7DA5D37630C7F3474664ED04FBBD4FA95CC43FAFBE4E32CA76FE988 400
+[2025-12-09 14:43:52.419][000000027.704] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:52.429][000000027.704] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:52.435][000000027.704] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:52.446][000000027.704] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:52.459][000000027.720] I/user.FOTA_DATA 数据写入成功,当前总计: 2600 字节
+[2025-12-09 14:43:52.515][000000027.878] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:52.525][000000027.878] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:52.532][000000027.879] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:52.541][000000027.879] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:52.551][000000027.879] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:52.559][000000027.881] I/user.BLE 收到写请求: F000 F002 2FA735BDB0849ECDF7274BAA573388358B7F78CD1F8DD5F64C1A955B430D75A6F465AB75E1E5E04BBF5046F16E0FD8223C68623D8A1195503AE296639EA6618AAEFD7E06DF0BF9C288B03A76FBCBB38C243164461E2B63CCEBDFC2A133C9FF098A354E309FBCFD93AFA80F007CD127295370EC97D2BA52559C7B7FCA3E5DDF5A2224ACDFE41D2E77446E23E344BD73BD84DD30E5C0C74E27D6981DE2E6D725A567266071A805DE5C6A2AA78E80A9278307C777DA72FC057CB1F75A636E0655AEF02F99FEFFDE701C
+[2025-12-09 14:43:52.572][000000027.882] I/user.ble_file_fota 处理写入数据 F000 F002 2FA735BDB0849ECDF7274BAA573388358B7F78CD1F8DD5F64C1A955B430D75A6F465AB75E1E5E04BBF5046F16E0FD8223C68623D8A1195503AE296639EA6618AAEFD7E06DF0BF9C288B03A76FBCBB38C243164461E2B63CCEBDFC2A133C9FF098A354E309FBCFD93AFA80F007CD127295370EC97D2BA52559C7B7FCA3E5DDF5A2224ACDFE41D2E77446E23E344BD73BD84DD30E5C0C74E27D6981DE2E6D725A567266071A805DE5C6A2AA78E80A9278307C777DA72FC057CB1F75A636E0655AEF02F99FEFFDE701C 400
+[2025-12-09 14:43:52.588][000000027.883] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:52.595][000000027.883] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:52.605][000000027.883] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:52.615][000000027.883] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:52.626][000000027.908] I/user.FOTA_DATA 数据写入成功,当前总计: 2800 字节
+[2025-12-09 14:43:52.686][000000028.051] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:52.697][000000028.052] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:52.708][000000028.052] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:52.717][000000028.052] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:52.727][000000028.052] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:52.738][000000028.054] I/user.BLE 收到写请求: F000 F002 6DAB482BEA2153C5381AE0282FF0C0EE3064D10F71F1F1C578D00E75833EAF0F3877DA8F09749CD10ED7E3582E6115D9383D06ECD21AA7C60BDB754624F53D4790D8C3541D1C8F4C9CFA92AFF9292642225D02D02097EC6725A8DEEAB878E5C66E659FDFC7D0FF13E1C06F34BFA967A7C81EB0851F9FFAB4A77D0DE110F6B0D1E8B8193B95862EDF0A6F23D6443ED26B094ACA3757391B5726D00E0EAFBF63685AD9C9FE5595F7814223AF6BF4D50899B622BD7AC11C95816A2540E8A043F96CECC53401DA8FA560
+[2025-12-09 14:43:52.748][000000028.054] I/user.ble_file_fota 处理写入数据 F000 F002 6DAB482BEA2153C5381AE0282FF0C0EE3064D10F71F1F1C578D00E75833EAF0F3877DA8F09749CD10ED7E3582E6115D9383D06ECD21AA7C60BDB754624F53D4790D8C3541D1C8F4C9CFA92AFF9292642225D02D02097EC6725A8DEEAB878E5C66E659FDFC7D0FF13E1C06F34BFA967A7C81EB0851F9FFAB4A77D0DE110F6B0D1E8B8193B95862EDF0A6F23D6443ED26B094ACA3757391B5726D00E0EAFBF63685AD9C9FE5595F7814223AF6BF4D50899B622BD7AC11C95816A2540E8A043F96CECC53401DA8FA560 400
+[2025-12-09 14:43:52.762][000000028.055] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:52.771][000000028.055] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:52.784][000000028.055] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:52.797][000000028.056] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:52.809][000000028.073] I/user.FOTA_DATA 数据写入成功,当前总计: 3000 字节
+[2025-12-09 14:43:52.872][000000028.225] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:52.884][000000028.225] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:52.893][000000028.226] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:52.903][000000028.226] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:52.913][000000028.226] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:52.927][000000028.227] I/user.BLE 收到写请求: F000 F002 5D925AA7B4D0D97993AF1C53DA1D092D141720A0AB7566B279B9A29604B76DF2E578D023EAE481D1784E925597DF2B82E234CF0FF4B3ECF2008316507EC2CEBD3D2D6967EE8984E000A1B3E44CABD30F0F66AF74B79107365098498346D57A6BC34832118FD2D30153BB266CA6C43B07E234A7BC228DC4A3DDF35DE4A4CAD682BCB32783933BFAB42500EAFE06E36090D1D618201A0793478F8A331EDAEA26879FD47164296C362C8562290B9A1358003F86440211516DE60781D924175FB7038B101EDA72E2FF7E
+[2025-12-09 14:43:52.939][000000028.229] I/user.ble_file_fota 处理写入数据 F000 F002 5D925AA7B4D0D97993AF1C53DA1D092D141720A0AB7566B279B9A29604B76DF2E578D023EAE481D1784E925597DF2B82E234CF0FF4B3ECF2008316507EC2CEBD3D2D6967EE8984E000A1B3E44CABD30F0F66AF74B79107365098498346D57A6BC34832118FD2D30153BB266CA6C43B07E234A7BC228DC4A3DDF35DE4A4CAD682BCB32783933BFAB42500EAFE06E36090D1D618201A0793478F8A331EDAEA26879FD47164296C362C8562290B9A1358003F86440211516DE60781D924175FB7038B101EDA72E2FF7E 400
+[2025-12-09 14:43:52.951][000000028.229] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:52.960][000000028.230] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:52.970][000000028.230] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:52.978][000000028.230] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:52.990][000000028.248] I/user.FOTA_DATA 数据写入成功,当前总计: 3200 字节
+[2025-12-09 14:43:53.090][000000028.445] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:53.098][000000028.445] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:53.108][000000028.446] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:53.122][000000028.446] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:53.129][000000028.446] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:53.138][000000028.448] I/user.BLE 收到写请求: F000 F002 C6ED9352E29483FE74144CFA5D8D849E2A9AFCDE915098F3F7A5BCC8555431EDC4924AB94FD102FBF554BFA79E070C1F5545AA2E923D06EAFA23FE90E9AB180331E5B7377B4C8B5B2C9AA19F7A6B20CE9B4A1F44DDDE5C1D2E192DE9D4657511DC7562465F2744CD117B2982FBC4FD85E62F1E06C142EF00CBE4236E9BD25A6B6F69E77A2C9C55C1156E82175C32F2FDC1D84670D86B8E7F13652BC15725A3EDA5C3C2C29A7A1DBDCE2A9DCE5658536BCD015DD658CBAFF5516D2AA48A505190A393FC1BE5EC8C9B
+[2025-12-09 14:43:53.146][000000028.448] I/user.ble_file_fota 处理写入数据 F000 F002 C6ED9352E29483FE74144CFA5D8D849E2A9AFCDE915098F3F7A5BCC8555431EDC4924AB94FD102FBF554BFA79E070C1F5545AA2E923D06EAFA23FE90E9AB180331E5B7377B4C8B5B2C9AA19F7A6B20CE9B4A1F44DDDE5C1D2E192DE9D4657511DC7562465F2744CD117B2982FBC4FD85E62F1E06C142EF00CBE4236E9BD25A6B6F69E77A2C9C55C1156E82175C32F2FDC1D84670D86B8E7F13652BC15725A3EDA5C3C2C29A7A1DBDCE2A9DCE5658536BCD015DD658CBAFF5516D2AA48A505190A393FC1BE5EC8C9B 400
+[2025-12-09 14:43:53.156][000000028.449] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:53.164][000000028.449] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:53.174][000000028.449] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:53.184][000000028.450] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:53.192][000000028.468] I/user.FOTA_DATA 数据写入成功,当前总计: 3400 字节
+[2025-12-09 14:43:53.260][000000028.620] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:53.269][000000028.621] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:53.280][000000028.621] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:53.291][000000028.621] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:53.300][000000028.621] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:53.313][000000028.623] I/user.BLE 收到写请求: F000 F002 A70F29FAA6721203B03486B6A99CAAC0FFFD66C89C5D4C9ED13CA10C36792C3DA8018B478CBEBFB7157D420D4A6AEF76ABFC111C64077F374AF3111AE30997393F04720EAFEC3B7578DC816FFE893C450DFD1440973A4CEFC1D78D9C682EB5F5386EE60498C89DDD43C3655CC63794CEC9B40CECC537E859C5B0DE47DBE84F9CB48FA7BBF039BEA4A8BF661ADEA46FFFBC5C43AC8FC1D7AAB02C0F7E81C5413F803B2CC3F21F1CDD0C4DB9055F31C22B94290C3D5FDB960DDC324EEE9F63AD9D0503EF65EE0C555B
+[2025-12-09 14:43:53.321][000000028.624] I/user.ble_file_fota 处理写入数据 F000 F002 A70F29FAA6721203B03486B6A99CAAC0FFFD66C89C5D4C9ED13CA10C36792C3DA8018B478CBEBFB7157D420D4A6AEF76ABFC111C64077F374AF3111AE30997393F04720EAFEC3B7578DC816FFE893C450DFD1440973A4CEFC1D78D9C682EB5F5386EE60498C89DDD43C3655CC63794CEC9B40CECC537E859C5B0DE47DBE84F9CB48FA7BBF039BEA4A8BF661ADEA46FFFBC5C43AC8FC1D7AAB02C0F7E81C5413F803B2CC3F21F1CDD0C4DB9055F31C22B94290C3D5FDB960DDC324EEE9F63AD9D0503EF65EE0C555B 400
+[2025-12-09 14:43:53.332][000000028.624] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:53.343][000000028.625] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:53.352][000000028.625] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:53.363][000000028.625] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:53.376][000000028.646] I/user.FOTA_DATA 数据写入成功,当前总计: 3600 字节
+[2025-12-09 14:43:53.432][000000028.793] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:53.440][000000028.793] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:53.451][000000028.794] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:53.460][000000028.794] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:53.468][000000028.794] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:53.477][000000028.795] I/user.BLE 收到写请求: F000 F002 BA6E163C2CD19D7647F897C4C0AC4A444B786E9515E397BCDB2B9BC041396371F39B5A8424D38A207EA5F50D0459914BA061BB99D2878C2766E9202A3561E3F452EFBF851081AA69A47B4BDB1BA016DAD805870C608895DE829582F0819AD58E57A958239CD5D6CAFB9CF9A394147BC601E44E4552C81B2360B5BC1B0D072E8C78076E91C699C048284D1A42B278FDF25FDDE90747B24A439601CA191D4FF8D836048BBB636E77B3C01B882AF7835E1430B95DBBDE0ECE12A8F1CC3EB593F68BC5D2D4C8D62A29A0
+[2025-12-09 14:43:53.488][000000028.796] I/user.ble_file_fota 处理写入数据 F000 F002 BA6E163C2CD19D7647F897C4C0AC4A444B786E9515E397BCDB2B9BC041396371F39B5A8424D38A207EA5F50D0459914BA061BB99D2878C2766E9202A3561E3F452EFBF851081AA69A47B4BDB1BA016DAD805870C608895DE829582F0819AD58E57A958239CD5D6CAFB9CF9A394147BC601E44E4552C81B2360B5BC1B0D072E8C78076E91C699C048284D1A42B278FDF25FDDE90747B24A439601CA191D4FF8D836048BBB636E77B3C01B882AF7835E1430B95DBBDE0ECE12A8F1CC3EB593F68BC5D2D4C8D62A29A0 400
+[2025-12-09 14:43:53.501][000000028.797] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:53.512][000000028.797] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:53.521][000000028.797] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:53.531][000000028.797] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:53.543][000000028.817] I/user.FOTA_DATA 数据写入成功,当前总计: 3800 字节
+[2025-12-09 14:43:53.619][000000028.970] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:53.627][000000028.970] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:53.641][000000028.971] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:53.657][000000028.971] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:53.667][000000028.971] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:53.676][000000028.972] I/user.BLE 收到写请求: F000 F002 0F4478DEB2E9F8BD5AC553678D96E05AB73C3323247D56BEE527EA8FCC6F3796D065F6696779BAB46F7A33A98EF7156E8E620516936A8EC48701DD50660E84879BD8F89DB833C7F5797D085ADAAEEA64F93EE711E56DE028A5DC6746E8BAD577630F8268F7294159A50E81C7AAAE036347BD4B02ACD4DD9850EF2B59EB91A75CC6F1C6FCAA39A14D3749C62E6BD0C24C5BE7B30BF20DBA13F34DD4C48E382F7029F18B0F17C5D39CA50902A5B55B733D3860DC9317515AF65A444DAD1404B924FAB5A0143AB597D5
+[2025-12-09 14:43:53.689][000000028.973] I/user.ble_file_fota 处理写入数据 F000 F002 0F4478DEB2E9F8BD5AC553678D96E05AB73C3323247D56BEE527EA8FCC6F3796D065F6696779BAB46F7A33A98EF7156E8E620516936A8EC48701DD50660E84879BD8F89DB833C7F5797D085ADAAEEA64F93EE711E56DE028A5DC6746E8BAD577630F8268F7294159A50E81C7AAAE036347BD4B02ACD4DD9850EF2B59EB91A75CC6F1C6FCAA39A14D3749C62E6BD0C24C5BE7B30BF20DBA13F34DD4C48E382F7029F18B0F17C5D39CA50902A5B55B733D3860DC9317515AF65A444DAD1404B924FAB5A0143AB597D5 400
+[2025-12-09 14:43:53.699][000000028.974] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:53.710][000000028.974] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:53.725][000000028.974] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:53.738][000000028.974] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:53.749][000000028.995] I/user.FOTA_DATA 数据写入成功,当前总计: 4000 字节
+[2025-12-09 14:43:53.790][000000029.147] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:53.799][000000029.147] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:53.812][000000029.148] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:53.823][000000029.148] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:53.832][000000029.148] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:53.842][000000029.149] I/user.BLE 收到写请求: F000 F002 A3D259FCC5EB3774A2586E5D400FEADA077435A68F6E29B78604E1E26DAFAADBFFB5477713DB9F1531AEF4E587BC82CDCA6098B2DFFAB6CF7559B3E3D6911C5D50DE4B340E630A38C44E652DD5FC48CB7DE00CC83C07E2050BBD56F046579EB4608DDF9B632B9AC1F8EF1054021F4706112B9C143AB85ECE86B631D656826C7EB53468CF860322915FE9009A7492AFCE2E8F5E4DE18FE3D32E7ED56B4DA55D8B0C94AA319A7F48569D17E9BCBCD579E31BBE9958D56B13546ACA87B156569A9AFD200955E6ABB487
+[2025-12-09 14:43:53.853][000000029.150] I/user.ble_file_fota 处理写入数据 F000 F002 A3D259FCC5EB3774A2586E5D400FEADA077435A68F6E29B78604E1E26DAFAADBFFB5477713DB9F1531AEF4E587BC82CDCA6098B2DFFAB6CF7559B3E3D6911C5D50DE4B340E630A38C44E652DD5FC48CB7DE00CC83C07E2050BBD56F046579EB4608DDF9B632B9AC1F8EF1054021F4706112B9C143AB85ECE86B631D656826C7EB53468CF860322915FE9009A7492AFCE2E8F5E4DE18FE3D32E7ED56B4DA55D8B0C94AA319A7F48569D17E9BCBCD579E31BBE9958D56B13546ACA87B156569A9AFD200955E6ABB487 400
+[2025-12-09 14:43:53.861][000000029.151] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:53.873][000000029.151] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:53.882][000000029.151] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:53.890][000000029.152] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:53.902][000000029.186] I/user.FOTA_DATA 数据写入成功,当前总计: 4200 字节
+[2025-12-09 14:43:53.961][000000029.320] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:53.972][000000029.320] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:53.982][000000029.321] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:53.993][000000029.321] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:54.005][000000029.321] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:54.017][000000029.323] I/user.BLE 收到写请求: F000 F002 6BE5E55BDC5204A19BFE2E7C205B5BE81F683ADCD8ACD5A202DBF521EC0C1EBBDCA27409534EFD7CCE4AEEF3CA58C8543C6AF43C86F2F6485A9AB4B544D9C19D83A989A05D8994F154001A5F542439910754539430A2688B69873D4168FB5EB756D6900770B03051B92799ADE2BC52655B1BAF9A14D03274EBC4C330C19DA0E6FF34ECA92F43CA5D50CE7B226D345520EA49F859681CC1F46F800696D2015E72EFEE01403414DF534740FF5B80F63959173834C43D7B037629A98051F8A0EAB7D101F0707797EA9A
+[2025-12-09 14:43:54.029][000000029.323] I/user.ble_file_fota 处理写入数据 F000 F002 6BE5E55BDC5204A19BFE2E7C205B5BE81F683ADCD8ACD5A202DBF521EC0C1EBBDCA27409534EFD7CCE4AEEF3CA58C8543C6AF43C86F2F6485A9AB4B544D9C19D83A989A05D8994F154001A5F542439910754539430A2688B69873D4168FB5EB756D6900770B03051B92799ADE2BC52655B1BAF9A14D03274EBC4C330C19DA0E6FF34ECA92F43CA5D50CE7B226D345520EA49F859681CC1F46F800696D2015E72EFEE01403414DF534740FF5B80F63959173834C43D7B037629A98051F8A0EAB7D101F0707797EA9A 400
+[2025-12-09 14:43:54.038][000000029.324] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:54.048][000000029.324] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:54.057][000000029.325] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:54.068][000000029.325] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:54.078][000000029.385] I/user.FOTA_DATA 数据写入成功,当前总计: 4400 字节
+[2025-12-09 14:43:54.133][000000029.497] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:54.142][000000029.497] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:54.152][000000029.498] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:54.161][000000029.498] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:54.172][000000029.498] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:54.182][000000029.499] I/user.BLE 收到写请求: F000 F002 D7DA355A9083EC81CC3C089D1C9BA096727287A2F7F602CE5522E7590298B738600571BBEEE96A32941EF147EA8A77097838974851483E37C861B14DB7CE8761152A56E49C515D11923B97AC78E02F1ADFA6DFAC1CDF2405D3B7E6DE41C2C97725F1015EF748DA755A10905024A4D721535B3522838050DBA769090E5D9008B8C31F312C20A7A1F3EFED4987AF28BEE4C03E8FCBC8625CF17DE0B4BDB28A71F2AFD9C1F6E624A5890F1D6CB75526E2E512A695F3D99DC6D4452A4FBE7251F10AAC794C5374C5BE99
+[2025-12-09 14:43:54.190][000000029.500] I/user.ble_file_fota 处理写入数据 F000 F002 D7DA355A9083EC81CC3C089D1C9BA096727287A2F7F602CE5522E7590298B738600571BBEEE96A32941EF147EA8A77097838974851483E37C861B14DB7CE8761152A56E49C515D11923B97AC78E02F1ADFA6DFAC1CDF2405D3B7E6DE41C2C97725F1015EF748DA755A10905024A4D721535B3522838050DBA769090E5D9008B8C31F312C20A7A1F3EFED4987AF28BEE4C03E8FCBC8625CF17DE0B4BDB28A71F2AFD9C1F6E624A5890F1D6CB75526E2E512A695F3D99DC6D4452A4FBE7251F10AAC794C5374C5BE99 400
+[2025-12-09 14:43:54.202][000000029.501] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:54.215][000000029.501] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:54.223][000000029.501] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:54.237][000000029.502] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:54.248][000000029.513] I/user.FOTA_DATA 数据写入成功,当前总计: 4600 字节
+[2025-12-09 14:43:54.305][000000029.669] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:54.318][000000029.669] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:54.330][000000029.670] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:54.342][000000029.670] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:54.352][000000029.670] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:54.364][000000029.672] I/user.BLE 收到写请求: F000 F002 F5392A156093A3FC708F821DAA16652202A12307D226875F97319CD5A92B65A6314E5519BD42E5DB61F14C8DA38A84ADDE240D50AF43B04DD28A11BE7380BD17EF28101AF040CD8D18236076516280E6D1C4715BB4331D3B0678F82E3DA831F471517209FD6092D7C193A92F1FAD1596E77AE0FE3EA6551BF45C3B9CB79991DFB7135CD6B9FFA8F81E6BABE6F979350DF254C76EB3700B4FC6FCD7C64A48301B9651BE65B6D8344AB5FF077966352CD781598D2A22E1670BE58914F441126E5EE00E86EB80473F02
+[2025-12-09 14:43:54.377][000000029.672] I/user.ble_file_fota 处理写入数据 F000 F002 F5392A156093A3FC708F821DAA16652202A12307D226875F97319CD5A92B65A6314E5519BD42E5DB61F14C8DA38A84ADDE240D50AF43B04DD28A11BE7380BD17EF28101AF040CD8D18236076516280E6D1C4715BB4331D3B0678F82E3DA831F471517209FD6092D7C193A92F1FAD1596E77AE0FE3EA6551BF45C3B9CB79991DFB7135CD6B9FFA8F81E6BABE6F979350DF254C76EB3700B4FC6FCD7C64A48301B9651BE65B6D8344AB5FF077966352CD781598D2A22E1670BE58914F441126E5EE00E86EB80473F02 400
+[2025-12-09 14:43:54.387][000000029.673] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:54.397][000000029.673] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:54.410][000000029.673] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:54.419][000000029.674] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:54.428][000000029.688] I/user.FOTA_DATA 数据写入成功,当前总计: 4800 字节
+[2025-12-09 14:43:54.523][000000029.884] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:54.534][000000029.885] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:54.546][000000029.885] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:54.557][000000029.885] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:54.568][000000029.885] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:54.582][000000029.887] I/user.BLE 收到写请求: F000 F002 A79FFA80A5085511747E3225520E5E7EA9EB4197F064C508002E59A86BE8E01D452C4D901B5750A2F22B3F2C06EAA13EDC89CA3BA47519C06AC0314F12E502A56ABD7C0BFF6B38554945124DCC89A91303EABF303E9E9A770F356F1C40556467439AB6EF85EAC9BCA0400889115454620D304AF5E0DBA8F6B081501D850F7E9C773841683C448C7F88DA350A54C78FAB18FE3EC6EB9F815B5AE4D275949978C7B715E6EE1A3CD1089A562E013D1F8F06B82ED55ADA0807A8B3A569BD3571F314D31E7E15FD127C1A
+[2025-12-09 14:43:54.593][000000029.887] I/user.ble_file_fota 处理写入数据 F000 F002 A79FFA80A5085511747E3225520E5E7EA9EB4197F064C508002E59A86BE8E01D452C4D901B5750A2F22B3F2C06EAA13EDC89CA3BA47519C06AC0314F12E502A56ABD7C0BFF6B38554945124DCC89A91303EABF303E9E9A770F356F1C40556467439AB6EF85EAC9BCA0400889115454620D304AF5E0DBA8F6B081501D850F7E9C773841683C448C7F88DA350A54C78FAB18FE3EC6EB9F815B5AE4D275949978C7B715E6EE1A3CD1089A562E013D1F8F06B82ED55ADA0807A8B3A569BD3571F314D31E7E15FD127C1A 400
+[2025-12-09 14:43:54.606][000000029.888] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:54.615][000000029.888] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:54.628][000000029.889] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:54.637][000000029.889] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:54.646][000000029.903] I/user.FOTA_DATA 数据写入成功,当前总计: 5000 字节
+[2025-12-09 14:43:54.711][000000030.065] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:54.718][000000030.065] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:54.727][000000030.066] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:54.737][000000030.066] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:54.746][000000030.066] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:54.754][000000030.068] I/user.BLE 收到写请求: F000 F002 2039CF1E3E8982802E6F4EF78A2F8937794B315244FED3E2BFF073BCD2295FDF5389962FFCAB8366426A2761C52660A65E1F1DE4A43755B931638B893755C50FB98AEDE6C47A0F1FB7409F35E9D643B92D2FFFB83B7D4DF089A5B4D59E11F4C3EE1880FAFBA4734084172B980851667F0D9B4CAE42D0201B2EB499C54081C88D4047AF4FA7F78942DE71EA1F83ED968C06FA3A9535A84161E68CD2BD1AC86DF97D10FD024D6EC66716F0813BA243F9BACB624F68B7D4EBF478B0761F5CA0F3017703B761E054E4B4
+[2025-12-09 14:43:54.766][000000030.069] I/user.ble_file_fota 处理写入数据 F000 F002 2039CF1E3E8982802E6F4EF78A2F8937794B315244FED3E2BFF073BCD2295FDF5389962FFCAB8366426A2761C52660A65E1F1DE4A43755B931638B893755C50FB98AEDE6C47A0F1FB7409F35E9D643B92D2FFFB83B7D4DF089A5B4D59E11F4C3EE1880FAFBA4734084172B980851667F0D9B4CAE42D0201B2EB499C54081C88D4047AF4FA7F78942DE71EA1F83ED968C06FA3A9535A84161E68CD2BD1AC86DF97D10FD024D6EC66716F0813BA243F9BACB624F68B7D4EBF478B0761F5CA0F3017703B761E054E4B4 400
+[2025-12-09 14:43:54.775][000000030.069] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:54.782][000000030.070] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:54.791][000000030.073] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:54.799][000000030.073] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:54.808][000000030.091] I/user.FOTA_DATA 数据写入成功,当前总计: 5200 字节
+[2025-12-09 14:43:54.882][000000030.238] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:54.890][000000030.238] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:54.905][000000030.239] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:54.914][000000030.239] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:54.921][000000030.239] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:54.931][000000030.241] I/user.BLE 收到写请求: F000 F002 25A7790B57C027D592D7C8C2BDCCBDD92322073E65330C3CCFE304B2A09197AB526EB517FEA520F87511FA17588F727D09994EFCB1945DCDAAAFEA1EC1A57B56C929FC47DAE333CF469ED2A3932DEE0F13D2B8D8DE28FDF4371AE9B89185486E88197B68BFCCDB392C256D1E8EF08F2D4D905D3D4E6B7326277F0A7F5BDB463C93500D630D0D8D715B826297DE265B0D7A81A82F6D06319E01D61DBBBC639A846094CE433EEFFF44B2EBA36E56CDFA2A0E100229DA1C5685A19D7E30AA8A5CD16F3FB4E6F2EF0606
+[2025-12-09 14:43:54.939][000000030.241] I/user.ble_file_fota 处理写入数据 F000 F002 25A7790B57C027D592D7C8C2BDCCBDD92322073E65330C3CCFE304B2A09197AB526EB517FEA520F87511FA17588F727D09994EFCB1945DCDAAAFEA1EC1A57B56C929FC47DAE333CF469ED2A3932DEE0F13D2B8D8DE28FDF4371AE9B89185486E88197B68BFCCDB392C256D1E8EF08F2D4D905D3D4E6B7326277F0A7F5BDB463C93500D630D0D8D715B826297DE265B0D7A81A82F6D06319E01D61DBBBC639A846094CE433EEFFF44B2EBA36E56CDFA2A0E100229DA1C5685A19D7E30AA8A5CD16F3FB4E6F2EF0606 400
+[2025-12-09 14:43:54.949][000000030.242] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:54.957][000000030.242] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:54.967][000000030.242] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:54.978][000000030.243] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:54.987][000000030.256] I/user.FOTA_DATA 数据写入成功,当前总计: 5400 字节
+[2025-12-09 14:43:55.021][000000030.383] I/user.fota version 001.000.000
+[2025-12-09 14:43:55.053][000000030.414] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:55.063][000000030.414] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:55.076][000000030.415] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:55.090][000000030.415] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:55.104][000000030.415] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:55.114][000000030.416] I/user.BLE 收到写请求: F000 F002 F866A14308D15F799B57A465462922B99F42A0EF8911C9A6CAF5F4BAB34A7DC4909E70E3E0D07A70984726604001FC9BE18E1D973B57FD29C8D29EB6830821BC63B59F0501ED48A33F83433589DEA55C7B42516996A1C0FC930C00456BB72B9096F1525E3054C97B15D820B30D629882D054AFD396757A5CBF722303312D30F897D9C5906F87BDA16BB377CD2901DD5D31853757A2C0DCF856DF62D0BFE8D00C3CF221F7E0D05A057C7F6C9528A2DC9119EACE8FD76D4B8BB2CE5233CC46239BC8E96600E5431810
+[2025-12-09 14:43:55.124][000000030.417] I/user.ble_file_fota 处理写入数据 F000 F002 F866A14308D15F799B57A465462922B99F42A0EF8911C9A6CAF5F4BAB34A7DC4909E70E3E0D07A70984726604001FC9BE18E1D973B57FD29C8D29EB6830821BC63B59F0501ED48A33F83433589DEA55C7B42516996A1C0FC930C00456BB72B9096F1525E3054C97B15D820B30D629882D054AFD396757A5CBF722303312D30F897D9C5906F87BDA16BB377CD2901DD5D31853757A2C0DCF856DF62D0BFE8D00C3CF221F7E0D05A057C7F6C9528A2DC9119EACE8FD76D4B8BB2CE5233CC46239BC8E96600E5431810 400
+[2025-12-09 14:43:55.141][000000030.418] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:55.159][000000030.418] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:55.174][000000030.418] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:55.186][000000030.419] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:55.203][000000030.435] I/user.FOTA_DATA 数据写入成功,当前总计: 5600 字节
+[2025-12-09 14:43:55.271][000000030.632] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:55.279][000000030.632] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:55.289][000000030.633] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:55.299][000000030.633] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:55.309][000000030.633] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:55.321][000000030.635] I/user.BLE 收到写请求: F000 F002 273DCFAB5EB90608BD68CAE92F476307ED47CDB1FADB8F59AF83250DA720FD56B627A93D781456D69C19065CEC4130386A9817A0CFFBCF475779C15E22BC0E01D7C6DC447E315EA16CB207B814AD2035A4E8D403F3EEA12EDF088D8D32ABD17E77C44A62669DFE802CF1D876AE0BA4500F8BC0B1A83CFCA11B8828367D36BC7DC13F8C5D407E01F5B5D63B697F450A23BAC77868E86AE6D15F7605CD2AD4327A12367F4894071C247E88950D6B67DB33DBF1D97C3CFEF90059E74ECFD17584B08219639DC025B361
+[2025-12-09 14:43:55.331][000000030.635] I/user.ble_file_fota 处理写入数据 F000 F002 273DCFAB5EB90608BD68CAE92F476307ED47CDB1FADB8F59AF83250DA720FD56B627A93D781456D69C19065CEC4130386A9817A0CFFBCF475779C15E22BC0E01D7C6DC447E315EA16CB207B814AD2035A4E8D403F3EEA12EDF088D8D32ABD17E77C44A62669DFE802CF1D876AE0BA4500F8BC0B1A83CFCA11B8828367D36BC7DC13F8C5D407E01F5B5D63B697F450A23BAC77868E86AE6D15F7605CD2AD4327A12367F4894071C247E88950D6B67DB33DBF1D97C3CFEF90059E74ECFD17584B08219639DC025B361 400
+[2025-12-09 14:43:55.342][000000030.636] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:55.348][000000030.636] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:55.358][000000030.636] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:55.368][000000030.637] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:55.378][000000030.652] I/user.FOTA_DATA 数据写入成功,当前总计: 5800 字节
+[2025-12-09 14:43:55.443][000000030.808] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:55.451][000000030.808] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:55.461][000000030.809] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:55.469][000000030.809] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:55.480][000000030.809] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:55.487][000000030.810] I/user.BLE 收到写请求: F000 F002 E16CA5457002B97D974F7F2314C9B30D52D58590364540B48C25334DED75C8F2395E52ABE5DFAA4A09A4066CA468E096201482BDBFE1686DF0EA1A27CEAC722337CCD7B98D0B3074B4C55D44C9D749D0A88F14CFD2247412C2F025DD7208C3E9BCDCCE2D9ED8CEA0EF23DD04CC780683C2A5895AFA3731DBAA3A450C6FB5E11D2BE7DE651F75AB461427C26881CB139B0A7D276B8569B1C8D5ADCCC11625652DEEDEE827C5B29A80F19ADE004DFC8F21BD87C6AA7DCB4F60B6E162CAF9A5E62886A49C50FBBAF959
+[2025-12-09 14:43:55.501][000000030.811] I/user.ble_file_fota 处理写入数据 F000 F002 E16CA5457002B97D974F7F2314C9B30D52D58590364540B48C25334DED75C8F2395E52ABE5DFAA4A09A4066CA468E096201482BDBFE1686DF0EA1A27CEAC722337CCD7B98D0B3074B4C55D44C9D749D0A88F14CFD2247412C2F025DD7208C3E9BCDCCE2D9ED8CEA0EF23DD04CC780683C2A5895AFA3731DBAA3A450C6FB5E11D2BE7DE651F75AB461427C26881CB139B0A7D276B8569B1C8D5ADCCC11625652DEEDEE827C5B29A80F19ADE004DFC8F21BD87C6AA7DCB4F60B6E162CAF9A5E62886A49C50FBBAF959 400
+[2025-12-09 14:43:55.513][000000030.812] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:55.524][000000030.812] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:55.535][000000030.812] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:55.547][000000030.812] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:55.559][000000030.828] I/user.FOTA_DATA 数据写入成功,当前总计: 6000 字节
+[2025-12-09 14:43:55.614][000000030.980] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:55.624][000000030.980] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:55.636][000000030.981] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:55.644][000000030.981] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:55.655][000000030.981] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:55.666][000000030.982] I/user.BLE 收到写请求: F000 F002 8D6EFD873A5DAC4E398D88153FE9218962D9E75EFD381E5DD1C0B7A3FF565F3758352CD44841F5FFCA829E825ECABC359B996C6C0497D406DC6C5A81C43F941F2498AC674911BECFCBBD1A096B26C705BAA46EFE9EA3B59DFB364C463D990C6674FB2AAB8A5CFA01F8B234EEE49313B13AD3374C00AACA533D86C100E758A0ABBBB390D34FAC4C073C761AFA5B4FB0202B10E52E60A001CFA8F47BF9A82708CB2117C0B3A7C0A7DE779A4053DCA875105ACDEFC504C8BD3EE679ADFD46E18DCCA5BCDE70D7090A79
+[2025-12-09 14:43:55.675][000000030.983] I/user.ble_file_fota 处理写入数据 F000 F002 8D6EFD873A5DAC4E398D88153FE9218962D9E75EFD381E5DD1C0B7A3FF565F3758352CD44841F5FFCA829E825ECABC359B996C6C0497D406DC6C5A81C43F941F2498AC674911BECFCBBD1A096B26C705BAA46EFE9EA3B59DFB364C463D990C6674FB2AAB8A5CFA01F8B234EEE49313B13AD3374C00AACA533D86C100E758A0ABBBB390D34FAC4C073C761AFA5B4FB0202B10E52E60A001CFA8F47BF9A82708CB2117C0B3A7C0A7DE779A4053DCA875105ACDEFC504C8BD3EE679ADFD46E18DCCA5BCDE70D7090A79 400
+[2025-12-09 14:43:55.689][000000030.984] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:55.700][000000030.984] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:55.708][000000030.984] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:55.718][000000030.984] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:55.729][000000030.999] I/user.FOTA_DATA 数据写入成功,当前总计: 6200 字节
+[2025-12-09 14:43:55.804][000000031.157] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:55.811][000000031.157] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:55.818][000000031.158] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:55.832][000000031.158] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:55.842][000000031.158] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:55.855][000000031.160] I/user.BLE 收到写请求: F000 F002 4A63D97FF5A93D477A75BD3B62DDB3F27D9657F6CE01F41645B735B2C4697B78198700C2738FAEF9A07E119AF56D0347F3166FC67AC5B90C7B5CD241EA3A3C52D0C7E186846785ED377AD9BD4F6F12FA208C5E108B246037653A371224C4255F8BAA9873EC667F632072FF09B42E3D7865D8AC39B80A0505ACB6AE952B265F8BAD0EAEC701EBA1BB752BC7E885A4F62C769F3E943CC7448E9F189F924F62B5C06FE950E992CF638FC999A42AE824046EF1F29A4F8EAF77E6B707E0E2F9CD88A317D8237DC676F68C
+[2025-12-09 14:43:55.866][000000031.160] I/user.ble_file_fota 处理写入数据 F000 F002 4A63D97FF5A93D477A75BD3B62DDB3F27D9657F6CE01F41645B735B2C4697B78198700C2738FAEF9A07E119AF56D0347F3166FC67AC5B90C7B5CD241EA3A3C52D0C7E186846785ED377AD9BD4F6F12FA208C5E108B246037653A371224C4255F8BAA9873EC667F632072FF09B42E3D7865D8AC39B80A0505ACB6AE952B265F8BAD0EAEC701EBA1BB752BC7E885A4F62C769F3E943CC7448E9F189F924F62B5C06FE950E992CF638FC999A42AE824046EF1F29A4F8EAF77E6B707E0E2F9CD88A317D8237DC676F68C 400
+[2025-12-09 14:43:55.877][000000031.161] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:55.888][000000031.161] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:55.902][000000031.162] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:55.910][000000031.162] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:55.922][000000031.229] I/user.FOTA_DATA 数据写入成功,当前总计: 6400 字节
+[2025-12-09 14:43:55.978][000000031.332] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:55.988][000000031.332] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:55.999][000000031.333] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:56.010][000000031.333] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:56.018][000000031.333] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:43:56.032][000000031.334] I/user.BLE 收到写请求: F000 F002 3AFFDF6E0E4B066DE6A80AF26C638752480CB2805DB597D7DBC8517F9BF383F6EAB90AA871C7C3BBEC893F5EF83F9A11DC2A6B037C20695FA1A612FBC4EFF0C3C73B06241AD49330A226465C0705572BEB57ABE04034739641974ED16ACB06C6AB6036E04EC55EDC011B4ECC42D44D0B0160F7F8EE32BB034AED332E7D18782113D98858FC681DE3EA6A0B08ABAD712D4ED878BBD3935179A6434D9E21040BDCF1777F923F706F9B7440CB5D58784F28891CDE4E9356B4062B7AAED398A5727762BA10429134D006
+[2025-12-09 14:43:56.043][000000031.335] I/user.ble_file_fota 处理写入数据 F000 F002 3AFFDF6E0E4B066DE6A80AF26C638752480CB2805DB597D7DBC8517F9BF383F6EAB90AA871C7C3BBEC893F5EF83F9A11DC2A6B037C20695FA1A612FBC4EFF0C3C73B06241AD49330A226465C0705572BEB57ABE04034739641974ED16ACB06C6AB6036E04EC55EDC011B4ECC42D44D0B0160F7F8EE32BB034AED332E7D18782113D98858FC681DE3EA6A0B08ABAD712D4ED878BBD3935179A6434D9E21040BDCF1777F923F706F9B7440CB5D58784F28891CDE4E9356B4062B7AAED398A5727762BA10429134D006 400
+[2025-12-09 14:43:56.051][000000031.336] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:56.064][000000031.336] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:56.075][000000031.336] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:43:56.082][000000031.336] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:56.092][000000031.354] I/user.FOTA_DATA 数据写入成功,当前总计: 6600 字节
+[2025-12-09 14:43:56.150][000000031.505] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:56.161][000000031.505] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:56.172][000000031.506] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:56.184][000000031.506] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:43:56.193][000000031.506] I/user.BLE_EVENT 数据长度: 191 字节
+[2025-12-09 14:43:56.204][000000031.507] I/user.BLE 收到写请求: F000 F002 707A137AE1B5F44B2AB40344A31ABFD272658424F0012C35BEE76B0869E6C61BE6232D58B976CB6E74F48C6161819EEA21BF257DD2136559E98A6DE63B0B4303FFA3F4C35CB4663B7118ED440B27474855F07C97AEB54B5448E3463A63A6C25DC5FF6ACB48D678E88515E34020A0B36B1D0553CCA70B4DC41BE54010968B6B535E716E1DFCCB500031B675CD1231701CA08E8002D79A07F80BD24D0EDC210AB9739E19461637A86AA4F64C5F7B878180B4DB18516A9D5A1473603E78782700
+[2025-12-09 14:43:56.218][000000031.508] I/user.ble_file_fota 处理写入数据 F000 F002 707A137AE1B5F44B2AB40344A31ABFD272658424F0012C35BEE76B0869E6C61BE6232D58B976CB6E74F48C6161819EEA21BF257DD2136559E98A6DE63B0B4303FFA3F4C35CB4663B7118ED440B27474855F07C97AEB54B5448E3463A63A6C25DC5FF6ACB48D678E88515E34020A0B36B1D0553CCA70B4DC41BE54010968B6B535E716E1DFCCB500031B675CD1231701CA08E8002D79A07F80BD24D0EDC210AB9739E19461637A86AA4F64C5F7B878180B4DB18516A9D5A1473603E78782700 382
+[2025-12-09 14:43:56.231][000000031.509] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:43:56.242][000000031.509] I/user.ble_file_fota 数据特征值匹配,处理数据
+[2025-12-09 14:43:56.251][000000031.509] I/user.FOTA_DATA 收到数据包,长度: 191 字节
+[2025-12-09 14:43:56.259][000000031.509] I/user.FOTA_DATA 写入文件: /ble_fota.bin
+[2025-12-09 14:43:56.275][000000031.526] I/user.FOTA_DATA 升级进度: 100 % ( 6791 / 6791 )
+[2025-12-09 14:43:56.285][000000031.526] I/user.FOTA_DATA 数据写入成功,当前总计: 6791 字节
+[2025-12-09 14:43:56.322][000000031.678] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:43:56.334][000000031.678] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:43:56.345][000000031.679] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:43:56.358][000000031.679] I/user.BLE_EVENT 特征值UUID: F001
+[2025-12-09 14:43:56.373][000000031.679] I/user.BLE_EVENT 数据长度: 1 字节
+[2025-12-09 14:43:56.382][000000031.680] I/user.BLE 收到写请求: F000 F001 02
+[2025-12-09 14:43:56.395][000000031.680] I/user.ble_file_fota 处理写入数据 F000 F001 02 2
+[2025-12-09 14:43:56.408][000000031.681] I/user.ble_file_fota UUID匹配结果: 服务匹配: 1 命令匹配: 1 数据匹配: nil
+[2025-12-09 14:43:56.419][000000031.681] I/user.ble_file_fota 命令特征值匹配,处理命令
+[2025-12-09 14:43:56.427][000000031.682] I/user.FOTA_CMD 收到命令数据: 02 长度: 1
+[2025-12-09 14:43:56.436][000000031.682] I/user.FOTA_CMD 解析命令码: 2 (0x02)
+[2025-12-09 14:43:56.449][000000031.682] I/user.FOTA_CMD 处理结束升级命令
+[2025-12-09 14:43:56.459][000000031.683] I/user.FOTA_CMD 验证文件完整性...
+[2025-12-09 14:43:56.472][000000031.683] I/user.FOTA_CMD 已接收: 6791 字节
+[2025-12-09 14:43:56.484][000000031.683] I/user.FOTA_CMD 应接收: 6791 字节
+[2025-12-09 14:43:56.496][000000031.683] I/user.FOTA_CMD 文件完整性验证通过
+[2025-12-09 14:43:56.509][000000031.684] I/user.FOTA_CMD 开始执行FOTA升级...
+[2025-12-09 14:43:56.521][000000031.686] I/fota write common data
+[2025-12-09 14:43:56.532][000000031.745] I/fota common data done, now checking 0
+[2025-12-09 14:43:56.545][000000031.747] I/fota common data md5 ok
+[2025-12-09 14:43:56.558][000000031.748] I/fota only common data
+[2025-12-09 14:43:56.568][000000031.773] I/fota fota type 0 ok!, wait reboot
+[2025-12-09 14:43:56.582][000000031.774] I/user.FOTA_CMD FOTA升级结果: result: true isDone: true
+[2025-12-09 14:43:56.594][000000031.774] I/user.FOTA_CMD  FOTA升级成功!
+[2025-12-09 14:43:56.608][000000031.775] I/user.FOTA_CMD 2秒后设备将自动重启...
+[2025-12-09 14:43:56.620][000000031.775] I/user.FOTA_CMD 清理升级状态...
+[2025-12-09 14:43:56.631][000000031.778] I/user.FOTA_CMD 已删除临时文件
+[2025-12-09 14:43:56.642][000000031.778] I/user.FOTA_CMD 升级流程结束
+[2025-12-09 14:43:58.032][000000033.383] I/user.fota version 001.000.000
+```
+
+packet 升级方式日志如下:
+```lua
+[2025-12-09 14:58:35.252][000000000.359] I/user.main fota_test 001.000.000
+[2025-12-09 14:58:35.264][000000000.380] I/user.BLE_INIT 开始初始化BLE...
+[2025-12-09 14:58:35.281][000000000.380] D/drv.bt 执行luat_bluetooth_init
+[2025-12-09 14:58:35.294][000000000.381] I/user.BLE_INIT 蓝牙核心初始化成功
+[2025-12-09 14:58:35.314][000000000.381] I/user.BLE_INIT BLE功能初始化成功
+[2025-12-09 14:58:35.330][000000000.382] I/user.BLE_INIT GATT服务创建成功
+[2025-12-09 14:58:35.356][000000000.382] I/user.BLE_INIT 配置广播数据...
+[2025-12-09 14:58:35.388][000000000.383] I/user.BLE_INIT 广播配置成功
+[2025-12-09 14:58:35.419][000000000.383] I/user.BLE_INIT BLE广播已启动,设备名称: Air8000_FOTA
+[2025-12-09 14:58:35.450][000000000.417] I/user.BLE_EVENT 收到BLE事件: 1
+[2025-12-09 14:58:35.475][000000000.417] I/user.BLE_EVENT 其他事件类型: 1
+[2025-12-09 14:58:35.493][000000000.444] I/user.BLE_EVENT 收到BLE事件: 3
+[2025-12-09 14:58:35.506][000000000.445] I/user.BLE_EVENT 其他事件类型: 3
+[2025-12-09 14:58:35.518][000000000.487] I/user.BLE_EVENT 收到BLE事件: 4
+[2025-12-09 14:58:35.532][000000000.488] I/user.BLE_EVENT 其他事件类型: 4
+[2025-12-09 14:58:36.458][000000002.405] D/mobile cid1, state0
+[2025-12-09 14:58:36.470][000000002.406] D/mobile bearer act 0, result 0
+[2025-12-09 14:58:36.482][000000002.406] D/mobile NETIF_LINK_ON -> IP_READY
+[2025-12-09 14:58:36.492][000000002.461] D/mobile TIME_SYNC 0
+[2025-12-09 14:58:37.324][000000003.360] I/user.fota version 001.000.000
+[2025-12-09 14:58:40.317][000000006.360] I/user.fota version 001.000.000
+[2025-12-09 14:58:43.326][000000009.360] I/user.fota version 001.000.000
+[2025-12-09 14:58:46.325][000000012.360] I/user.fota version 001.000.000
+[2025-12-09 14:58:49.321][000000015.360] I/user.fota version 001.000.000
+[2025-12-09 14:58:51.692][000000017.736] I/user.BLE_EVENT 收到BLE事件: 12
+[2025-12-09 14:58:51.699][000000017.737] I/user.BLE_EVENT 设备已连接 地址: 141333C76D92
+[2025-12-09 14:58:51.707][000000017.738] I/user.BLE 设备连接成功: 141333C76D92
+[2025-12-09 14:58:52.315][000000018.360] I/user.fota version 001.000.000
+[2025-12-09 14:58:52.754][000000018.794] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:52.765][000000018.795] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:52.774][000000018.795] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:52.783][000000018.795] I/user.BLE_EVENT 特征值UUID: F001
+[2025-12-09 14:58:52.793][000000018.796] I/user.BLE_EVENT 数据长度: 5 字节
+[2025-12-09 14:58:52.804][000000018.797] I/user.BLE 收到写请求: F000 F001 01871A0000
+[2025-12-09 14:58:52.812][000000018.797] I/user.ble_packet_fota 处理写入数据 F000 F001 01871A0000 10
+[2025-12-09 14:58:52.824][000000018.798] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: 1 数据匹配: nil
+[2025-12-09 14:58:52.836][000000018.798] I/user.ble_packet_fota 命令特征值匹配,处理命令
+[2025-12-09 14:58:52.844][000000018.798] I/user.FOTA_CMD 收到命令数据: 01871A0000 长度: 5
+[2025-12-09 14:58:52.854][000000018.799] I/user.FOTA_CMD 解析命令码: 1 (0x01)
+[2025-12-09 14:58:52.865][000000018.799] I/user.FOTA_CMD 处理开始升级命令
+[2025-12-09 14:58:52.873][000000018.799] I/user.FOTA_CMD 文件总大小: 6791 字节
+[2025-12-09 14:58:52.882][000000018.800] I/user.FOTA_CMD 初始化FOTA子系统...
+[2025-12-09 14:58:52.892][000000018.814] I/user.FOTA_CMD FOTA初始化成功
+[2025-12-09 14:58:52.902][000000018.815] I/user.FOTA_CMD 等待FOTA底层准备...
+[2025-12-09 14:58:52.913][000000018.815] I/user.FOTA_CMD FOTA底层准备就绪
+[2025-12-09 14:58:52.923][000000018.815] I/user.FOTA_CMD 升级状态已设置 总大小: 6791
+[2025-12-09 14:58:52.933][000000018.815] I/user.FOTA_CMD 准备接收固件数据...
+[2025-12-09 14:58:53.859][000000019.900] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:53.867][000000019.901] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:53.879][000000019.901] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:53.889][000000019.901] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:53.902][000000019.902] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:53.912][000000019.903] I/user.BLE 收到写请求: F000 F002 1872C3EA2C25123600000000000000000000000000000000000000000000000000000000000000000000000000000000000000002B1A00000000000024342525E2585E1B996AB9E251859848000000000000000000000000000000001872C3EA071A0000005C00000E4148EF5A243F7A94C524D49C393BE10000040000E05400056600000400FD19000000008109C7536188201E4070E4F8F94AA2C68473C100329390791CEB9BE96EC35E8E58408C2AC66F005FE972F833C5F3DA9A10A24DFFE32A2D8B93D3FA34
+[2025-12-09 14:58:53.924][000000019.904] I/user.ble_packet_fota 处理写入数据 F000 F002 1872C3EA2C25123600000000000000000000000000000000000000000000000000000000000000000000000000000000000000002B1A00000000000024342525E2585E1B996AB9E251859848000000000000000000000000000000001872C3EA071A0000005C00000E4148EF5A243F7A94C524D49C393BE10000040000E05400056600000400FD19000000008109C7536188201E4070E4F8F94AA2C68473C100329390791CEB9BE96EC35E8E58408C2AC66F005FE972F833C5F3DA9A10A24DFFE32A2D8B93D3FA34 400
+[2025-12-09 14:58:53.934][000000019.904] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:53.942][000000019.905] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:53.954][000000019.905] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:53.965][000000019.905] I/user.FOTA_DATA 处理分段数据,包序号: 0
+[2025-12-09 14:58:53.974][000000019.906] I/fota write common data
+[2025-12-09 14:58:53.981][000000019.906] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:53.992][000000019.906] I/user.FOTA_DATA 数据写入成功,当前总计: 200 字节
+[2025-12-09 14:58:54.004][000000020.033] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:54.014][000000020.034] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:54.030][000000020.034] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:54.038][000000020.034] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:54.049][000000020.034] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:54.058][000000020.036] I/user.BLE 收到写请求: F000 F002 578326A8397A8359CFCDB2A5DF2D0F4B55688C2923877D02A65E6A9BEF12A5670C81644C22535802647B3D9DFA35855E2FBF332AA3D80A94E8999A561C368B017A27D3AE1A0CA7C12880C29DADDA628F33041F6E683EAFB72C42A510AB7F5F8DBE623F348D0BD82A12E5A73AE737E3F3AD3B200F768C91297D22E99F748C7E9DD6DE3A692DED4F8B2A0D70AF92ECD2AFD865E60404CBF122A59A5590EF494E4E9F4A9C703C976B1A63F6ECB8CB31146389BFFD8321D48D07B0DF4C3BE1CA40C1B6D6E7F3956BFC27
+[2025-12-09 14:58:54.069][000000020.036] I/user.ble_packet_fota 处理写入数据 F000 F002 578326A8397A8359CFCDB2A5DF2D0F4B55688C2923877D02A65E6A9BEF12A5670C81644C22535802647B3D9DFA35855E2FBF332AA3D80A94E8999A561C368B017A27D3AE1A0CA7C12880C29DADDA628F33041F6E683EAFB72C42A510AB7F5F8DBE623F348D0BD82A12E5A73AE737E3F3AD3B200F768C91297D22E99F748C7E9DD6DE3A692DED4F8B2A0D70AF92ECD2AFD865E60404CBF122A59A5590EF494E4E9F4A9C703C976B1A63F6ECB8CB31146389BFFD8321D48D07B0DF4C3BE1CA40C1B6D6E7F3956BFC27 400
+[2025-12-09 14:58:54.081][000000020.037] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:54.090][000000020.037] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:54.103][000000020.037] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:54.116][000000020.038] I/user.FOTA_DATA 处理分段数据,包序号: 1
+[2025-12-09 14:58:54.131][000000020.038] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:54.140][000000020.038] I/user.FOTA_DATA 数据写入成功,当前总计: 400 字节
+[2025-12-09 14:58:54.154][000000020.165] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:54.167][000000020.166] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:54.184][000000020.166] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:54.196][000000020.166] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:54.207][000000020.166] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:54.222][000000020.168] I/user.BLE 收到写请求: F000 F002 BFAE6FA013B4867390CDA0870A6B7A3FB7F27C519343384418178F72CA8DD59C03C100DC9D5164426EAA229591718358C05796458854B37880CC94AE917BC8E803BB404BC29BB67C7D7079AACDEA8BE9F128332B4082C33022B5E9A381DC3C8E1DAFFDA7DADCEFFBB24E917E2300454A251E2BCDC42C37976E1E67CE95E1296DCC28E9647783D3BB69FD0D2FA8E21AB1A0C273801D4FA26ABE48B2DF7E2EBFACF919F76F7B16A7AEA63B5940D38DA7247DC55368F3FECA9C622E340C70A6E384959B0262F57BEB47
+[2025-12-09 14:58:54.237][000000020.168] I/user.ble_packet_fota 处理写入数据 F000 F002 BFAE6FA013B4867390CDA0870A6B7A3FB7F27C519343384418178F72CA8DD59C03C100DC9D5164426EAA229591718358C05796458854B37880CC94AE917BC8E803BB404BC29BB67C7D7079AACDEA8BE9F128332B4082C33022B5E9A381DC3C8E1DAFFDA7DADCEFFBB24E917E2300454A251E2BCDC42C37976E1E67CE95E1296DCC28E9647783D3BB69FD0D2FA8E21AB1A0C273801D4FA26ABE48B2DF7E2EBFACF919F76F7B16A7AEA63B5940D38DA7247DC55368F3FECA9C622E340C70A6E384959B0262F57BEB47 400
+[2025-12-09 14:58:54.251][000000020.169] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:54.262][000000020.169] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:54.274][000000020.169] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:54.287][000000020.170] I/user.FOTA_DATA 处理分段数据,包序号: 2
+[2025-12-09 14:58:54.300][000000020.170] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:54.311][000000020.170] I/user.FOTA_DATA 数据写入成功,当前总计: 600 字节
+[2025-12-09 14:58:54.320][000000020.338] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:54.333][000000020.339] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:54.341][000000020.339] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:54.354][000000020.339] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:54.364][000000020.340] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:54.371][000000020.341] I/user.BLE 收到写请求: F000 F002 9F91D745FD569A414C89ABA1FADC28AEDBFC7914B989922EC7C2DD96B671A2EA0469B6E0D0C551667CBFB86181535A9B22FFC681501D9782AA99F4ACF3E2226B517EF94FCF225CA046FC4EBA70E295A124859F973CBD2BC54006E0A1C25C0CBF1429405D9A55BF8256947328E167EE7FD1189695C70F6570317586F61573509FE11BE46BAF25E84C9E8C635556B4E36D5E8577A0613B21E506CA71CCD0972C7339FD71B5038C1CDB183D2C7072123C0539CBBD7658B09A6678D79CBEBCC00B3349911BF90AC528E1
+[2025-12-09 14:58:54.384][000000020.341] I/user.ble_packet_fota 处理写入数据 F000 F002 9F91D745FD569A414C89ABA1FADC28AEDBFC7914B989922EC7C2DD96B671A2EA0469B6E0D0C551667CBFB86181535A9B22FFC681501D9782AA99F4ACF3E2226B517EF94FCF225CA046FC4EBA70E295A124859F973CBD2BC54006E0A1C25C0CBF1429405D9A55BF8256947328E167EE7FD1189695C70F6570317586F61573509FE11BE46BAF25E84C9E8C635556B4E36D5E8577A0613B21E506CA71CCD0972C7339FD71B5038C1CDB183D2C7072123C0539CBBD7658B09A6678D79CBEBCC00B3349911BF90AC528E1 400
+[2025-12-09 14:58:54.394][000000020.342] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:54.403][000000020.343] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:54.412][000000020.343] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:54.422][000000020.343] I/user.FOTA_DATA 处理分段数据,包序号: 3
+[2025-12-09 14:58:54.432][000000020.343] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:54.447][000000020.344] I/user.FOTA_DATA 数据写入成功,当前总计: 800 字节
+[2025-12-09 14:58:54.484][000000020.516] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:54.495][000000020.517] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:54.505][000000020.517] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:54.515][000000020.517] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:54.524][000000020.518] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:54.540][000000020.519] I/user.BLE 收到写请求: F000 F002 E223812664DD4D928A3E8EDA95352501825004DA6A79B02F259CCCCD81E7EB6999E56EF07D72F0C89F93DE646E91FDD4F508ADEA6AAFC39653592D1135F981883C2B14DFCE97DFA8C63D8C99A78C46EFA6B7A2F07D3D372A025C2E4F7355FADCB664ED0C63ABF28DA2C73A5DB4693A296E2E0A0AEF8B0091EF1EB5B13B2EF9D662A5FC8B4DB84A4BB0229A1E82F9376A79D1DBD0EF770708EB8A4A6637EDB112DF402AA83089A33DC90E9911FA0C81AF39450ECE80C4B7BA580661286900635BE4A1F81583331816
+[2025-12-09 14:58:54.550][000000020.519] I/user.ble_packet_fota 处理写入数据 F000 F002 E223812664DD4D928A3E8EDA95352501825004DA6A79B02F259CCCCD81E7EB6999E56EF07D72F0C89F93DE646E91FDD4F508ADEA6AAFC39653592D1135F981883C2B14DFCE97DFA8C63D8C99A78C46EFA6B7A2F07D3D372A025C2E4F7355FADCB664ED0C63ABF28DA2C73A5DB4693A296E2E0A0AEF8B0091EF1EB5B13B2EF9D662A5FC8B4DB84A4BB0229A1E82F9376A79D1DBD0EF770708EB8A4A6637EDB112DF402AA83089A33DC90E9911FA0C81AF39450ECE80C4B7BA580661286900635BE4A1F81583331816 400
+[2025-12-09 14:58:54.561][000000020.520] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:54.571][000000020.520] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:54.580][000000020.521] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:54.591][000000020.521] I/user.FOTA_DATA 处理分段数据,包序号: 4
+[2025-12-09 14:58:54.601][000000020.521] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:54.615][000000020.522] I/user.FOTA_DATA 数据写入成功,当前总计: 1000 字节
+[2025-12-09 14:58:54.640][000000020.688] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:54.653][000000020.689] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:54.661][000000020.689] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:54.669][000000020.689] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:54.682][000000020.689] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:54.695][000000020.691] I/user.BLE 收到写请求: F000 F002 E873D77EDC3E717CAA4CB2A7085406D8B4F680F9CF38F4193308A241CFE7F59AD54E7A9A5EB5D818D0C4D193C20318EB3AA118A3F485FB83183D1711B43B59672410A3838F7564CA912FAF597744A170AE45380D29773CDC72330AF7792D3A58E9395F3226B70462F739677B77347338CB374A4411CC3AD721931228728A3082E06B9742903999708C366C3EA2E35CCDF1E59F3B4BB94139B229A775E54D470FA10BC45DB934F73D36DFBE1E076247EDDA39A17A7F281C6B405EE564D144879021AE75435DA61B0B
+[2025-12-09 14:58:54.712][000000020.691] I/user.ble_packet_fota 处理写入数据 F000 F002 E873D77EDC3E717CAA4CB2A7085406D8B4F680F9CF38F4193308A241CFE7F59AD54E7A9A5EB5D818D0C4D193C20318EB3AA118A3F485FB83183D1711B43B59672410A3838F7564CA912FAF597744A170AE45380D29773CDC72330AF7792D3A58E9395F3226B70462F739677B77347338CB374A4411CC3AD721931228728A3082E06B9742903999708C366C3EA2E35CCDF1E59F3B4BB94139B229A775E54D470FA10BC45DB934F73D36DFBE1E076247EDDA39A17A7F281C6B405EE564D144879021AE75435DA61B0B 400
+[2025-12-09 14:58:54.724][000000020.692] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:54.737][000000020.692] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:54.754][000000020.692] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:54.771][000000020.693] I/user.FOTA_DATA 处理分段数据,包序号: 5
+[2025-12-09 14:58:54.785][000000020.693] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:54.798][000000020.693] I/user.FOTA_DATA 数据写入成功,当前总计: 1200 字节
+[2025-12-09 14:58:54.826][000000020.866] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:54.835][000000020.866] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:54.845][000000020.867] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:54.857][000000020.867] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:54.867][000000020.867] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:54.880][000000020.869] I/user.BLE 收到写请求: F000 F002 410FFED6705B09EA71340D2EE49489909141E21CF9FCC15B26AE57779C8ADA469F3544578C2EB7C2AEB55B759AB3539322B1A3CA5670FAB659E5DEF3AC982485982C93F540C526F944C421DFC15E0246A5428FA23FD4EA607077CE1355FC00ED2522B0AB448A43A136D8FEDA5C756EF4D37D707E6CBC01046AA33B12B57AB1058AFF6ED30B6B86F664834CB287D62C1651EAB588EBEB903596CE12DD9E36E282E0F4B3BB2C17AA0B0E220B3088FF1542C89E18F4D621E438884D1A48F425888FC986F6589FF41553
+[2025-12-09 14:58:54.890][000000020.869] I/user.ble_packet_fota 处理写入数据 F000 F002 410FFED6705B09EA71340D2EE49489909141E21CF9FCC15B26AE57779C8ADA469F3544578C2EB7C2AEB55B759AB3539322B1A3CA5670FAB659E5DEF3AC982485982C93F540C526F944C421DFC15E0246A5428FA23FD4EA607077CE1355FC00ED2522B0AB448A43A136D8FEDA5C756EF4D37D707E6CBC01046AA33B12B57AB1058AFF6ED30B6B86F664834CB287D62C1651EAB588EBEB903596CE12DD9E36E282E0F4B3BB2C17AA0B0E220B3088FF1542C89E18F4D621E438884D1A48F425888FC986F6589FF41553 400
+[2025-12-09 14:58:54.900][000000020.870] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:54.909][000000020.870] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:54.923][000000020.870] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:54.934][000000020.871] I/user.FOTA_DATA 处理分段数据,包序号: 6
+[2025-12-09 14:58:54.945][000000020.871] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:54.956][000000020.871] I/user.FOTA_DATA 数据写入成功,当前总计: 1400 字节
+[2025-12-09 14:58:54.998][000000021.041] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:55.010][000000021.042] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:55.027][000000021.042] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:55.037][000000021.042] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:55.055][000000021.043] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:55.067][000000021.044] I/user.BLE 收到写请求: F000 F002 AF8909FB5E3F2931D6AEA8AC8570EBDAE58D62286B0CB3D6EF5536EEA795DC4D7F116520A50DE34925E8253CD9476B67E8B9C7D3A7ED4EF074FA573E087A3F1DD0A7EE0241854D002016D33F56A54F56ED6CBB4207061FD64AF40AECCE0B33C96FDF83303E00A4413B1305516FF04C7052FA4B354EDFC9C8151DBCE50A56171CB112F11864F50045F8C70F0788ECF30AC28C67347A518CB032A2E9D7C10A8FCEA12190E46E3D11F896F18B8EB38CEE94FE8A32C44A14F827EA6E352BEA31D786943436AE5396CFE2
+[2025-12-09 14:58:55.083][000000021.045] I/user.ble_packet_fota 处理写入数据 F000 F002 AF8909FB5E3F2931D6AEA8AC8570EBDAE58D62286B0CB3D6EF5536EEA795DC4D7F116520A50DE34925E8253CD9476B67E8B9C7D3A7ED4EF074FA573E087A3F1DD0A7EE0241854D002016D33F56A54F56ED6CBB4207061FD64AF40AECCE0B33C96FDF83303E00A4413B1305516FF04C7052FA4B354EDFC9C8151DBCE50A56171CB112F11864F50045F8C70F0788ECF30AC28C67347A518CB032A2E9D7C10A8FCEA12190E46E3D11F896F18B8EB38CEE94FE8A32C44A14F827EA6E352BEA31D786943436AE5396CFE2 400
+[2025-12-09 14:58:55.101][000000021.045] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:55.112][000000021.045] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:55.124][000000021.046] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:55.137][000000021.046] I/user.FOTA_DATA 处理分段数据,包序号: 7
+[2025-12-09 14:58:55.146][000000021.046] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:55.156][000000021.047] I/user.FOTA_DATA 数据写入成功,当前总计: 1600 字节
+[2025-12-09 14:58:55.168][000000021.213] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:55.178][000000021.214] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:55.190][000000021.214] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:55.199][000000021.214] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:55.208][000000021.214] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:55.219][000000021.216] I/user.BLE 收到写请求: F000 F002 EDB607310E11E77F72F5F13F50651C339B3A0AE817EE467BA4E6ED338114AF1854D051FB48D13A814239B33805F0EADB89BF82F2D207B9AEB59DB2BE8BD9F4F0EE3FFB28B2E8B28534844A1C4274AE4728329F676E25414207226BCEC0F197FBCBC5C139C7086682A4511E542923B4EB79DCB32DE59784DF5DF8F13FD2F84E550CA85703945F317B1012F4364BC3128819E37F22DB62B91F2CF6297CD569A28A638E77B0063DBEBDE6740677B1D118FFA4859F8D50087E0BBFAC077E262AC204A5F2C80D74BD8FEE
+[2025-12-09 14:58:55.230][000000021.216] I/user.ble_packet_fota 处理写入数据 F000 F002 EDB607310E11E77F72F5F13F50651C339B3A0AE817EE467BA4E6ED338114AF1854D051FB48D13A814239B33805F0EADB89BF82F2D207B9AEB59DB2BE8BD9F4F0EE3FFB28B2E8B28534844A1C4274AE4728329F676E25414207226BCEC0F197FBCBC5C139C7086682A4511E542923B4EB79DCB32DE59784DF5DF8F13FD2F84E550CA85703945F317B1012F4364BC3128819E37F22DB62B91F2CF6297CD569A28A638E77B0063DBEBDE6740677B1D118FFA4859F8D50087E0BBFAC077E262AC204A5F2C80D74BD8FEE 400
+[2025-12-09 14:58:55.241][000000021.217] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:55.250][000000021.217] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:55.261][000000021.217] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:55.272][000000021.218] I/user.FOTA_DATA 处理分段数据,包序号: 8
+[2025-12-09 14:58:55.283][000000021.218] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:55.296][000000021.218] I/user.FOTA_DATA 数据写入成功,当前总计: 1800 字节
+[2025-12-09 14:58:55.326][000000021.360] I/user.fota version 001.000.000
+[2025-12-09 14:58:55.357][000000021.388] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:55.367][000000021.388] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:55.377][000000021.389] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:55.387][000000021.389] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:55.398][000000021.389] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:55.407][000000021.391] I/user.BLE 收到写请求: F000 F002 94E91700D87B64F42F5066347F9AA5F5D03C35E7FD6AC34265D5183B77DFB8B9ABC25C778AEB347F9D6BA4D99BA6CAA424383D4CF341160B4B0D4D80CB74B495B481381D21D6774E367A43D2121CF399774973F46FDED2FF379DDB14D6A2E2E4DA79783AC902C2039E1DDB8F14640CEA0A23DCD23FF5ACAB0129F935CC43822A2370E150BE3232E079449AC2438E541DD0A2B95742507017B7645BEAE7804D12989706640D4DFDEF4679ADA908B88AC01D00DB28837D18B9A3391181661ADAAA79F2B6DFE121C14A
+[2025-12-09 14:58:55.417][000000021.391] I/user.ble_packet_fota 处理写入数据 F000 F002 94E91700D87B64F42F5066347F9AA5F5D03C35E7FD6AC34265D5183B77DFB8B9ABC25C778AEB347F9D6BA4D99BA6CAA424383D4CF341160B4B0D4D80CB74B495B481381D21D6774E367A43D2121CF399774973F46FDED2FF379DDB14D6A2E2E4DA79783AC902C2039E1DDB8F14640CEA0A23DCD23FF5ACAB0129F935CC43822A2370E150BE3232E079449AC2438E541DD0A2B95742507017B7645BEAE7804D12989706640D4DFDEF4679ADA908B88AC01D00DB28837D18B9A3391181661ADAAA79F2B6DFE121C14A 400
+[2025-12-09 14:58:55.430][000000021.392] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:55.440][000000021.392] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:55.455][000000021.392] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:55.464][000000021.393] I/user.FOTA_DATA 处理分段数据,包序号: 9
+[2025-12-09 14:58:55.475][000000021.393] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:55.489][000000021.393] I/user.FOTA_DATA 数据写入成功,当前总计: 2000 字节
+[2025-12-09 14:58:55.559][000000021.606] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:55.569][000000021.607] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:55.579][000000021.607] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:55.589][000000021.607] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:55.602][000000021.608] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:55.611][000000021.609] I/user.BLE 收到写请求: F000 F002 3834A5479DF6DDDC5EBADFFF3C8BB9808F67A25A9916D0E5CF17A49FDD72A49C13D258E1E955462C882A05FDC65C7B25E32C6EC8DB5104C3C44D9F50F588DD4A574D93A7C1A55AE21100CA17AB225AF060DA2E9FC234D544D6C499B3E2C967E8AA8F88086C24A7C7688D9C3651200721DE5309CA1E9AEC2EBAD606B5B5BCCA5659DC6EF24B9A886C29F27870882873C76AF5D9728CDFD82307382516962F202EDC31B1CFE82CD3AA5239567AE9B1E44B00AB9CB0112000C6977B621AFEE42BDD42A023014474D2E8
+[2025-12-09 14:58:55.632][000000021.609] I/user.ble_packet_fota 处理写入数据 F000 F002 3834A5479DF6DDDC5EBADFFF3C8BB9808F67A25A9916D0E5CF17A49FDD72A49C13D258E1E955462C882A05FDC65C7B25E32C6EC8DB5104C3C44D9F50F588DD4A574D93A7C1A55AE21100CA17AB225AF060DA2E9FC234D544D6C499B3E2C967E8AA8F88086C24A7C7688D9C3651200721DE5309CA1E9AEC2EBAD606B5B5BCCA5659DC6EF24B9A886C29F27870882873C76AF5D9728CDFD82307382516962F202EDC31B1CFE82CD3AA5239567AE9B1E44B00AB9CB0112000C6977B621AFEE42BDD42A023014474D2E8 400
+[2025-12-09 14:58:55.641][000000021.610] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:55.652][000000021.610] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:55.666][000000021.611] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:55.677][000000021.611] I/user.FOTA_DATA 处理分段数据,包序号: 10
+[2025-12-09 14:58:55.689][000000021.611] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:55.698][000000021.612] I/user.FOTA_DATA 数据写入成功,当前总计: 2200 字节
+[2025-12-09 14:58:55.779][000000021.825] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:55.794][000000021.826] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:55.804][000000021.826] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:55.819][000000021.826] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:55.827][000000021.827] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:55.836][000000021.828] I/user.BLE 收到写请求: F000 F002 63663F8E2E57002B58CD4812D54992E3DC35630D911747925F0525FE2146969453955C12F6ECF3F086401F7B400842562866F63775646ECEC3EB75A592697C3353B30BDA7D5E7417259AC51EB15D4C9630DA0DAD132232C969B0A5738E613556CBF6C194E11EFE106E0AC67ECF088CE3F0F0005E6A4E88CD7DE728D8F7677025033E575C7C26128CD97E3CF9861BDADE2DFA95C1DAF7AF765E6F5436A58AB9EF5FC46D24D6010D204A63DEAABD64E86FFF7C266259E7281177FC7DE905D83AD76EB54D4E4F42DBE9
+[2025-12-09 14:58:55.852][000000021.828] I/user.ble_packet_fota 处理写入数据 F000 F002 63663F8E2E57002B58CD4812D54992E3DC35630D911747925F0525FE2146969453955C12F6ECF3F086401F7B400842562866F63775646ECEC3EB75A592697C3353B30BDA7D5E7417259AC51EB15D4C9630DA0DAD132232C969B0A5738E613556CBF6C194E11EFE106E0AC67ECF088CE3F0F0005E6A4E88CD7DE728D8F7677025033E575C7C26128CD97E3CF9861BDADE2DFA95C1DAF7AF765E6F5436A58AB9EF5FC46D24D6010D204A63DEAABD64E86FFF7C266259E7281177FC7DE905D83AD76EB54D4E4F42DBE9 400
+[2025-12-09 14:58:55.861][000000021.829] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:55.869][000000021.829] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:55.878][000000021.830] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:55.894][000000021.830] I/user.FOTA_DATA 处理分段数据,包序号: 11
+[2025-12-09 14:58:55.903][000000021.830] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:55.915][000000021.831] I/user.FOTA_DATA 数据写入成功,当前总计: 2400 字节
+[2025-12-09 14:58:55.966][000000022.002] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:55.974][000000022.002] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:55.986][000000022.003] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:55.995][000000022.003] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:56.004][000000022.003] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:56.013][000000022.005] I/user.BLE 收到写请求: F000 F002 CDF7739542AF28D7E4A0C93AE2AFD9FB4F8F025ED1D352BFA14FF85085D795CBC8087C3912E010319D728434CCD90531248A0EA2C4DC65936324E7DABB15C5A7774DA6C761DDBF31CB3242B17CED11DC18BFA1E5663CACF3ECEA84D6E49798742CD98C9F61F7ED4513B69449DEEA1ADBD300C9B2E6DB5920133424E25ECD3809D7C59D4A3AB15BC57EC72554059E953C6172F46557E20FE1C3C25D9AE36380EBC5BE3FC29835D7AB7F5B1844B7DA5D37630C7F3474664ED04FBBD4FA95CC43FAFBE4E32CA76FE988
+[2025-12-09 14:58:56.028][000000022.005] I/user.ble_packet_fota 处理写入数据 F000 F002 CDF7739542AF28D7E4A0C93AE2AFD9FB4F8F025ED1D352BFA14FF85085D795CBC8087C3912E010319D728434CCD90531248A0EA2C4DC65936324E7DABB15C5A7774DA6C761DDBF31CB3242B17CED11DC18BFA1E5663CACF3ECEA84D6E49798742CD98C9F61F7ED4513B69449DEEA1ADBD300C9B2E6DB5920133424E25ECD3809D7C59D4A3AB15BC57EC72554059E953C6172F46557E20FE1C3C25D9AE36380EBC5BE3FC29835D7AB7F5B1844B7DA5D37630C7F3474664ED04FBBD4FA95CC43FAFBE4E32CA76FE988 400
+[2025-12-09 14:58:56.036][000000022.006] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:56.050][000000022.006] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:56.058][000000022.006] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:56.068][000000022.007] I/user.FOTA_DATA 处理分段数据,包序号: 12
+[2025-12-09 14:58:56.079][000000022.007] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:56.089][000000022.007] I/user.FOTA_DATA 数据写入成功,当前总计: 2600 字节
+[2025-12-09 14:58:56.138][000000022.175] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:56.148][000000022.175] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:56.164][000000022.176] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:56.173][000000022.176] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:56.185][000000022.176] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:56.198][000000022.177] I/user.BLE 收到写请求: F000 F002 2FA735BDB0849ECDF7274BAA573388358B7F78CD1F8DD5F64C1A955B430D75A6F465AB75E1E5E04BBF5046F16E0FD8223C68623D8A1195503AE296639EA6618AAEFD7E06DF0BF9C288B03A76FBCBB38C243164461E2B63CCEBDFC2A133C9FF098A354E309FBCFD93AFA80F007CD127295370EC97D2BA52559C7B7FCA3E5DDF5A2224ACDFE41D2E77446E23E344BD73BD84DD30E5C0C74E27D6981DE2E6D725A567266071A805DE5C6A2AA78E80A9278307C777DA72FC057CB1F75A636E0655AEF02F99FEFFDE701C
+[2025-12-09 14:58:56.210][000000022.178] I/user.ble_packet_fota 处理写入数据 F000 F002 2FA735BDB0849ECDF7274BAA573388358B7F78CD1F8DD5F64C1A955B430D75A6F465AB75E1E5E04BBF5046F16E0FD8223C68623D8A1195503AE296639EA6618AAEFD7E06DF0BF9C288B03A76FBCBB38C243164461E2B63CCEBDFC2A133C9FF098A354E309FBCFD93AFA80F007CD127295370EC97D2BA52559C7B7FCA3E5DDF5A2224ACDFE41D2E77446E23E344BD73BD84DD30E5C0C74E27D6981DE2E6D725A567266071A805DE5C6A2AA78E80A9278307C777DA72FC057CB1F75A636E0655AEF02F99FEFFDE701C 400
+[2025-12-09 14:58:56.224][000000022.179] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:56.238][000000022.179] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:56.253][000000022.179] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:56.265][000000022.180] I/user.FOTA_DATA 处理分段数据,包序号: 13
+[2025-12-09 14:58:56.276][000000022.180] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:56.287][000000022.180] I/user.FOTA_DATA 数据写入成功,当前总计: 2800 字节
+[2025-12-09 14:58:56.310][000000022.353] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:56.324][000000022.354] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:56.334][000000022.354] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:56.344][000000022.354] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:56.354][000000022.355] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:56.367][000000022.356] I/user.BLE 收到写请求: F000 F002 6DAB482BEA2153C5381AE0282FF0C0EE3064D10F71F1F1C578D00E75833EAF0F3877DA8F09749CD10ED7E3582E6115D9383D06ECD21AA7C60BDB754624F53D4790D8C3541D1C8F4C9CFA92AFF9292642225D02D02097EC6725A8DEEAB878E5C66E659FDFC7D0FF13E1C06F34BFA967A7C81EB0851F9FFAB4A77D0DE110F6B0D1E8B8193B95862EDF0A6F23D6443ED26B094ACA3757391B5726D00E0EAFBF63685AD9C9FE5595F7814223AF6BF4D50899B622BD7AC11C95816A2540E8A043F96CECC53401DA8FA560
+[2025-12-09 14:58:56.379][000000022.356] I/user.ble_packet_fota 处理写入数据 F000 F002 6DAB482BEA2153C5381AE0282FF0C0EE3064D10F71F1F1C578D00E75833EAF0F3877DA8F09749CD10ED7E3582E6115D9383D06ECD21AA7C60BDB754624F53D4790D8C3541D1C8F4C9CFA92AFF9292642225D02D02097EC6725A8DEEAB878E5C66E659FDFC7D0FF13E1C06F34BFA967A7C81EB0851F9FFAB4A77D0DE110F6B0D1E8B8193B95862EDF0A6F23D6443ED26B094ACA3757391B5726D00E0EAFBF63685AD9C9FE5595F7814223AF6BF4D50899B622BD7AC11C95816A2540E8A043F96CECC53401DA8FA560 400
+[2025-12-09 14:58:56.392][000000022.357] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:56.401][000000022.357] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:56.410][000000022.358] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:56.423][000000022.358] I/user.FOTA_DATA 处理分段数据,包序号: 14
+[2025-12-09 14:58:56.432][000000022.358] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:56.443][000000022.359] I/user.FOTA_DATA 数据写入成功,当前总计: 3000 字节
+[2025-12-09 14:58:56.527][000000022.570] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:56.535][000000022.570] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:56.544][000000022.571] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:56.555][000000022.571] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:56.566][000000022.571] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:56.573][000000022.572] I/user.BLE 收到写请求: F000 F002 5D925AA7B4D0D97993AF1C53DA1D092D141720A0AB7566B279B9A29604B76DF2E578D023EAE481D1784E925597DF2B82E234CF0FF4B3ECF2008316507EC2CEBD3D2D6967EE8984E000A1B3E44CABD30F0F66AF74B79107365098498346D57A6BC34832118FD2D30153BB266CA6C43B07E234A7BC228DC4A3DDF35DE4A4CAD682BCB32783933BFAB42500EAFE06E36090D1D618201A0793478F8A331EDAEA26879FD47164296C362C8562290B9A1358003F86440211516DE60781D924175FB7038B101EDA72E2FF7E
+[2025-12-09 14:58:56.587][000000022.573] I/user.ble_packet_fota 处理写入数据 F000 F002 5D925AA7B4D0D97993AF1C53DA1D092D141720A0AB7566B279B9A29604B76DF2E578D023EAE481D1784E925597DF2B82E234CF0FF4B3ECF2008316507EC2CEBD3D2D6967EE8984E000A1B3E44CABD30F0F66AF74B79107365098498346D57A6BC34832118FD2D30153BB266CA6C43B07E234A7BC228DC4A3DDF35DE4A4CAD682BCB32783933BFAB42500EAFE06E36090D1D618201A0793478F8A331EDAEA26879FD47164296C362C8562290B9A1358003F86440211516DE60781D924175FB7038B101EDA72E2FF7E 400
+[2025-12-09 14:58:56.598][000000022.574] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:56.608][000000022.574] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:56.617][000000022.574] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:56.627][000000022.575] I/user.FOTA_DATA 处理分段数据,包序号: 15
+[2025-12-09 14:58:56.638][000000022.575] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:56.648][000000022.575] I/user.FOTA_DATA 数据写入成功,当前总计: 3200 字节
+[2025-12-09 14:58:56.715][000000022.748] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:56.728][000000022.748] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:56.736][000000022.749] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:56.745][000000022.749] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:56.756][000000022.749] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:56.768][000000022.750] I/user.BLE 收到写请求: F000 F002 C6ED9352E29483FE74144CFA5D8D849E2A9AFCDE915098F3F7A5BCC8555431EDC4924AB94FD102FBF554BFA79E070C1F5545AA2E923D06EAFA23FE90E9AB180331E5B7377B4C8B5B2C9AA19F7A6B20CE9B4A1F44DDDE5C1D2E192DE9D4657511DC7562465F2744CD117B2982FBC4FD85E62F1E06C142EF00CBE4236E9BD25A6B6F69E77A2C9C55C1156E82175C32F2FDC1D84670D86B8E7F13652BC15725A3EDA5C3C2C29A7A1DBDCE2A9DCE5658536BCD015DD658CBAFF5516D2AA48A505190A393FC1BE5EC8C9B
+[2025-12-09 14:58:56.779][000000022.752] I/user.ble_packet_fota 处理写入数据 F000 F002 C6ED9352E29483FE74144CFA5D8D849E2A9AFCDE915098F3F7A5BCC8555431EDC4924AB94FD102FBF554BFA79E070C1F5545AA2E923D06EAFA23FE90E9AB180331E5B7377B4C8B5B2C9AA19F7A6B20CE9B4A1F44DDDE5C1D2E192DE9D4657511DC7562465F2744CD117B2982FBC4FD85E62F1E06C142EF00CBE4236E9BD25A6B6F69E77A2C9C55C1156E82175C32F2FDC1D84670D86B8E7F13652BC15725A3EDA5C3C2C29A7A1DBDCE2A9DCE5658536BCD015DD658CBAFF5516D2AA48A505190A393FC1BE5EC8C9B 400
+[2025-12-09 14:58:56.790][000000022.752] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:56.800][000000022.753] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:56.807][000000022.753] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:56.820][000000022.753] I/user.FOTA_DATA 处理分段数据,包序号: 16
+[2025-12-09 14:58:56.830][000000022.754] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:56.840][000000022.754] I/user.FOTA_DATA 数据写入成功,当前总计: 3400 字节
+[2025-12-09 14:58:56.887][000000022.920] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:56.897][000000022.921] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:56.907][000000022.922] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:56.915][000000022.922] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:56.924][000000022.922] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:56.936][000000022.924] I/user.BLE 收到写请求: F000 F002 A70F29FAA6721203B03486B6A99CAAC0FFFD66C89C5D4C9ED13CA10C36792C3DA8018B478CBEBFB7157D420D4A6AEF76ABFC111C64077F374AF3111AE30997393F04720EAFEC3B7578DC816FFE893C450DFD1440973A4CEFC1D78D9C682EB5F5386EE60498C89DDD43C3655CC63794CEC9B40CECC537E859C5B0DE47DBE84F9CB48FA7BBF039BEA4A8BF661ADEA46FFFBC5C43AC8FC1D7AAB02C0F7E81C5413F803B2CC3F21F1CDD0C4DB9055F31C22B94290C3D5FDB960DDC324EEE9F63AD9D0503EF65EE0C555B
+[2025-12-09 14:58:56.947][000000022.924] I/user.ble_packet_fota 处理写入数据 F000 F002 A70F29FAA6721203B03486B6A99CAAC0FFFD66C89C5D4C9ED13CA10C36792C3DA8018B478CBEBFB7157D420D4A6AEF76ABFC111C64077F374AF3111AE30997393F04720EAFEC3B7578DC816FFE893C450DFD1440973A4CEFC1D78D9C682EB5F5386EE60498C89DDD43C3655CC63794CEC9B40CECC537E859C5B0DE47DBE84F9CB48FA7BBF039BEA4A8BF661ADEA46FFFBC5C43AC8FC1D7AAB02C0F7E81C5413F803B2CC3F21F1CDD0C4DB9055F31C22B94290C3D5FDB960DDC324EEE9F63AD9D0503EF65EE0C555B 400
+[2025-12-09 14:58:56.956][000000022.925] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:56.963][000000022.925] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:56.977][000000022.925] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:56.989][000000022.926] I/user.FOTA_DATA 处理分段数据,包序号: 17
+[2025-12-09 14:58:56.998][000000022.926] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:57.007][000000022.927] I/user.FOTA_DATA 数据写入成功,当前总计: 3600 字节
+[2025-12-09 14:58:57.059][000000023.094] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:57.072][000000023.094] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:57.084][000000023.095] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:57.092][000000023.095] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:57.103][000000023.095] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:57.117][000000023.097] I/user.BLE 收到写请求: F000 F002 BA6E163C2CD19D7647F897C4C0AC4A444B786E9515E397BCDB2B9BC041396371F39B5A8424D38A207EA5F50D0459914BA061BB99D2878C2766E9202A3561E3F452EFBF851081AA69A47B4BDB1BA016DAD805870C608895DE829582F0819AD58E57A958239CD5D6CAFB9CF9A394147BC601E44E4552C81B2360B5BC1B0D072E8C78076E91C699C048284D1A42B278FDF25FDDE90747B24A439601CA191D4FF8D836048BBB636E77B3C01B882AF7835E1430B95DBBDE0ECE12A8F1CC3EB593F68BC5D2D4C8D62A29A0
+[2025-12-09 14:58:57.129][000000023.097] I/user.ble_packet_fota 处理写入数据 F000 F002 BA6E163C2CD19D7647F897C4C0AC4A444B786E9515E397BCDB2B9BC041396371F39B5A8424D38A207EA5F50D0459914BA061BB99D2878C2766E9202A3561E3F452EFBF851081AA69A47B4BDB1BA016DAD805870C608895DE829582F0819AD58E57A958239CD5D6CAFB9CF9A394147BC601E44E4552C81B2360B5BC1B0D072E8C78076E91C699C048284D1A42B278FDF25FDDE90747B24A439601CA191D4FF8D836048BBB636E77B3C01B882AF7835E1430B95DBBDE0ECE12A8F1CC3EB593F68BC5D2D4C8D62A29A0 400
+[2025-12-09 14:58:57.140][000000023.098] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:57.149][000000023.098] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:57.160][000000023.099] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:57.171][000000023.099] I/user.FOTA_DATA 处理分段数据,包序号: 18
+[2025-12-09 14:58:57.180][000000023.099] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:57.189][000000023.100] I/user.FOTA_DATA 数据写入成功,当前总计: 3800 字节
+[2025-12-09 14:58:57.229][000000023.271] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:57.242][000000023.272] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:57.251][000000023.272] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:57.262][000000023.272] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:57.274][000000023.272] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:57.283][000000023.274] I/user.BLE 收到写请求: F000 F002 0F4478DEB2E9F8BD5AC553678D96E05AB73C3323247D56BEE527EA8FCC6F3796D065F6696779BAB46F7A33A98EF7156E8E620516936A8EC48701DD50660E84879BD8F89DB833C7F5797D085ADAAEEA64F93EE711E56DE028A5DC6746E8BAD577630F8268F7294159A50E81C7AAAE036347BD4B02ACD4DD9850EF2B59EB91A75CC6F1C6FCAA39A14D3749C62E6BD0C24C5BE7B30BF20DBA13F34DD4C48E382F7029F18B0F17C5D39CA50902A5B55B733D3860DC9317515AF65A444DAD1404B924FAB5A0143AB597D5
+[2025-12-09 14:58:57.297][000000023.274] I/user.ble_packet_fota 处理写入数据 F000 F002 0F4478DEB2E9F8BD5AC553678D96E05AB73C3323247D56BEE527EA8FCC6F3796D065F6696779BAB46F7A33A98EF7156E8E620516936A8EC48701DD50660E84879BD8F89DB833C7F5797D085ADAAEEA64F93EE711E56DE028A5DC6746E8BAD577630F8268F7294159A50E81C7AAAE036347BD4B02ACD4DD9850EF2B59EB91A75CC6F1C6FCAA39A14D3749C62E6BD0C24C5BE7B30BF20DBA13F34DD4C48E382F7029F18B0F17C5D39CA50902A5B55B733D3860DC9317515AF65A444DAD1404B924FAB5A0143AB597D5 400
+[2025-12-09 14:58:57.311][000000023.275] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:57.320][000000023.275] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:57.330][000000023.275] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:57.342][000000023.276] I/user.FOTA_DATA 处理分段数据,包序号: 19
+[2025-12-09 14:58:57.350][000000023.276] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:57.362][000000023.277] I/user.FOTA_DATA 数据写入成功,当前总计: 4000 字节
+[2025-12-09 14:58:57.448][000000023.488] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:57.469][000000023.488] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:57.479][000000023.489] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:57.489][000000023.489] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:57.503][000000023.489] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:57.518][000000023.491] I/user.BLE 收到写请求: F000 F002 A3D259FCC5EB3774A2586E5D400FEADA077435A68F6E29B78604E1E26DAFAADBFFB5477713DB9F1531AEF4E587BC82CDCA6098B2DFFAB6CF7559B3E3D6911C5D50DE4B340E630A38C44E652DD5FC48CB7DE00CC83C07E2050BBD56F046579EB4608DDF9B632B9AC1F8EF1054021F4706112B9C143AB85ECE86B631D656826C7EB53468CF860322915FE9009A7492AFCE2E8F5E4DE18FE3D32E7ED56B4DA55D8B0C94AA319A7F48569D17E9BCBCD579E31BBE9958D56B13546ACA87B156569A9AFD200955E6ABB487
+[2025-12-09 14:58:57.530][000000023.491] I/user.ble_packet_fota 处理写入数据 F000 F002 A3D259FCC5EB3774A2586E5D400FEADA077435A68F6E29B78604E1E26DAFAADBFFB5477713DB9F1531AEF4E587BC82CDCA6098B2DFFAB6CF7559B3E3D6911C5D50DE4B340E630A38C44E652DD5FC48CB7DE00CC83C07E2050BBD56F046579EB4608DDF9B632B9AC1F8EF1054021F4706112B9C143AB85ECE86B631D656826C7EB53468CF860322915FE9009A7492AFCE2E8F5E4DE18FE3D32E7ED56B4DA55D8B0C94AA319A7F48569D17E9BCBCD579E31BBE9958D56B13546ACA87B156569A9AFD200955E6ABB487 400
+[2025-12-09 14:58:57.543][000000023.492] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:57.552][000000023.492] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:57.563][000000023.492] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:57.574][000000023.493] I/user.FOTA_DATA 处理分段数据,包序号: 20
+[2025-12-09 14:58:57.584][000000023.523] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:57.593][000000023.524] I/user.FOTA_DATA 数据写入成功,当前总计: 4200 字节
+[2025-12-09 14:58:57.619][000000023.666] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:57.631][000000023.667] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:57.643][000000023.667] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:57.657][000000023.667] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:57.669][000000023.668] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:57.678][000000023.669] I/user.BLE 收到写请求: F000 F002 6BE5E55BDC5204A19BFE2E7C205B5BE81F683ADCD8ACD5A202DBF521EC0C1EBBDCA27409534EFD7CCE4AEEF3CA58C8543C6AF43C86F2F6485A9AB4B544D9C19D83A989A05D8994F154001A5F542439910754539430A2688B69873D4168FB5EB756D6900770B03051B92799ADE2BC52655B1BAF9A14D03274EBC4C330C19DA0E6FF34ECA92F43CA5D50CE7B226D345520EA49F859681CC1F46F800696D2015E72EFEE01403414DF534740FF5B80F63959173834C43D7B037629A98051F8A0EAB7D101F0707797EA9A
+[2025-12-09 14:58:57.689][000000023.669] I/user.ble_packet_fota 处理写入数据 F000 F002 6BE5E55BDC5204A19BFE2E7C205B5BE81F683ADCD8ACD5A202DBF521EC0C1EBBDCA27409534EFD7CCE4AEEF3CA58C8543C6AF43C86F2F6485A9AB4B544D9C19D83A989A05D8994F154001A5F542439910754539430A2688B69873D4168FB5EB756D6900770B03051B92799ADE2BC52655B1BAF9A14D03274EBC4C330C19DA0E6FF34ECA92F43CA5D50CE7B226D345520EA49F859681CC1F46F800696D2015E72EFEE01403414DF534740FF5B80F63959173834C43D7B037629A98051F8A0EAB7D101F0707797EA9A 400
+[2025-12-09 14:58:57.702][000000023.670] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:57.711][000000023.670] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:57.721][000000023.671] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:57.734][000000023.671] I/user.FOTA_DATA 处理分段数据,包序号: 21
+[2025-12-09 14:58:57.742][000000023.671] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:57.750][000000023.672] I/user.FOTA_DATA 数据写入成功,当前总计: 4400 字节
+[2025-12-09 14:58:57.837][000000023.883] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:57.846][000000023.883] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:57.859][000000023.884] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:57.871][000000023.884] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:57.880][000000023.884] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:57.889][000000023.886] I/user.BLE 收到写请求: F000 F002 D7DA355A9083EC81CC3C089D1C9BA096727287A2F7F602CE5522E7590298B738600571BBEEE96A32941EF147EA8A77097838974851483E37C861B14DB7CE8761152A56E49C515D11923B97AC78E02F1ADFA6DFAC1CDF2405D3B7E6DE41C2C97725F1015EF748DA755A10905024A4D721535B3522838050DBA769090E5D9008B8C31F312C20A7A1F3EFED4987AF28BEE4C03E8FCBC8625CF17DE0B4BDB28A71F2AFD9C1F6E624A5890F1D6CB75526E2E512A695F3D99DC6D4452A4FBE7251F10AAC794C5374C5BE99
+[2025-12-09 14:58:57.898][000000023.886] I/user.ble_packet_fota 处理写入数据 F000 F002 D7DA355A9083EC81CC3C089D1C9BA096727287A2F7F602CE5522E7590298B738600571BBEEE96A32941EF147EA8A77097838974851483E37C861B14DB7CE8761152A56E49C515D11923B97AC78E02F1ADFA6DFAC1CDF2405D3B7E6DE41C2C97725F1015EF748DA755A10905024A4D721535B3522838050DBA769090E5D9008B8C31F312C20A7A1F3EFED4987AF28BEE4C03E8FCBC8625CF17DE0B4BDB28A71F2AFD9C1F6E624A5890F1D6CB75526E2E512A695F3D99DC6D4452A4FBE7251F10AAC794C5374C5BE99 400
+[2025-12-09 14:58:57.912][000000023.887] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:57.923][000000023.887] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:57.937][000000023.887] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:57.950][000000023.888] I/user.FOTA_DATA 处理分段数据,包序号: 22
+[2025-12-09 14:58:57.959][000000023.888] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:57.970][000000023.888] I/user.FOTA_DATA 数据写入成功,当前总计: 4600 字节
+[2025-12-09 14:58:58.007][000000024.055] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:58.019][000000024.055] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:58.030][000000024.056] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:58.042][000000024.056] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:58.051][000000024.056] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:58.062][000000024.058] I/user.BLE 收到写请求: F000 F002 F5392A156093A3FC708F821DAA16652202A12307D226875F97319CD5A92B65A6314E5519BD42E5DB61F14C8DA38A84ADDE240D50AF43B04DD28A11BE7380BD17EF28101AF040CD8D18236076516280E6D1C4715BB4331D3B0678F82E3DA831F471517209FD6092D7C193A92F1FAD1596E77AE0FE3EA6551BF45C3B9CB79991DFB7135CD6B9FFA8F81E6BABE6F979350DF254C76EB3700B4FC6FCD7C64A48301B9651BE65B6D8344AB5FF077966352CD781598D2A22E1670BE58914F441126E5EE00E86EB80473F02
+[2025-12-09 14:58:58.076][000000024.058] I/user.ble_packet_fota 处理写入数据 F000 F002 F5392A156093A3FC708F821DAA16652202A12307D226875F97319CD5A92B65A6314E5519BD42E5DB61F14C8DA38A84ADDE240D50AF43B04DD28A11BE7380BD17EF28101AF040CD8D18236076516280E6D1C4715BB4331D3B0678F82E3DA831F471517209FD6092D7C193A92F1FAD1596E77AE0FE3EA6551BF45C3B9CB79991DFB7135CD6B9FFA8F81E6BABE6F979350DF254C76EB3700B4FC6FCD7C64A48301B9651BE65B6D8344AB5FF077966352CD781598D2A22E1670BE58914F441126E5EE00E86EB80473F02 400
+[2025-12-09 14:58:58.087][000000024.059] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:58.096][000000024.059] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:58.106][000000024.059] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:58.120][000000024.060] I/user.FOTA_DATA 处理分段数据,包序号: 23
+[2025-12-09 14:58:58.128][000000024.060] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:58.139][000000024.060] I/user.FOTA_DATA 数据写入成功,当前总计: 4800 字节
+[2025-12-09 14:58:58.195][000000024.233] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:58.210][000000024.234] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:58.221][000000024.234] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:58.233][000000024.234] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:58.243][000000024.235] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:58.250][000000024.236] I/user.BLE 收到写请求: F000 F002 A79FFA80A5085511747E3225520E5E7EA9EB4197F064C508002E59A86BE8E01D452C4D901B5750A2F22B3F2C06EAA13EDC89CA3BA47519C06AC0314F12E502A56ABD7C0BFF6B38554945124DCC89A91303EABF303E9E9A770F356F1C40556467439AB6EF85EAC9BCA0400889115454620D304AF5E0DBA8F6B081501D850F7E9C773841683C448C7F88DA350A54C78FAB18FE3EC6EB9F815B5AE4D275949978C7B715E6EE1A3CD1089A562E013D1F8F06B82ED55ADA0807A8B3A569BD3571F314D31E7E15FD127C1A
+[2025-12-09 14:58:58.262][000000024.236] I/user.ble_packet_fota 处理写入数据 F000 F002 A79FFA80A5085511747E3225520E5E7EA9EB4197F064C508002E59A86BE8E01D452C4D901B5750A2F22B3F2C06EAA13EDC89CA3BA47519C06AC0314F12E502A56ABD7C0BFF6B38554945124DCC89A91303EABF303E9E9A770F356F1C40556467439AB6EF85EAC9BCA0400889115454620D304AF5E0DBA8F6B081501D850F7E9C773841683C448C7F88DA350A54C78FAB18FE3EC6EB9F815B5AE4D275949978C7B715E6EE1A3CD1089A562E013D1F8F06B82ED55ADA0807A8B3A569BD3571F314D31E7E15FD127C1A 400
+[2025-12-09 14:58:58.272][000000024.237] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:58.282][000000024.237] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:58.295][000000024.238] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:58.309][000000024.238] I/user.FOTA_DATA 处理分段数据,包序号: 24
+[2025-12-09 14:58:58.318][000000024.238] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:58.333][000000024.239] I/user.FOTA_DATA 数据写入成功,当前总计: 5000 字节
+[2025-12-09 14:58:58.344][000000024.360] I/user.fota version 001.000.000
+[2025-12-09 14:58:58.366][000000024.406] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:58.378][000000024.406] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:58.388][000000024.407] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:58.401][000000024.407] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:58.410][000000024.407] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:58.419][000000024.409] I/user.BLE 收到写请求: F000 F002 2039CF1E3E8982802E6F4EF78A2F8937794B315244FED3E2BFF073BCD2295FDF5389962FFCAB8366426A2761C52660A65E1F1DE4A43755B931638B893755C50FB98AEDE6C47A0F1FB7409F35E9D643B92D2FFFB83B7D4DF089A5B4D59E11F4C3EE1880FAFBA4734084172B980851667F0D9B4CAE42D0201B2EB499C54081C88D4047AF4FA7F78942DE71EA1F83ED968C06FA3A9535A84161E68CD2BD1AC86DF97D10FD024D6EC66716F0813BA243F9BACB624F68B7D4EBF478B0761F5CA0F3017703B761E054E4B4
+[2025-12-09 14:58:58.429][000000024.409] I/user.ble_packet_fota 处理写入数据 F000 F002 2039CF1E3E8982802E6F4EF78A2F8937794B315244FED3E2BFF073BCD2295FDF5389962FFCAB8366426A2761C52660A65E1F1DE4A43755B931638B893755C50FB98AEDE6C47A0F1FB7409F35E9D643B92D2FFFB83B7D4DF089A5B4D59E11F4C3EE1880FAFBA4734084172B980851667F0D9B4CAE42D0201B2EB499C54081C88D4047AF4FA7F78942DE71EA1F83ED968C06FA3A9535A84161E68CD2BD1AC86DF97D10FD024D6EC66716F0813BA243F9BACB624F68B7D4EBF478B0761F5CA0F3017703B761E054E4B4 400
+[2025-12-09 14:58:58.441][000000024.410] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:58.453][000000024.410] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:58.464][000000024.410] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:58.475][000000024.411] I/user.FOTA_DATA 处理分段数据,包序号: 25
+[2025-12-09 14:58:58.485][000000024.411] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:58.498][000000024.411] I/user.FOTA_DATA 数据写入成功,当前总计: 5200 字节
+[2025-12-09 14:58:58.537][000000024.583] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:58.547][000000024.584] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:58.558][000000024.584] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:58.572][000000024.584] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:58.586][000000024.584] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:58.601][000000024.586] I/user.BLE 收到写请求: F000 F002 25A7790B57C027D592D7C8C2BDCCBDD92322073E65330C3CCFE304B2A09197AB526EB517FEA520F87511FA17588F727D09994EFCB1945DCDAAAFEA1EC1A57B56C929FC47DAE333CF469ED2A3932DEE0F13D2B8D8DE28FDF4371AE9B89185486E88197B68BFCCDB392C256D1E8EF08F2D4D905D3D4E6B7326277F0A7F5BDB463C93500D630D0D8D715B826297DE265B0D7A81A82F6D06319E01D61DBBBC639A846094CE433EEFFF44B2EBA36E56CDFA2A0E100229DA1C5685A19D7E30AA8A5CD16F3FB4E6F2EF0606
+[2025-12-09 14:58:58.618][000000024.586] I/user.ble_packet_fota 处理写入数据 F000 F002 25A7790B57C027D592D7C8C2BDCCBDD92322073E65330C3CCFE304B2A09197AB526EB517FEA520F87511FA17588F727D09994EFCB1945DCDAAAFEA1EC1A57B56C929FC47DAE333CF469ED2A3932DEE0F13D2B8D8DE28FDF4371AE9B89185486E88197B68BFCCDB392C256D1E8EF08F2D4D905D3D4E6B7326277F0A7F5BDB463C93500D630D0D8D715B826297DE265B0D7A81A82F6D06319E01D61DBBBC639A846094CE433EEFFF44B2EBA36E56CDFA2A0E100229DA1C5685A19D7E30AA8A5CD16F3FB4E6F2EF0606 400
+[2025-12-09 14:58:58.634][000000024.587] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:58.651][000000024.587] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:58.665][000000024.587] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:58.680][000000024.588] I/user.FOTA_DATA 处理分段数据,包序号: 26
+[2025-12-09 14:58:58.695][000000024.588] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:58.710][000000024.588] I/user.FOTA_DATA 数据写入成功,当前总计: 5400 字节
+[2025-12-09 14:58:58.755][000000024.800] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:58.769][000000024.801] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:58.783][000000024.801] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:58.797][000000024.801] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:58.813][000000024.802] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:58.824][000000024.803] I/user.BLE 收到写请求: F000 F002 F866A14308D15F799B57A465462922B99F42A0EF8911C9A6CAF5F4BAB34A7DC4909E70E3E0D07A70984726604001FC9BE18E1D973B57FD29C8D29EB6830821BC63B59F0501ED48A33F83433589DEA55C7B42516996A1C0FC930C00456BB72B9096F1525E3054C97B15D820B30D629882D054AFD396757A5CBF722303312D30F897D9C5906F87BDA16BB377CD2901DD5D31853757A2C0DCF856DF62D0BFE8D00C3CF221F7E0D05A057C7F6C9528A2DC9119EACE8FD76D4B8BB2CE5233CC46239BC8E96600E5431810
+[2025-12-09 14:58:58.839][000000024.804] I/user.ble_packet_fota 处理写入数据 F000 F002 F866A14308D15F799B57A465462922B99F42A0EF8911C9A6CAF5F4BAB34A7DC4909E70E3E0D07A70984726604001FC9BE18E1D973B57FD29C8D29EB6830821BC63B59F0501ED48A33F83433589DEA55C7B42516996A1C0FC930C00456BB72B9096F1525E3054C97B15D820B30D629882D054AFD396757A5CBF722303312D30F897D9C5906F87BDA16BB377CD2901DD5D31853757A2C0DCF856DF62D0BFE8D00C3CF221F7E0D05A057C7F6C9528A2DC9119EACE8FD76D4B8BB2CE5233CC46239BC8E96600E5431810 400
+[2025-12-09 14:58:58.849][000000024.804] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:58.859][000000024.805] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:58.870][000000024.805] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:58.880][000000024.805] I/user.FOTA_DATA 处理分段数据,包序号: 27
+[2025-12-09 14:58:58.893][000000024.806] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:58.907][000000024.806] I/user.FOTA_DATA 数据写入成功,当前总计: 5600 字节
+[2025-12-09 14:58:58.946][000000024.978] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:58.955][000000024.979] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:58.969][000000024.979] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:58.980][000000024.979] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:58.993][000000024.980] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:59.002][000000024.981] I/user.BLE 收到写请求: F000 F002 273DCFAB5EB90608BD68CAE92F476307ED47CDB1FADB8F59AF83250DA720FD56B627A93D781456D69C19065CEC4130386A9817A0CFFBCF475779C15E22BC0E01D7C6DC447E315EA16CB207B814AD2035A4E8D403F3EEA12EDF088D8D32ABD17E77C44A62669DFE802CF1D876AE0BA4500F8BC0B1A83CFCA11B8828367D36BC7DC13F8C5D407E01F5B5D63B697F450A23BAC77868E86AE6D15F7605CD2AD4327A12367F4894071C247E88950D6B67DB33DBF1D97C3CFEF90059E74ECFD17584B08219639DC025B361
+[2025-12-09 14:58:59.014][000000024.981] I/user.ble_packet_fota 处理写入数据 F000 F002 273DCFAB5EB90608BD68CAE92F476307ED47CDB1FADB8F59AF83250DA720FD56B627A93D781456D69C19065CEC4130386A9817A0CFFBCF475779C15E22BC0E01D7C6DC447E315EA16CB207B814AD2035A4E8D403F3EEA12EDF088D8D32ABD17E77C44A62669DFE802CF1D876AE0BA4500F8BC0B1A83CFCA11B8828367D36BC7DC13F8C5D407E01F5B5D63B697F450A23BAC77868E86AE6D15F7605CD2AD4327A12367F4894071C247E88950D6B67DB33DBF1D97C3CFEF90059E74ECFD17584B08219639DC025B361 400
+[2025-12-09 14:58:59.022][000000024.982] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:59.036][000000024.982] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:59.046][000000024.983] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:59.056][000000024.983] I/user.FOTA_DATA 处理分段数据,包序号: 28
+[2025-12-09 14:58:59.066][000000024.983] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:59.082][000000024.984] I/user.FOTA_DATA 数据写入成功,当前总计: 5800 字节
+[2025-12-09 14:58:59.114][000000025.150] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:59.126][000000025.150] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:59.138][000000025.151] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:59.148][000000025.151] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:59.159][000000025.151] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:59.172][000000025.152] I/user.BLE 收到写请求: F000 F002 E16CA5457002B97D974F7F2314C9B30D52D58590364540B48C25334DED75C8F2395E52ABE5DFAA4A09A4066CA468E096201482BDBFE1686DF0EA1A27CEAC722337CCD7B98D0B3074B4C55D44C9D749D0A88F14CFD2247412C2F025DD7208C3E9BCDCCE2D9ED8CEA0EF23DD04CC780683C2A5895AFA3731DBAA3A450C6FB5E11D2BE7DE651F75AB461427C26881CB139B0A7D276B8569B1C8D5ADCCC11625652DEEDEE827C5B29A80F19ADE004DFC8F21BD87C6AA7DCB4F60B6E162CAF9A5E62886A49C50FBBAF959
+[2025-12-09 14:58:59.184][000000025.153] I/user.ble_packet_fota 处理写入数据 F000 F002 E16CA5457002B97D974F7F2314C9B30D52D58590364540B48C25334DED75C8F2395E52ABE5DFAA4A09A4066CA468E096201482BDBFE1686DF0EA1A27CEAC722337CCD7B98D0B3074B4C55D44C9D749D0A88F14CFD2247412C2F025DD7208C3E9BCDCCE2D9ED8CEA0EF23DD04CC780683C2A5895AFA3731DBAA3A450C6FB5E11D2BE7DE651F75AB461427C26881CB139B0A7D276B8569B1C8D5ADCCC11625652DEEDEE827C5B29A80F19ADE004DFC8F21BD87C6AA7DCB4F60B6E162CAF9A5E62886A49C50FBBAF959 400
+[2025-12-09 14:58:59.195][000000025.154] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:59.210][000000025.154] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:59.220][000000025.154] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:59.234][000000025.155] I/user.FOTA_DATA 处理分段数据,包序号: 29
+[2025-12-09 14:58:59.247][000000025.155] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:59.262][000000025.155] I/user.FOTA_DATA 数据写入成功,当前总计: 6000 字节
+[2025-12-09 14:58:59.299][000000025.327] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:59.311][000000025.327] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:59.321][000000025.328] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:59.335][000000025.328] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:59.344][000000025.328] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:59.353][000000025.330] I/user.BLE 收到写请求: F000 F002 8D6EFD873A5DAC4E398D88153FE9218962D9E75EFD381E5DD1C0B7A3FF565F3758352CD44841F5FFCA829E825ECABC359B996C6C0497D406DC6C5A81C43F941F2498AC674911BECFCBBD1A096B26C705BAA46EFE9EA3B59DFB364C463D990C6674FB2AAB8A5CFA01F8B234EEE49313B13AD3374C00AACA533D86C100E758A0ABBBB390D34FAC4C073C761AFA5B4FB0202B10E52E60A001CFA8F47BF9A82708CB2117C0B3A7C0A7DE779A4053DCA875105ACDEFC504C8BD3EE679ADFD46E18DCCA5BCDE70D7090A79
+[2025-12-09 14:58:59.366][000000025.330] I/user.ble_packet_fota 处理写入数据 F000 F002 8D6EFD873A5DAC4E398D88153FE9218962D9E75EFD381E5DD1C0B7A3FF565F3758352CD44841F5FFCA829E825ECABC359B996C6C0497D406DC6C5A81C43F941F2498AC674911BECFCBBD1A096B26C705BAA46EFE9EA3B59DFB364C463D990C6674FB2AAB8A5CFA01F8B234EEE49313B13AD3374C00AACA533D86C100E758A0ABBBB390D34FAC4C073C761AFA5B4FB0202B10E52E60A001CFA8F47BF9A82708CB2117C0B3A7C0A7DE779A4053DCA875105ACDEFC504C8BD3EE679ADFD46E18DCCA5BCDE70D7090A79 400
+[2025-12-09 14:58:59.380][000000025.331] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:59.389][000000025.331] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:59.400][000000025.331] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:59.414][000000025.332] I/user.FOTA_DATA 处理分段数据,包序号: 30
+[2025-12-09 14:58:59.428][000000025.332] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:59.441][000000025.332] I/user.FOTA_DATA 数据写入成功,当前总计: 6200 字节
+[2025-12-09 14:58:59.454][000000025.500] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:59.465][000000025.501] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:59.478][000000025.501] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:59.487][000000025.501] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:59.500][000000025.502] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:59.512][000000025.503] I/user.BLE 收到写请求: F000 F002 4A63D97FF5A93D477A75BD3B62DDB3F27D9657F6CE01F41645B735B2C4697B78198700C2738FAEF9A07E119AF56D0347F3166FC67AC5B90C7B5CD241EA3A3C52D0C7E186846785ED377AD9BD4F6F12FA208C5E108B246037653A371224C4255F8BAA9873EC667F632072FF09B42E3D7865D8AC39B80A0505ACB6AE952B265F8BAD0EAEC701EBA1BB752BC7E885A4F62C769F3E943CC7448E9F189F924F62B5C06FE950E992CF638FC999A42AE824046EF1F29A4F8EAF77E6B707E0E2F9CD88A317D8237DC676F68C
+[2025-12-09 14:58:59.526][000000025.503] I/user.ble_packet_fota 处理写入数据 F000 F002 4A63D97FF5A93D477A75BD3B62DDB3F27D9657F6CE01F41645B735B2C4697B78198700C2738FAEF9A07E119AF56D0347F3166FC67AC5B90C7B5CD241EA3A3C52D0C7E186846785ED377AD9BD4F6F12FA208C5E108B246037653A371224C4255F8BAA9873EC667F632072FF09B42E3D7865D8AC39B80A0505ACB6AE952B265F8BAD0EAEC701EBA1BB752BC7E885A4F62C769F3E943CC7448E9F189F924F62B5C06FE950E992CF638FC999A42AE824046EF1F29A4F8EAF77E6B707E0E2F9CD88A317D8237DC676F68C 400
+[2025-12-09 14:58:59.541][000000025.504] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:59.551][000000025.504] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:59.567][000000025.505] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:59.577][000000025.505] I/user.FOTA_DATA 处理分段数据,包序号: 31
+[2025-12-09 14:58:59.589][000000025.505] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:59.602][000000025.506] I/user.FOTA_DATA 数据写入成功,当前总计: 6400 字节
+[2025-12-09 14:58:59.630][000000025.677] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:59.642][000000025.677] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:59.651][000000025.678] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:59.666][000000025.678] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:59.683][000000025.678] I/user.BLE_EVENT 数据长度: 200 字节
+[2025-12-09 14:58:59.696][000000025.679] I/user.BLE 收到写请求: F000 F002 3AFFDF6E0E4B066DE6A80AF26C638752480CB2805DB597D7DBC8517F9BF383F6EAB90AA871C7C3BBEC893F5EF83F9A11DC2A6B037C20695FA1A612FBC4EFF0C3C73B06241AD49330A226465C0705572BEB57ABE04034739641974ED16ACB06C6AB6036E04EC55EDC011B4ECC42D44D0B0160F7F8EE32BB034AED332E7D18782113D98858FC681DE3EA6A0B08ABAD712D4ED878BBD3935179A6434D9E21040BDCF1777F923F706F9B7440CB5D58784F28891CDE4E9356B4062B7AAED398A5727762BA10429134D006
+[2025-12-09 14:58:59.711][000000025.680] I/user.ble_packet_fota 处理写入数据 F000 F002 3AFFDF6E0E4B066DE6A80AF26C638752480CB2805DB597D7DBC8517F9BF383F6EAB90AA871C7C3BBEC893F5EF83F9A11DC2A6B037C20695FA1A612FBC4EFF0C3C73B06241AD49330A226465C0705572BEB57ABE04034739641974ED16ACB06C6AB6036E04EC55EDC011B4ECC42D44D0B0160F7F8EE32BB034AED332E7D18782113D98858FC681DE3EA6A0B08ABAD712D4ED878BBD3935179A6434D9E21040BDCF1777F923F706F9B7440CB5D58784F28891CDE4E9356B4062B7AAED398A5727762BA10429134D006 400
+[2025-12-09 14:58:59.720][000000025.681] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:59.735][000000025.681] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:59.749][000000025.681] I/user.FOTA_DATA 收到数据包,长度: 200 字节
+[2025-12-09 14:58:59.764][000000025.682] I/user.FOTA_DATA 处理分段数据,包序号: 32
+[2025-12-09 14:58:59.781][000000025.682] I/user.FOTA_DATA 分段写入结果: result: true isDone: false
+[2025-12-09 14:58:59.795][000000025.682] I/user.FOTA_DATA 数据写入成功,当前总计: 6600 字节
+[2025-12-09 14:58:59.810][000000025.849] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:58:59.824][000000025.849] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:58:59.842][000000025.850] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:58:59.855][000000025.850] I/user.BLE_EVENT 特征值UUID: F002
+[2025-12-09 14:58:59.869][000000025.850] I/user.BLE_EVENT 数据长度: 191 字节
+[2025-12-09 14:58:59.882][000000025.851] I/user.BLE 收到写请求: F000 F002 707A137AE1B5F44B2AB40344A31ABFD272658424F0012C35BEE76B0869E6C61BE6232D58B976CB6E74F48C6161819EEA21BF257DD2136559E98A6DE63B0B4303FFA3F4C35CB4663B7118ED440B27474855F07C97AEB54B5448E3463A63A6C25DC5FF6ACB48D678E88515E34020A0B36B1D0553CCA70B4DC41BE54010968B6B535E716E1DFCCB500031B675CD1231701CA08E8002D79A07F80BD24D0EDC210AB9739E19461637A86AA4F64C5F7B878180B4DB18516A9D5A1473603E78782700
+[2025-12-09 14:58:59.893][000000025.852] I/user.ble_packet_fota 处理写入数据 F000 F002 707A137AE1B5F44B2AB40344A31ABFD272658424F0012C35BEE76B0869E6C61BE6232D58B976CB6E74F48C6161819EEA21BF257DD2136559E98A6DE63B0B4303FFA3F4C35CB4663B7118ED440B27474855F07C97AEB54B5448E3463A63A6C25DC5FF6ACB48D678E88515E34020A0B36B1D0553CCA70B4DC41BE54010968B6B535E716E1DFCCB500031B675CD1231701CA08E8002D79A07F80BD24D0EDC210AB9739E19461637A86AA4F64C5F7B878180B4DB18516A9D5A1473603E78782700 382
+[2025-12-09 14:58:59.908][000000025.853] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: nil 数据匹配: 1
+[2025-12-09 14:58:59.918][000000025.853] I/user.ble_packet_fota 数据特征值匹配,处理数据
+[2025-12-09 14:58:59.929][000000025.853] I/user.FOTA_DATA 收到数据包,长度: 191 字节
+[2025-12-09 14:58:59.940][000000025.853] I/user.FOTA_DATA 处理分段数据,包序号: 33
+[2025-12-09 14:58:59.954][000000025.876] I/fota common data done, now checking 0
+[2025-12-09 14:58:59.962][000000025.877] I/fota common data md5 ok
+[2025-12-09 14:58:59.969][000000025.877] I/fota only common data
+[2025-12-09 14:58:59.978][000000025.901] I/fota fota type 0 ok!, wait reboot
+[2025-12-09 14:58:59.986][000000025.902] I/user.FOTA_DATA 分段写入结果: result: true isDone: true
+[2025-12-09 14:59:00.000][000000025.902] I/user.FOTA_DATA 升级进度: 100 % ( 6791 / 6791 )
+[2025-12-09 14:59:00.015][000000025.902] I/user.FOTA_DATA 数据写入成功,当前总计: 6791 字节
+[2025-12-09 14:59:00.027][000000025.903] I/user.FOTA_DATA 所有数据已接收,等待升级完成...
+[2025-12-09 14:59:00.042][000000026.003] I/user.FOTA_DATA FOTA升级成功!
+[2025-12-09 14:59:00.056][000000026.003] I/user.FOTA_DATA 2秒后设备将自动重启...
+[2025-12-09 14:59:00.064][000000026.021] I/user.BLE_EVENT 收到BLE事件: 15
+[2025-12-09 14:59:00.077][000000026.021] I/user.BLE_EVENT 处理写入事件
+[2025-12-09 14:59:00.089][000000026.022] I/user.BLE_EVENT 服务UUID: F000
+[2025-12-09 14:59:00.103][000000026.022] I/user.BLE_EVENT 特征值UUID: F001
+[2025-12-09 14:59:00.114][000000026.022] I/user.BLE_EVENT 数据长度: 1 字节
+[2025-12-09 14:59:00.124][000000026.023] I/user.BLE 收到写请求: F000 F001 02
+[2025-12-09 14:59:00.134][000000026.024] I/user.ble_packet_fota 处理写入数据 F000 F001 02 2
+[2025-12-09 14:59:00.146][000000026.024] I/user.ble_packet_fota UUID匹配结果: 服务匹配: 1 命令匹配: 1 数据匹配: nil
+[2025-12-09 14:59:00.153][000000026.025] I/user.ble_packet_fota 命令特征值匹配,处理命令
+[2025-12-09 14:59:00.165][000000026.025] I/user.FOTA_CMD 收到命令数据: 02 长度: 1
+[2025-12-09 14:59:00.179][000000026.025] I/user.FOTA_CMD 解析命令码: 2 (0x02)
+[2025-12-09 14:59:00.190][000000026.026] I/user.FOTA_CMD 处理结束升级命令
+[2025-12-09 14:59:00.201][000000026.026] I/user.FOTA_CMD 验证文件完整性...
+[2025-12-09 14:59:00.210][000000026.026] I/user.FOTA_CMD 已接收: 6791 字节
+[2025-12-09 14:59:00.221][000000026.026] I/user.FOTA_CMD 应接收: 6791 字节
+[2025-12-09 14:59:00.229][000000026.027] I/user.FOTA_CMD 文件完整性验证通过
+[2025-12-09 14:59:00.242][000000026.027] I/user.FOTA_CMD 升级数据已全部接收,等待升级完成...
+[2025-12-09 14:59:00.251][000000026.127] I/user.FOTA_CMD FOTA升级成功!
+[2025-12-09 14:59:00.260][000000026.128] I/user.FOTA_CMD 2秒后设备将自动重启...
+[2025-12-09 14:59:00.274][000000026.128] I/user.FOTA_CMD 升级流程结束
+[2025-12-09 14:59:01.324][000000027.360] I/user.fota version 001.000.000
+[2025-12-09 14:59:01.965][000000028.004] I/user.FOTA_DATA 正在重启设备...
+
+```
+
+
+10、两种升级方式设备重启后现象一样,可以看到版本更新以及打印
+```lua
+[2025-12-05 16:32:51.986][000000003.361] I/user.fota version 001.000.001
+[2025-12-05 16:32:51.991][000000003.361] I/user.hezhou ble_fota test
+[2025-12-05 16:32:54.981][000000006.361] I/user.fota version 001.000.001
+[2025-12-05 16:32:54.985][000000006.362] I/user.hezhou ble_fota test
+```
+
+11、工具日志如下:
+```lua
+D:\gitee_hz\LuatOS\module\Air8000\demo\fota\ble_fota>python ble_fota_tool.py -f ble_fota.bin
+
+1. 加载固件文件...
+   固件加载完成,大小: 5197 字节
+
+2. 扫描目标设备...
+   正在扫描,请等待...
+   找到匹配设备: Air8000_FOTA (地址: C8:C2:C6:8F:09:10)
+   选择设备: Air8000_FOTA (地址: C8:C2:C6:8F:09:10)
+
+3. 建立BLE连接...
+   连接成功,状态: True
+
+4. 发现服务和特征值...
+   找到FOTA服务: 0000f000-0000-1000-8000-00805f9b34fb
+     特征值: 0000f001-0000-1000-8000-00805f9b34fb - 属性: ['write']
+       -> 命令特征值 (可写)
+     特征值: 0000f002-0000-1000-8000-00805f9b34fb - 属性: ['write']
+       -> 数据特征值 (可写)
+
+5. 发送开始升级命令...
+   连接成功,等待1秒...
+   写入特征值 f001,数据长度: 5 字节
+   开始命令发送完成
+
+6. 分块传输固件数据...
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   进度: 77.0% - 0.9 KB/s - 已发送 20 包 - 预计剩余: 1.4s
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 200 字节
+   写入特征值 f002,数据长度: 197 字节
+   进度: 100.0% - 0.9 KB/s - 已发送 26 包 - 预计剩余: 0.0s
+   数据传输完成! 总时间: 5.7s, 平均速度: 0.9 KB/s
+
+7. 发送结束升级命令...
+   写入特征值 f001,数据长度: 1 字节
+   结束命令发送完成
+
+8. 等待设备处理升级...
+
+==================================================
+升级流程完成! 设备应该正在重启...
+==================================================
+   已断开连接
+```

+ 149 - 0
module/Air8000/demo/modbus/rtu_master/main.lua

@@ -0,0 +1,149 @@
+--[[
+@module  main
+@summary LuatOS 用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+
+本 demo 演示的核心功能为:
+1、将设备配置为 modbus RTU 主站模式
+2、与从站 1 和 从站 2 进行通信
+    1. 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
+    2. 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作
+3、读取温湿度传感器数据
+    1. 配置 modbus RTU 主站,读取温湿度传感器数据
+    2. 每 2 秒读取一次传感器数据并解析温度和湿度值
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、在 main.lua 中 require "param_field" 模块,可以演示标准 modbus RTU 请求报文格式的使用方式
+3、在 main.lua 中 require "raw_frame" 模块,可以演示非标准 modbus RTU 请求报文格式的使用方式
+4、在 main.lua 中 require "temp_hum_sensor" 模块,可以演示读取485温湿度传感器数值的使用方式
+5、require "param_field"、require "raw_frame" 和 require "temp_hum_sensor",不要同时打开,否则功能会有冲突
+
+特别说明:
+关于 RTU 报文,exmodbus 扩展库支持通过 字段参数 或 原始帧 两种方式进行配置
+这两种配置方式本质都由用户将其放入 table 中在调用接口时传入,区别如下:
+1、字段参数方式
+    这种方式需要用户将请求报文进行解析后,将其放入 table 中,例如:
+    读取请求:
+    local config = {
+        slave_id = 1,                         -- 从站地址
+        reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+        start_addr = 0x0000,                  -- 寄存器起始地址
+        reg_count = 0x0002,                   -- 寄存器数量
+        timeout = 1000                        -- 超时时间
+    }
+    写入请求:
+    local config = {
+        slave_id = 2,                         -- 从站地址
+        reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+        start_addr = 0x0000,                  -- 寄存器起始地址
+        reg_count = 0x0002,                   -- 寄存器数量
+        data = {
+            [start_addr] = 0x0012,            -- 寄存器 0 的值
+            [start_addr + 1] = 0x0034,        -- 寄存器 1 的值
+        }
+        force_multiple = true, -- 是否强制使用多个寄存器写入操作(写多个线圈功能码:0x0F;写多个寄存器功能码:0x10)
+        timeout = 1000                        -- 超时时间
+    }
+    
+2、原始帧方式
+    这种方式只需要用户将原始请求报文放入 table 中,例如:
+    读取请求:
+    local config = {
+        raw_request = string.char(
+            0x01,       -- 从站地址
+            0x03,       -- 功能码:读取保持寄存器
+            0x00, 0x00, -- 寄存器起始地址
+            0x00, 0x02, -- 寄存器数量
+            0xC4, 0x0B  -- CRC16校验码
+        )
+        timeout = 1000  -- 超时时间
+    }
+    写入请求:
+    local config = {
+        raw_request = string.char(
+            0x02,       -- 从站地址
+            0x10,       -- 功能码:写入保持寄存器
+            0x00, 0x00, -- 寄存器起始地址
+            0x00, 0x02, -- 寄存器数量
+            0x04,       -- 字节数量
+            0x00, 0x12, -- 寄存器 0 的值
+            0x00, 0x34, -- 寄存器 1 的值
+            0x5D, 0x39  -- CRC16校验码
+        )
+        timeout = 1000  -- 超时时间
+    }
+如果你需要发送的请求报文是符合 modbus RTU 标准格式,可以使用 字段参数 或者 原始帧 方式
+如果你需要发送的请求报文是非标准格式,必须使用 原始帧 方式,使用 字段参数 方式会导致解析的数据不正确
+更多说明参考本目录下的 readme.md 文件;
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+PROJECT = "RTU_MASTER"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 以下三种方式只能选择其中一种打开,否则会导致功能冲突
+
+-- 加载 RTU 主站应用模块(字段参数方式)
+require "param_field"
+
+-- 加载 RTU 主站应用模块(原始帧方式)
+-- require "raw_frame"
+
+-- 加载温湿度传感器模块
+-- require "temp_hum_sensor"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 159 - 0
module/Air8000/demo/modbus/rtu_master/param_field.lua

@@ -0,0 +1,159 @@
+--[[
+@module  param_field
+@summary RTU 主站应用模块(字段参数方式)
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+本功能模块演示的内容为:
+1、将设备配置为 modbus RTU 主站模式
+2、与从站 1 和 从站 2 进行通信
+    1. 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
+    2. 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作
+        可通过修改字段参数 force_multiple 为 true 来强制使用写多个功能码(写多个线圈功能码:0x0F;写多个寄存器功能码:0x10)
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、本功能模块只适合使用标准 modbus RTU 请求报文格式的用户
+3、如果你使用的是非标准 modbus RTU 请求报文格式,请参考 raw_frame 功能模块
+
+本文件没有对外接口,直接在 main.lua 中 require "param_field" 就可以加载运行;
+]]
+
+local exmodbus = require("exmodbus")
+
+
+gpio.setup(16, 1)         -- Air8000 开发板 RS485 芯片供电引脚
+local rs485_dir_gpio = 17 -- Air8000 开发板 RS485 方向引脚
+
+
+-- 创建 RTU 主站配置参数;
+-- 说明:创建 RTU 主站时只需要配置如下参数即可;
+local create_config = {
+    -- 串口配置参数;
+    mode = exmodbus.RTU_MASTER,      -- 通信模式
+    uart_id = 1,                     -- UART 端口号
+    baud_rate = 115200,              -- 波特率
+    data_bits = 8,                   -- 数据位
+    stop_bits = 1,                   -- 停止位
+    parity_bits = uart.None,         -- 校验位
+    byte_order = uart.LSB,           -- 字节顺序
+    rs485_dir_gpio = rs485_dir_gpio, -- RS485 方向引脚
+    rs485_dir_rx_level = 0,          -- RS485 接收方向电平
+}
+
+-- 初始化从站 1 数据结构
+-- 用于记录从站 1 保持寄存器 0-1 的值;
+local slave1_data = {}
+
+-- 读取从站 1 保持寄存器 0-1 的值时,配置读命令的字段参数;
+local read_config = {
+    slave_id = 1,                         -- 从站地址 1
+    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+    start_addr = 0x0000,                  -- 起始地址 0
+    reg_count = 0x0002,                   -- 读取 2 个寄存器
+    timeout = 1000                        -- 超时时间 1000 ms
+}
+
+
+-- 初始化从站 2 数据结构;
+local slave2_data = {
+    data1 = 123,
+    data2 = 456
+}
+
+-- 定义从站 2 保持寄存器的起始地址;
+local start_addr = 0x0000
+
+-- 写入从站 2 保持寄存器 0-1 的值时,配置写命令的字段参数;
+local write_config = {
+    slave_id = 2,                                            -- 从站地址 2
+    reg_type = exmodbus.HOLDING_REGISTER,                    -- 寄存器类型:保持寄存器
+    start_addr = start_addr,                                 -- 起始地址 0
+    reg_count = 0x0002,                                      -- 写入 2 个寄存器
+    data = {
+        [start_addr] = slave2_data.data1,                    -- 第一个寄存器值
+        [start_addr + 1] = slave2_data.data2,                -- 第二个寄存器值
+    },                                                       -- 写入寄存器值
+    force_multiple = true,                                   -- 强制使用写多个功能码
+                                                                -- 设置为 true 时,写单个或多个线圈时强制功能码为 0x0F,写单个或多个保持寄存器时强制功能码为 0x10
+                                                                -- 设置为 false 时,写单个线圈时功能码为 0x05,写单个保持寄存器时功能码为 0x06,写多个线圈时功能码为 0x0F,写多个保持寄存器时功能码为 0x10
+    timeout = 1000                                           -- 超时时间 1000 ms
+}
+
+
+-- 创建 RTU 主站实例
+local rtu_master = exmodbus.create(create_config)
+
+-- 判断主站是否创建成功并记录日志
+if not rtu_master then
+    log.info("exmodbus_test", "rtu_master 创建失败")
+else
+    log.info("exmodbus_test", "rtu_master 创建成功")
+end
+
+-- 读取从站 1 保持寄存器数据的函数
+local function read_slave1_holding_registers()
+
+    log.info("exmodbus_test", "开始读取从站 1 保持寄存器 0-1 的值")
+
+    -- 执行读取操作
+    local read_result = rtu_master:read(read_config)
+
+    -- 根据返回状态处理结果
+    if read_result.status == exmodbus.STATUS_SUCCESS then
+        slave1_data.data1 = read_result.data[read_config.start_addr]
+        slave1_data.data2 = read_result.data[read_config.start_addr + 1]
+        log.info("exmodbus_test", "成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为", slave1_data.data1, ",寄存器 1 数值为", slave1_data.data2)
+    elseif read_result.status == exmodbus.STATUS_DATA_INVALID then
+        log.info("exmodbus_test", "收到从站 1 的响应数据但数据损坏/校验失败")
+    elseif read_result.status == exmodbus.STATUS_EXCEPTION then
+        log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", read_result.execption_code)
+    elseif read_result.status == exmodbus.STATUS_TIMEOUT then
+        log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
+    end
+end
+
+-- 写入从站 2 保持寄存器数据的函数
+local function write_slave2_holding_registers()
+
+    log.info("exmodbus_test", "开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为", slave2_data.data1, ",寄存器 1 数值为", slave2_data.data2)
+
+    -- 执行写入操作
+    local write_result = rtu_master:write(write_config)
+
+    -- 根据返回状态处理结果
+    if write_result.status == exmodbus.STATUS_SUCCESS then
+        log.info("exmodbus_test", "成功写入从站 2 保持寄存器 0-1 的值")
+    elseif write_result.status == exmodbus.STATUS_DATA_INVALID then
+        log.info("exmodbus_test", "收到从站 2 的响应数据但数据损坏/校验失败")
+    elseif write_result.status == exmodbus.STATUS_EXCEPTION then
+        log.info("exmodbus_test", "收到从站 2 的 modbus 标准异常响应,异常码为", write_result.execption_code)
+    elseif write_result.status == exmodbus.STATUS_TIMEOUT then
+        log.info("exmodbus_test", "未收到从站 2 的响应(超时)")
+    end
+end
+
+-- 定时任务函数:每 2 秒调用一次读取函数,每 4 秒调用一次写入函数
+local function task()
+
+    local count = 0 -- 计数器
+
+    while true do
+        if rtu_master then
+            -- 每 2 秒调用一次读取函数
+            read_slave1_holding_registers()
+            if count == 0 then
+                -- 每 4 秒调用一次写入函数
+                write_slave2_holding_registers()
+            end
+            count = (count + 1) % 2
+        else
+            log.info("exmodbus_test", "rtu_master 未创建,无法执行 read_slave1_holding_registers()")
+        end
+        sys.wait(2000)
+    end
+end
+
+-- 初始化任务
+sys.taskInit(task)

+ 257 - 0
module/Air8000/demo/modbus/rtu_master/raw_frame.lua

@@ -0,0 +1,257 @@
+--[[
+@module  raw_frame
+@summary RTU 主站应用模块(原始帧方式)
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+本功能模块演示的内容为:
+1、将设备配置为 modbus RTU 主站模式
+2、与从站 1 和 从站 2 进行通信
+    1. 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
+    2. 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、本功能模块只适合使用非标准 modbus RTU 请求报文格式的用户
+3、如果你使用的是标准 modbus RTU 请求报文格式,请参考 param_field 功能模块
+
+本文件没有对外接口,直接在 main.lua 中 require "raw_frame" 就可以加载运行;
+]]
+
+local exmodbus = require("exmodbus")
+
+
+gpio.setup(16, 1)         -- Air8000 开发板 RS485 芯片供电引脚
+local rs485_dir_gpio = 17 -- Air8000 开发板 RS485 方向引脚
+
+
+-- 创建 RTU 主站配置参数;
+-- 说明:创建 RTU 主站时只需要配置如下参数即可;
+local create_config = {
+    -- 串口配置参数;
+    mode = exmodbus.RTU_MASTER,      -- 通信模式
+    uart_id = 1,                     -- UART 端口号
+    baud_rate = 115200,              -- 波特率
+    data_bits = 8,                   -- 数据位
+    stop_bits = 1,                   -- 停止位
+    parity_bits = uart.None,         -- 校验位
+    byte_order = uart.LSB,           -- 字节顺序
+    rs485_dir_gpio = rs485_dir_gpio, -- RS485 方向引脚
+    rs485_dir_rx_level = 0,          -- RS485 接收方向电平
+}
+
+-- 初始化从站 1 数据结构
+-- 用于记录从站 1 保持寄存器 0-1 的值;
+local slave1_data = {}
+
+-- 配置读取从站 1 保持寄存器 0-1 的值;
+local read_config = {
+    raw_request = string.char(
+        0x01,           -- 从站地址
+        0x03,           -- 功能码:读取保持寄存器
+        0x00, 0x00,     -- 寄存器起始地址
+        0x00, 0x02,     -- 寄存器数量
+        0xC4, 0x0B      -- CRC16校验码
+    ),
+    timeout = 1000      -- 超时时间 1000 ms
+}
+
+
+-- 配置写入从站 2 保持寄存器 0-1 的值;
+local write_config = {
+    raw_request = string.char(
+        0x02,           -- 从站地址
+        0x10,           -- 功能码:写入保持寄存器
+        0x00, 0x00,     -- 寄存器起始地址
+        0x00, 0x02,     -- 寄存器数量
+        0x04,           -- 字节数量
+        0x00, 0x12,     -- 寄存器 0 的值
+        0x00, 0x34,     -- 寄存器 1 的值
+        0x5D, 0x39      -- CRC16校验码
+    ),
+    timeout = 1000,     -- 超时时间 1000 ms
+}
+
+
+-- 创建 RTU 主站实例
+local rtu_master = exmodbus.create(create_config)
+
+
+-- 判断主站是否创建成功并记录日志
+if not rtu_master then
+    log.info("exmodbus_test", "rtu_master 创建失败")
+else
+    log.info("exmodbus_test", "rtu_master 创建成功")
+end
+
+
+-- 读取从站 1 保持寄存器数据的函数
+local function read_slave1_holding_registers()
+
+    log.info("exmodbus_test", "开始读取从站 1 保持寄存器 0-1 的值")
+
+    -- 执行读取操作
+    local read_result = rtu_master:read(read_config)
+
+    -- 根据返回状态处理结果
+    if read_result.status == exmodbus.STATUS_SUCCESS then
+        local resp = read_result.raw_response
+
+        -- 特别说明:
+        -- 接下来的判断是针对 modbus RTU 标准响应格式的应答原始帧来解析的
+        -- 在实际项目中,应根据自己项目中的实际应答原始帧格式进行解析
+        -- 如果实际格式与此处演示的格式不一致,需要修改接下来的解析代码
+
+        -- 1. 检查总长度:必须为 9 字节(1 地址 + 1 功能码 + 1 字节数 + 4 数据 + 2 CRC)
+        if #resp ~= 9 then
+            log.info("exmodbus_test", "响应长度错误,期望 9 字节,实际:", #resp)
+            return
+        end
+
+        -- 2. 检查从站地址
+        if string.byte(resp, 1) ~= 0x01 then
+            log.info("exmodbus_test", "从站地址不匹配,收到:", string.byte(resp, 1))
+            return
+        end
+
+        -- 3. 检查功能码
+        local func_code = string.byte(resp, 2)
+        if func_code == 0x83 then
+            local exc_code = string.byte(resp, 3)
+            log.info("exmodbus_test", "从站返回异常响应,异常码:", exc_code)
+            return
+        elseif func_code ~= 0x03 then
+            log.info("exmodbus_test", "功能码错误,收到:", func_code)
+            return
+        end
+
+        -- 4. 检查字节数字段(应为 4)
+        local byte_count = string.byte(resp, 3)
+        if byte_count ~= 4 then
+            log.info("exmodbus_test", "字节数字段错误,期望 4,实际:", byte_count)
+            return
+        end
+
+        -- 5. 校验CRC
+        -- 计算前 7 字节的 CRC
+        local crc_calculated = crypto.crc16_modbus(resp:sub(1, 7))
+        -- 提取接收到的 CRC
+        local crc_received = string.unpack("<I2", resp, 8)
+        -- 比较 CRC
+        if crc_calculated ~= crc_received then
+            log.info("exmodbus_test", "CRC 校验错误,计算值:", crc_calculated, ",接收值:", crc_received)
+            return
+        end
+
+        -- 6. 解析寄存器数据(从第 4 字节开始,大端序)
+        local data1 = string.unpack(">I2", resp, 4) -- 寄存器 0,偏移 4
+        local data2 = string.unpack(">I2", resp, 6) -- 寄存器 1,偏移 6
+
+        -- 7. 记录数据
+        slave1_data[0] = data1
+        slave1_data[1] = data2
+
+        -- 8. 记录日志
+        log.info("exmodbus_test", "成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为", slave1_data[0], ",寄存器 1 数值为", slave1_data[1])
+    elseif read_result.status == exmodbus.STATUS_TIMEOUT then
+        log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
+    end
+end
+
+
+-- 写入从站 2 保持寄存器数据的函数
+local function write_slave2_holding_registers()
+
+    log.info("exmodbus_test", "开始写入从站 2 保持寄存器 0-1 的值")
+
+    -- 执行写入操作
+    local write_result = rtu_master:write(write_config)
+
+    -- 根据返回状态处理结果
+    if write_result.status == exmodbus.STATUS_SUCCESS then
+        local resp = write_result.raw_response
+
+        -- 特别说明:
+        -- 接下来的判断是针对 modbus RTU 标准响应格式的应答原始帧来解析的
+        -- 在实际项目中,应根据自己项目中的实际应答原始帧格式进行解析
+        -- 如果实际格式与此处演示的格式不一致,需要修改接下来的解析代码
+
+        -- 1. 检查总长度:必须为 8 字节(1 地址 + 1 功能码 + 2 起始地址 + 2 寄存器数量 + 2 CRC)
+        if #resp ~= 8 then
+            log.info("exmodbus_test", "响应长度错误,期望 8 字节,实际:", #resp)
+            return
+        end
+
+        -- 2. 检查从站地址
+        if string.byte(resp, 1) ~= 0x02 then
+            log.info("exmodbus_test", "从站地址不匹配,收到:", string.byte(resp, 1))
+            return
+        end
+
+        -- 3. 检查功能码
+        local func_code = string.byte(resp, 2)
+        if func_code == 0x90 then
+            local exc_code = string.byte(resp, 3)
+            log.info("exmodbus_test", "从站返回异常响应,异常码:", exc_code)
+            return
+        elseif func_code ~= 0x10 then
+            log.info("exmodbus_test", "功能码错误,收到:", func_code)
+            return
+        end
+
+        -- 4. 检查起始地址(应为 0x0000)
+        local start_addr = string.unpack(">I2", resp, 3)
+        if start_addr ~= 0x0000 then
+            log.info("exmodbus_test", "起始地址不匹配,收到:", start_addr)
+            return
+        end
+
+        -- 5. 检查寄存器数量(应为 0x0002)
+        local reg_count = string.unpack(">I2", resp, 5)
+        if reg_count ~= 0x0002 then
+            log.info("exmodbus_test", "寄存器数量错误,期望 0x0002,实际:", reg_count)
+            return
+        end
+
+        -- 6. 校验CRC
+        -- 计算前 6 字节的 CRC
+        local crc_calculated = crypto.crc16_modbus(resp:sub(1, 6))
+        -- 提取接收到的 CRC
+        local crc_received = string.unpack("<I2", resp, 7)
+        -- 比较 CRC
+        if crc_calculated ~= crc_received then
+            log.info("exmodbus_test", "CRC 校验错误,计算值:", crc_calculated, ",接收值:", crc_received)
+            return
+        end
+
+        log.info("exmodbus_test", "成功写入从站 2 保持寄存器 0-1")
+    elseif write_result.status == exmodbus.STATUS_TIMEOUT then
+        log.info("exmodbus_test", "未收到从站 2 的响应(超时)")
+    end
+end
+
+-- 定时任务函数:每 2 秒调用一次读取函数,每 4 秒调用一次写入函数
+local function task()
+
+    local count = 0 -- 计数器
+
+    while true do
+        if rtu_master then
+            -- 每 2 秒调用一次读取函数
+            read_slave1_holding_registers()
+            if count == 0 then
+                -- 每 4 秒调用一次写入函数
+                write_slave2_holding_registers()
+            end
+            count = (count + 1) % 2
+        else
+            log.info("exmodbus_test", "rtu_master 未创建,无法执行 read_slave1_holding_registers()")
+        end
+        sys.wait(2000)
+    end
+end
+
+
+-- 初始化任务
+sys.taskInit(task)

+ 444 - 0
module/Air8000/demo/modbus/rtu_master/readme.md

@@ -0,0 +1,444 @@
+## 演示模块概述
+
+1、main.lua:主程序入口;
+
+2、param_field.lua:RTU 主站应用模块(字段参数方式);
+
+3、raw_frame.lua:RTU 主站应用模块(原始帧方式);
+
+4、temp_hum_sensor.lua:485温湿度传感器读取模块;
+
+## 演示功能概述
+
+本 demo 演示的核心功能为:
+
+1、将设备配置为 modbus RTU 主站模式
+
+2、与从站 1 和 从站 2 进行通信
+
+- 对从站 1 进行 2 秒一次的读取保持寄存器 0-1 操作
+- 对从站 2 进行 4 秒一次的写入保持寄存器 0-1 操作
+
+3、读取温湿度传感器数据
+
+- 配置 modbus RTU 主站,读取温湿度传感器数据
+- 每 2 秒读取一次传感器数据并解析温度和湿度值
+
+
+
+注意事项:
+
+1、该示例程序需要搭配 exmodbus 扩展库使用
+
+2、在 main.lua 中 require "param_field" 模块,可以演示标准 modbus RTU 请求报文格式的使用方式
+
+3、在 main.lua 中 require "raw_frame" 模块,可以演示非标准 modbus RTU 请求报文格式的使用方式
+
+4、在 main.lua 中 require "temp_hum_sensor" 模块,可以演示读取485温湿度传感器数值的使用方式
+
+5、require "param_field"、require "raw_frame" 和 require "temp_hum_sensor",不要同时打开,否则功能会有冲突
+
+
+
+特别说明:
+
+关于 RTU 报文,exmodbus 扩展库支持通过 字段参数 或 原始帧 两种方式进行配置
+
+这两种配置方式本质都由用户将其放入 table 中在调用接口时传入,区别如下:
+
+1、字段参数方式
+
+这种方式需要用户将请求报文进行解析后,将其放入 table 中,例如:
+
+```lua
+-- 读取请求:
+local config = {
+    slave_id = 1,                         -- 从站地址
+    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+    start_addr = 0x0000,                  -- 寄存器起始地址
+    reg_count = 0x0002,                   -- 寄存器数量
+    timeout = 1000                        -- 超时时间
+}
+
+-- 写入请求:
+local config = {
+    slave_id = 2,                         -- 从站地址
+    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+    start_addr = 0x0000,                  -- 寄存器起始地址
+    reg_count = 0x0002,                   -- 寄存器数量
+    data = {
+        [start_addr] = 0x0012,            -- 寄存器 0 的值
+        [start_addr + 1] = 0x0034,        -- 寄存器 1 的值
+    }
+    force_multiple = true, -- 是否强制使用多个寄存器写入操作(写多个线圈功能码:0x0F;写多个寄存器功能码:0x10)
+    timeout = 1000                        -- 超时时间
+}
+```
+
+2、原始帧方式
+
+这种方式只需要用户将原始请求报文放入 table 中,例如:
+
+```Lua
+-- 读取请求:
+local config = {
+    raw_request = string.char(
+        0x01,       -- 从站地址
+        0x03,       -- 功能码:读取保持寄存器
+        0x00, 0x00, -- 寄存器起始地址
+        0x00, 0x02, -- 寄存器数量
+        0xC4, 0x0B  -- CRC16校验码
+    )
+    timeout = 1000  -- 超时时间
+}
+
+-- 写入请求:
+local config = {
+    raw_request = string.char(
+        0x02,       -- 从站地址
+        0x10,       -- 功能码:写入保持寄存器
+        0x00, 0x00, -- 寄存器起始地址
+        0x00, 0x02, -- 寄存器数量
+        0x04,       -- 字节数量
+        0x00, 0x12, -- 寄存器 0 的值
+        0x00, 0x34, -- 寄存器 1 的值
+        0x5D, 0x39  -- CRC16校验码
+    )
+    timeout = 1000  -- 超时时间
+}
+```
+
+如果你需要发送的请求报文是符合 modbus RTU 标准格式,可以使用 字段参数 或者 原始帧 方式
+
+如果你需要发送的请求报文是非标准格式,必须使用 原始帧 方式,使用 字段参数 方式会导致解析的数据不正确
+
+## 演示硬件环境
+
+1、Air8000 开发板一块
+
+2、TYPE-C USB数据线一根
+
+3、USB-RS485 串口板
+
+> 此处购买链接仅为推荐,如有问题请直接联系店家
+
+- 购买链接:https://e.tb.cn/h.7YHZDX57ex5G68h?tk=AvE4fsurFy8
+
+4、气体浓度变送器(RS485 版)
+
+> 如果你是小白,建议直接购买同款变送器,由于不同型号的温湿度模块默认的参数也会有所区别
+
+- 购买链接:https://e.tb.cn/h.71oGcXUfrc46J83?tk=RJ1WfsuBfuY
+
+Air8000 与 USB-RS385 串口板接线图如下:
+
+![](https://docs.openluat.com/cdn/image/Air8000_rs485.png)
+
+Air8000 与气体浓度变送器(RS-485 版)接线图如下:
+
+![](https://docs.openluat.com/cdn/image/Air8000_RS485.jpg)
+
+## 演示软件环境
+
+1、[Luatools下载调试工具](https://docs.openluat.com/air8000/luatos/common/download/)
+
+2、[Air8000 V2018 版本](https://docs.openluat.com/air8000/luatos/firmware/)(理论上最新版本固件也可以,如果使用最新版本的固件不可以,可以烧录 V2018-1 固件对比验证)
+
+3、[摩尔信使(MThings)官网](https://www.gulink.cn/)(用于模拟 modbus 从站设备)
+
+## 演示核心步骤
+
+### RTU 主站应用模块(字段参数方式,对应 param_field.lua)
+
+1、搭建硬件环境
+
+- 将 USB-RS485 串口板与 Air8000 开发板进行连接
+- 将 USB-RS485 串口板 与 Air8000 开发板的 USB 端同时接在电脑上
+- 参考图见 演示硬件环境
+
+2、在摩尔信使上配置模拟 RTU 从站设备环境
+
+- 点击左上角的 “通道管理”按钮,在 “通道管理” 窗口选择对应的串口(USB-RS485 串口板与 Air8000 开发板进行 485 通信时的端口),点击对应串口后面的 “配置” 按钮,在 “串口参数配置” 窗口配置串口参数(要求与代码中调用 exmodbus.create 接口时填入的配置参数一致),操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/1.png)
+
+- 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/2.png)
+
+- 点击左侧的第一个从站(我这里显示为 “COM36-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/3.png)
+
+- 点击左侧的第二个从站(我这里显示为 “COM36-002”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/4.png)
+
+- 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/5.png)
+
+3、调整软件代码
+
+- 打开 require "param_field" ,注释掉 require "raw_frame" 和 require "temp_hum_sensor",操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/modbus/1.png)
+
+4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码
+
+5、烧录成功后,自动开机运行
+
+6、开机运行后 Luatools 工具上记录的日志如下:
+
+```
+[2025-12-08 14:14:56.464][000000000.394] I/user.main RTU_MASTER 001.000.000
+[2025-12-08 14:14:56.468][000000000.425] Uart_ChangeBR 1338:uart1, 115200 115203 26000000 3611
+[2025-12-08 14:14:56.472][000000000.426] I/user.exmodbus 串口 1 初始化成功,波特率 115200
+[2025-12-08 14:14:56.477][000000000.426] I/user.exmodbus_test rtu_master 创建成功
+[2025-12-08 14:14:56.481][000000000.427] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:14:57.124][000000001.433] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:14:57.128][000000001.433] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:14:58.135][000000002.437] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 14:15:00.139][000000004.438] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:01.139][000000005.441] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:03.144][000000007.442] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:04.140][000000008.445] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:04.146][000000008.446] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:05.149][000000009.449] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 14:15:07.139][000000011.450] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:08.146][000000012.453] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:10.148][000000014.454] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:11.154][000000015.457] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:11.165][000000015.458] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:12.160][000000016.461] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 14:15:14.150][000000018.462] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:15.158][000000019.465] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:17.166][000000021.466] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:18.160][000000022.469] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:15:18.163][000000022.470] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:18.177][000000022.486] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 14:15:20.178][000000024.487] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:20.207][000000024.503] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:22.197][000000026.504] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:22.228][000000026.519] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:22.231][000000026.520] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:22.235][000000026.534] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 14:15:24.224][000000028.534] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:24.255][000000028.550] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:26.246][000000030.551] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:26.257][000000030.569] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:26.265][000000030.570] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:26.275][000000030.583] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 14:15:28.284][000000032.584] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:28.289][000000032.598] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:30.292][000000034.599] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:30.309][000000034.614] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 14:15:30.322][000000034.615] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:30.334][000000034.634] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
+```
+
+7、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air8000 之间的串口通道关闭后,此时 Air8000 在发送请求时便会收不到响应,Luatools 工具上显示的日志如下:
+
+![](https://docs.openluat.com/cdn/image/MThings/6.png)
+
+```
+[2025-12-08 14:14:56.481][000000000.427] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:14:57.124][000000001.433] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 14:14:57.128][000000001.433] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:14:58.135][000000002.437] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+```
+
+8、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air8000 之间的串口通道打开后,此时 Air8000 在发送请求时便会接收到响应,Luatools 工具与摩尔信使上显示的日志如下:
+
+![](https://docs.openluat.com/cdn/image/MThings/7.png)
+
+```
+[2025-12-08 14:15:18.163][000000022.470] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值,寄存器 0 数值为 123 ,寄存器 1 数值为 456
+[2025-12-08 14:15:18.177][000000022.486] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 14:15:20.178][000000024.487] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 14:15:20.207][000000024.503] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/8.png)
+
+9、关于 Air8000 执行读取和写入请求后,摩尔信使上位机的数值变化如下图所示:
+
+![](https://docs.openluat.com/cdn/image/MThings/9.png)
+
+![](https://docs.openluat.com/cdn/image/MThings/10.png)
+
+### RTU 主站应用模块(原始帧方式,对应 raw_frame.lua)
+
+1、搭建硬件环境
+
+- 将 USB-RS485 串口板与 Air8000 开发板进行连接
+- 将 USB-RS485 串口板 与 Air8000 开发板的 USB 端同时接在电脑上
+- 参考图见 演示硬件环境
+
+2、在摩尔信使上配置模拟 RTU 从站设备环境
+
+- 点击左上角的 “通道管理”按钮,在 “通道管理” 窗口选择对应的串口(USB-RS485 串口板与 Air8000 开发板进行 485 通信时的端口),点击对应串口后面的 “配置” 按钮,在 “串口参数配置” 窗口配置串口参数(要求与代码中调用 exmodbus.create 接口时填入的配置参数一致),操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/1.png)
+
+- 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/2.png)
+
+- 点击左侧的第一个从站(我这里显示为 “COM36-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/3.png)
+
+- 点击左侧的第二个从站(我这里显示为 “COM36-002”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增保持寄存器 0 和 保持寄存器 1,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/4.png)
+
+- 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/5.png)
+
+3、调整软件代码
+
+- 打开 require "raw_frame" ,注释掉 require "param_field" 和 require "temp_hum_sensor",操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/modbus/2.png)
+
+4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码
+
+5、烧录成功后,自动开机运行
+
+6、开机运行后 Luatools 工具上记录的日志如下:
+
+```
+[2025-12-08 15:14:13.691][000000000.703] I/user.main RTU_MASTER 001.000.000
+[2025-12-08 15:14:13.695][000000000.736] Uart_ChangeBR 1338:uart1, 115200 115203 26000000 3611
+[2025-12-08 15:14:13.699][000000000.737] I/user.exmodbus 串口 1 初始化成功,波特率 115200
+[2025-12-08 15:14:13.702][000000000.737] I/user.exmodbus_test rtu_master 创建成功
+[2025-12-08 15:14:13.706][000000000.738] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:14.595][000000001.743] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:14.600][000000001.743] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:15.602][000000002.747] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 15:14:17.591][000000004.747] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:18.591][000000005.750] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:20.591][000000007.750] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:21.602][000000008.754] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:21.611][000000008.754] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:22.607][000000009.758] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 15:14:24.603][000000011.758] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:25.612][000000012.762] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:27.618][000000014.762] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:28.616][000000015.766] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:28.620][000000015.766] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:29.623][000000016.770] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+[2025-12-08 15:14:31.615][000000018.770] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:31.646][000000018.790] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:33.633][000000020.790] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:33.651][000000020.808] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:33.656][000000020.808] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:33.662][000000020.821] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
+[2025-12-08 15:14:35.670][000000022.821] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:35.682][000000022.839] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:37.692][000000024.839] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:37.699][000000024.854] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:37.705][000000024.854] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:37.716][000000024.870] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
+[2025-12-08 15:14:39.717][000000026.870] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:39.748][000000026.887] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:41.737][000000028.887] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:41.753][000000028.902] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:41.755][000000028.902] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:41.758][000000028.916] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
+[2025-12-08 15:14:43.765][000000030.916] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:43.795][000000030.934] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:45.788][000000032.934] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:45.795][000000032.949] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:45.801][000000032.949] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:45.812][000000032.964] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
+```
+
+7、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air8000 之间的串口通道关闭后,此时 Air8000 在发送请求时便会收不到响应,Luatools 工具上显示的日志如下:
+
+![](https://docs.openluat.com/cdn/image/MThings/6.png)
+
+```
+[2025-12-08 15:14:13.706][000000000.738] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:14.595][000000001.743] I/user.exmodbus_test 未收到从站 1 的响应(超时)
+[2025-12-08 15:14:14.600][000000001.743] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:15.602][000000002.747] I/user.exmodbus_test 未收到从站 2 的响应(超时)
+```
+
+8、如下图所示,鼠标右键点击 “通道” 下方的按钮,当我们把摩尔信使上由上位机与 Air8000 之间的串口通道打开后,此时 Air8000 在发送请求时便会接收到响应,Luatools 工具与摩尔信使上显示的日志如下:
+
+> 程序设计为每隔 2 秒执行一次读取,每隔 4 秒执行一次写入,在日志上呈现出现就是先执行两次读取再执行一次写入
+
+![](https://docs.openluat.com/cdn/image/MThings/7.png)
+
+```
+[2025-12-08 15:14:31.615][000000018.770] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:31.646][000000018.790] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:33.633][000000020.790] I/user.exmodbus_test 开始读取从站 1 保持寄存器 0-1 的值
+[2025-12-08 15:14:33.651][000000020.808] I/user.exmodbus_test 成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为 0 ,寄存器 1 数值为 0
+[2025-12-08 15:14:33.656][000000020.808] I/user.exmodbus_test 开始写入从站 2 保持寄存器 0-1 的值
+[2025-12-08 15:14:33.662][000000020.821] I/user.exmodbus_test 成功写入从站 2 保持寄存器 0-1
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/11.png)
+
+9、关于 Air8000 执行读取和写入请求后,摩尔信使上位机的数值变化如下图所示:
+
+![](https://docs.openluat.com/cdn/image/MThings/9.png)
+
+![](https://docs.openluat.com/cdn/image/MThings/10.png)
+
+### 485 温湿度传感器读取模块(对应 temp_hum_sensor.lua)
+
+1、搭建硬件环境
+
+- 将气体浓度变送器(RS-485 版)的 '+' 和 '-' 与供电设备(稳压电源等)进行连接,供电范围 8V ~ 36V DC
+
+- 将气体浓度变送器(RS-485 版)与 Air8000 开发板进行连接
+- 将 Air8000 开发板的 USB 端接在电脑上
+- 参考图见 演示硬件环境
+
+2、了解气体浓度变送器(RS-485 版)
+
+- 该变送器模块上电后默认输出数据,从站地址默认为 1,波特率默认为 9600
+- 温度传感器数值通过保持寄存器地址 0x001E 输出,输出数据为 16 位有符号整数(-0x7FFF ~ +0x7FFF),
+  - 数据范围为 -40℃ ~ +85℃,分辨率为 0.1℃
+  - 注:寄存器值为 235,实际温度值为 235 * 0.1 = 23.5
+- 湿度传感器对应保持寄存器地址 0x001F 输出,输出数据为 16 位无符号整数(0 ~ 0xFFFF)
+  - 数据范围为 0%RH ~ 85%RH,分辨率为 0.1%RH
+  - 注:寄存器值为 653,实际湿度值为 653 * 0.1 = 65.3
+
+3、调整软件代码
+
+- 打开 require "temp_hum_sensor" ,注释掉 require "raw_frame" 和 require "param_field" ,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/modbus/3.png)
+
+4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码,为气体浓度变送器(RS-485 版)进行供电(提前通电也可以)
+
+5、烧录成功后,自动开机运行
+
+6、开机运行后 Luatools 工具上记录的日志如下:
+
+```
+[2025-12-09 22:11:43.077][000000000.588] I/user.main RTU_MASTER 001.000.000
+[2025-12-09 22:11:43.092][000000000.620] Uart_ChangeBR 1338:uart1, 9600 9600 26000000 43333
+[2025-12-09 22:11:43.101][000000000.621] I/user.exmodbus 串口 1 初始化成功,波特率 9600
+[2025-12-09 22:11:43.112][000000000.621] I/user.temp_hum_sensor RTU 主站创建成功
+[2025-12-09 22:11:43.123][000000000.622] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:43.138][000000000.724] I/user.temp_hum_sensor 读取成功,温度为 16.70000 ℃,湿度为 83.20000 %RH
+[2025-12-09 22:11:43.154][000000002.724] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:43.166][000000002.761] I/user.temp_hum_sensor 读取成功,温度为 16.80000 ℃,湿度为 82.90000 %RH
+[2025-12-09 22:11:43.172][000000004.761] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:43.178][000000004.793] I/user.temp_hum_sensor 读取成功,温度为 16.60000 ℃,湿度为 83.50000 %RH
+[2025-12-09 22:11:43.388][000000006.794] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:43.399][000000006.828] I/user.temp_hum_sensor 读取成功,温度为 16.90000 ℃,湿度为 82.70000 %RH
+[2025-12-09 22:11:45.096][000000008.828] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:45.128][000000008.858] I/user.temp_hum_sensor 读取成功,温度为 17.00000 ℃,湿度为 82.30000 %RH
+[2025-12-09 22:11:47.116][000000010.858] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:47.148][000000010.890] I/user.temp_hum_sensor 读取成功,温度为 16.80000 ℃,湿度为 83.00000 %RH
+[2025-12-09 22:11:49.156][000000012.890] I/user.temp_hum_sensor 开始读取温湿度传感器数据
+[2025-12-09 22:11:49.188][000000012.920] I/user.temp_hum_sensor 读取成功,温度为 16.70000 ℃,湿度为 83.40000 %RH
+```

+ 129 - 0
module/Air8000/demo/modbus/rtu_master/temp_hum_sensor.lua

@@ -0,0 +1,129 @@
+--[[
+@module  temp_hum_sensor
+@summary 485温湿度传感器读取模块
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+本功能模块演示的内容为:
+1、将设备配置为 modbus RTU 主站模式
+2、读取485接口的温湿度传感器数据
+3、每 2 秒读取一次传感器数据并解析温度和湿度值
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、参考对应传感器手册,配置从站地址、寄存器地址等参数
+
+特别说明:
+1、本示例演示使用的是中盛科技的气体浓度变送器(RS485 版),
+    该变送器模块上电后默认输出数据,从站地址为 1,波特率为 9600
+2、温度传感器数值通过保持寄存器地址 0x001E 输出,输出数据为 16 位有符号整数(-0x7FFF ~ +0x7FFF),
+    数据范围为 -40℃ ~ +85℃,分辨率为 0.1℃
+    注:寄存器值为 235,实际温度值为 235 * 0.1 = 23.5
+3、湿度传感器数值通过保持寄存器地址 0x001F 输出,输出数据为 16 位无符号整数(0 ~ 0xFFFF),
+    数据范围为 0%RH ~ 85%RH,分辨率为 0.1%RH
+    注:寄存器值为 653,实际湿度值为 653 * 0.1 = 65.3
+
+本文件没有对外接口,直接在 main.lua 中 require "temp_hum_sensor" 就可以加载运行;
+]]
+
+local exmodbus = require("exmodbus")
+
+
+-- Air8000 开发板硬件配置
+gpio.setup(16, 1)         -- RS485 芯片供电引脚
+local rs485_dir_gpio = 17 -- RS485 方向引脚
+
+
+-- 创建 RTU 主站配置参数
+local create_config = {
+    -- 串口配置参数;
+    mode = exmodbus.RTU_MASTER,      -- 通信模式:RTU主站
+    uart_id = 1,                     -- UART 端口号:1
+    baud_rate = 9600,                -- 波特率:9600(根据传感器手册调整)
+    data_bits = 8,                   -- 数据位:8
+    stop_bits = 1,                   -- 停止位:1
+    parity_bits = uart.None,         -- 校验位:无
+    byte_order = uart.LSB,           -- 字节顺序:LSB(低位优先)
+    rs485_dir_gpio = rs485_dir_gpio, -- RS485 方向引脚:17
+    rs485_dir_rx_level = 0,          -- RS485 接收方向电平:0
+}
+
+
+-- 初始化传感器数据结构
+-- 用于记录传感器的温度和湿度值
+local sensor_data = {
+    temperature = 0, -- 温度值
+    humidity = 0     -- 湿度值
+}
+
+-- 配置读取温湿度传感器的参数
+local read_config = {
+    slave_id = 1,                         -- 从站地址:1
+    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+    start_addr = 0x001E,                  -- 起始地址:0x001E(温度寄存器)
+    reg_count = 0x0002,                   -- 读取 2 个寄存器:温度和湿度
+    timeout = 1000                        -- 超时时间 1000 ms
+}
+
+
+-- 创建 RTU 主站实例
+local rtu_master = exmodbus.create(create_config)
+
+-- 判断主站是否创建成功并记录日志
+if not rtu_master then
+    log.info("temp_hum_sensor", "RTU 主站创建失败")
+else
+    log.info("temp_hum_sensor", "RTU 主站创建成功")
+end
+
+
+-- 读取温湿度传感器数据的函数
+local function read_temp_humidity()
+
+    log.info("temp_hum_sensor", "开始读取温湿度传感器数据")
+
+    -- 执行读取操作
+    local read_result = rtu_master:read(read_config)
+
+    -- 根据返回状态处理结果
+    if read_result.status == exmodbus.STATUS_SUCCESS then
+        -- 读取原始寄存器值
+        local temp_raw = read_result.data[read_config.start_addr]
+        local humi_raw = read_result.data[read_config.start_addr + 1]
+
+        -- 处理温度值的符号位
+        if temp_raw > 0x7FFF then
+            temp_raw = temp_raw - 0x10000
+        end
+
+        -- 解析温度和湿度值
+        -- 这里假设温度和湿度都是16位整数,单位分别为0.1℃和0.1%RH
+        sensor_data.temperature = temp_raw / 10.0
+        sensor_data.humidity = humi_raw / 10.0
+
+        log.info("temp_hum_sensor", "读取成功,温度为", sensor_data.temperature, "℃,湿度为", sensor_data.humidity, "%RH")
+    elseif read_result.status == exmodbus.STATUS_DATA_INVALID then
+        log.info("temp_hum_sensor", "收到传感器响应数据但数据损坏/校验失败")
+    elseif read_result.status == exmodbus.STATUS_EXCEPTION then
+        log.info("temp_hum_sensor", "收到传感器异常响应,标准异常码为", read_result.execption_code)
+    elseif read_result.status == exmodbus.STATUS_TIMEOUT then
+        log.info("temp_hum_sensor", "未收到传感器的响应(超时)")
+    end
+end
+
+
+-- 定时任务函数:每 2 秒读取一次温湿度数据
+local function task()
+    while true do
+        if rtu_master then
+            read_temp_humidity()
+        else
+            log.info("temp_hum_sensor", "RTU主站未创建,无法读取传感器数据")
+        end
+        sys.wait(2000)
+    end
+end
+
+-- 初始化任务
+sys.taskInit(task)

+ 79 - 0
module/Air8000/demo/modbus/rtu_slave/main.lua

@@ -0,0 +1,79 @@
+--[[
+@module  main
+@summary LuatOS 用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+
+本 demo 演示的核心功能为:
+1、将设备配置为 modbus RTU 从站模式
+2、等待并且应答主站请求
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、设备作为 modbus RTU 从站模式时,仅支持接收 modbus RTU 标准格式的请求报文
+3、进行回应时也需要符合 modbus RTU 标准格式
+
+更多说明参考本目录下的 readme.md 文件;
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+PROJECT = "RTU_SLAVE"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+
+-- 加载 RTU 从站应用模块(字段参数方式)
+require "rtu_slave_manage"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 128 - 0
module/Air8000/demo/modbus/rtu_slave/readme.md

@@ -0,0 +1,128 @@
+## 演示模块概述
+
+1、main.lua:主程序入口;
+
+2、rtu_slave_manage.lua:RTU 从站应用模块;
+
+## 演示功能概述
+
+本 demo 演示的核心功能为:
+
+1、将设备配置为 modbus RTU 从站模式
+
+2、等待并且应答主站请求
+
+
+
+注意事项:
+
+1、该示例程序需要搭配 exmodbus 扩展库使用
+
+2、设备作为 modbus RTU 从站模式时,仅支持接收 modbus RTU 标准格式的请求报文
+
+3、进行回应时也需要符合 modbus RTU 标准格式
+
+## 演示硬件环境
+
+1、Air8000 开发板一块
+
+2、TYPE-C USB数据线一根
+
+3、USB-RS485 串口板
+
+![](https://docs.openluat.com/cdn/image/Air8000_rs485.png)
+
+## 演示软件环境
+
+1、[Luatools下载调试工具](https://docs.openluat.com/air8000/luatos/common/download/)
+
+2、[Air8000 V2018 版本](https://docs.openluat.com/air8000/luatos/firmware/)(理论上最新版本固件也可以,如果使用最新版本的固件不可以,可以烧录 V2018-1 固件对比验证)
+
+3、[摩尔信使(MThings)官网](https://www.gulink.cn/)(用于模拟 modbus 主站设备)
+
+## 演示核心步骤
+
+1、搭建硬件环境
+
+- 将 USB-RS485 串口板与 Air8000 开发板进行连接
+- 将 USB-RS485 串口板 与 Air8000 开发板的 USB 端同时接在电脑上
+- 参考图见 演示硬件环境
+
+2、在摩尔信使上配置模拟 RTU 主站设备环境
+
+- 点击左上角的 “通道管理”按钮,在 “通道管理” 窗口选择对应的串口(USB-RS485 串口板与 Air8000 开发板进行 485 通信时的端口),点击对应串口后面的 “配置” 按钮,在 “串口参数配置” 窗口配置串口参数(要求与代码中调用 exmodbus.create 接口时填入的配置参数一致),操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/1.png)
+
+- 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/20.png)
+
+- 点击左侧的第一个主站(我这里显示为 “COM36-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增线圈 0-2,操作流程图如下,此时按照刚才操作,依次分别创建离散输入 0-2、保持寄存器 0-2、输入寄存器 0-2
+
+  ![](https://docs.openluat.com/cdn/image/MThings/21.png)
+
+- 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/22.png)
+
+4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码
+
+5、烧录成功后,自动开机运行
+
+6、开机运行后 Luatools 工具上记录的日志如下,此时便开始等待主站发送请求
+
+```
+[2025-12-08 17:23:19.802][000000000.678] I/user.main RTU_SLAVE 001.000.000
+[2025-12-08 17:23:19.806][000000000.711] Uart_ChangeBR 1338:uart1, 115200 115203 26000000 3611
+[2025-12-08 17:23:19.809][000000000.711] I/user.exmodbus 串口 1 初始化成功,波特率 115200
+[2025-12-08 17:23:19.811][000000000.712] I/user.exmodbus_test rtu_slave 创建成功, 从站 ID 为 1
+[2025-12-08 17:23:19.813][000000000.712] I/user.exmodbus 已注册从站请求处理回调函数
+[2025-12-08 17:23:19.816][000000000.712] I/user.从站回调函数已注册,开始监听主站请求...
+```
+
+7、如下图所示,在摩尔信使上鼠标右击第一个主站,然后点击 “启动轮询”,此时上位机便会模拟主站设备开始执行轮询请求操作
+
+![](https://docs.openluat.com/cdn/image/MThings/23.png)
+
+8、如下图所示,如果需要修改轮询的间隔时间或者其他参数,先将滑动条滑到右边,然后鼠标左键双击对应参数即可修改
+
+![](https://docs.openluat.com/cdn/image/MThings/24.png)
+
+9、开启轮询后 Luatools 工具与摩尔信使上的日志如下:
+
+```
+[2025-12-08 17:32:09.235][000000023.394] I/user.exmodbus_test rtu_slave 收到主站请求
+[2025-12-08 17:32:09.239][000000023.394] I/user.exmodbus_test 读取成功,返回数据:  0, 0
+[2025-12-08 17:32:12.251][000000026.402] I/user.exmodbus_test rtu_slave 收到主站请求
+[2025-12-08 17:32:12.255][000000026.402] I/user.exmodbus_test 读取成功,返回数据:  1, 1
+[2025-12-08 17:32:15.266][000000029.417] I/user.exmodbus_test rtu_slave 收到主站请求
+[2025-12-08 17:32:15.269][000000029.418] I/user.exmodbus_test 读取成功,返回数据:  201, 202
+[2025-12-08 17:32:18.270][000000032.420] I/user.exmodbus_test rtu_slave 收到主站请求
+[2025-12-08 17:32:18.273][000000032.420] I/user.exmodbus_test 读取成功,返回数据:  101, 102
+[2025-12-08 17:32:21.294][000000035.443] I/user.exmodbus_test rtu_slave 收到主站请求
+[2025-12-08 17:32:21.296][000000035.444] I/user.exmodbus_test 读取成功,返回数据:  0, 0
+[2025-12-08 17:32:24.288][000000038.446] I/user.exmodbus_test rtu_slave 收到主站请求
+[2025-12-08 17:32:24.290][000000038.447] I/user.exmodbus_test 读取成功,返回数据:  1, 1
+[2025-12-08 17:32:27.326][000000041.476] I/user.exmodbus_test rtu_slave 收到主站请求
+[2025-12-08 17:32:27.329][000000041.477] I/user.exmodbus_test 读取成功,返回数据:  201, 202
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/25.png)
+
+10、如下图所示,如果需要执行写入请求,需要先在执行可写操作的对应区块行的指令处鼠标左键双击填入要写入的数值,然后在鼠标右键双击该数值,最后点击下发写指令
+
+![](https://docs.openluat.com/cdn/image/MThings/26.png)
+
+11、执行写入请求后 Luatools 工具与摩尔信使上的日志如下:
+
+```
+[2025-12-08 17:42:53.696][000000667.848] I/user.exmodbus_test rtu_slave 收到主站请求
+[2025-12-08 17:42:53.704][000000667.848] I/user.exmodbus_test 写入成功,写入地址:  0 写入数据:  123
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/27.png)
+
+12、如下图所示,在摩尔信使上鼠标右击第一个主站,然后点击 “停止轮询”,此时上位机便不会再执行轮询请求操作
+
+![](https://docs.openluat.com/cdn/image/MThings/28.png)

+ 147 - 0
module/Air8000/demo/modbus/rtu_slave/rtu_slave_manage.lua

@@ -0,0 +1,147 @@
+--[[
+@module  rtu_slave_manage
+@summary RTU 从站应用模块
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+本功能模块演示的内容为:
+1、将设备配置为 modbus RTU 从站模式
+2、等待并且应答主站请求
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、设备作为 modbus RTU 从站模式时,仅支持接收 modbus RTU 标准格式的请求报文
+3、进行回应时也需要符合 modbus RTU 标准格式
+
+本文件没有对外接口,直接在 main.lua 中 require "rtu_slave_manage" 就可以加载运行;
+]]
+
+local exmodbus = require("exmodbus")
+
+gpio.setup(16, 1)         -- Air8000 开发板 RS485 芯片供电引脚
+local rs485_dir_gpio = 17 -- Air8000 开发板 RS485 方向引脚
+
+
+-- 创建 RTU 从站配置参数
+-- 说明:创建 RTU 从站时只需要配置如下参数即可
+local rtu_slave_config = {
+    -- 串口配置参数
+    mode = exmodbus.RTU_SLAVE,       -- 通信模式
+    uart_id = 1,                     -- UART 端口号
+    baud_rate = 115200,              -- 波特率
+    data_bits = 8,                   -- 数据位
+    stop_bits = 1,                   -- 停止位
+    parity_bits = uart.None,         -- 校验位
+    byte_order = uart.LSB,           -- 字节顺序
+    rs485_dir_gpio = rs485_dir_gpio, -- RS485 方向引脚
+    rs485_dir_rx_level = 0,          -- RS485 接收方向电平
+}
+
+
+-- 当前从站地址(ID 号)
+local SLAVE_ID = 1
+
+
+-- 寄存器映射表(按类型组织)
+local modbus_data = {
+    coils = {},            -- 线圈,可读可写,布尔值 (0/1)
+    inputs = {},           -- 输入状态,只读,布尔值 (0/1)
+    input_registers = {},  -- 输入寄存器,只读,16 位无符号整数
+    holding_registers = {} -- 保持寄存器,可读可写,16 位无符号整数
+}
+
+
+-- 初始化一些默认值,便于测试
+for i = 0, 3 do
+    modbus_data.coils[i] = 0
+    modbus_data.inputs[i] = 1
+    modbus_data.input_registers[i] = 100 + i
+    modbus_data.holding_registers[i] = 200 + i
+end
+
+
+-- 创建 RTU 从站实例
+local rtu_slave = exmodbus.create(rtu_slave_config)
+
+-- 判断从站是否创建成功
+if not rtu_slave then
+    log.info("exmodbus_test", "rtu_slave 创建失败")
+else
+    log.info("exmodbus_test", "rtu_slave 创建成功, 从站 ID 为", SLAVE_ID)
+end
+
+
+-- 定义主站请求处理回调函数
+local function callback(request)
+    log.info("exmodbus_test", "rtu_slave 收到主站请求")
+
+    -- 检查从站 ID 是否匹配
+    if request.slave_id ~= SLAVE_ID then
+        log.info("exmodbus_test", "从站 ID 不匹配,请求从站 ID 为", request.slave_id, ",当前从站 ID 为", SLAVE_ID)
+        return nil
+    end
+
+    -- 根据功能码和寄存器类型,匹配对应的数据表
+    local data_table = nil
+    local is_write = false -- 标记是否为写操作
+
+    -- 检查请求的功能码是否支持
+    if request.func_code == exmodbus.READ_COILS then -- 读线圈
+        data_table = modbus_data.coils
+    elseif request.func_code == exmodbus.READ_DISCRETE_INPUTS then -- 读离散输入
+        data_table = modbus_data.inputs
+    elseif request.func_code == exmodbus.READ_HOLDING_REGISTERS then -- 读保持寄存器
+        data_table = modbus_data.holding_registers
+    elseif request.func_code == exmodbus.READ_INPUT_REGISTERS then -- 读输入寄存器
+        data_table = modbus_data.input_registers
+    elseif request.func_code == exmodbus.WRITE_SINGLE_COIL or request.func_code == exmodbus.WRITE_MULTIPLE_COILS then -- 写单个/多个线圈
+        is_write = true
+        data_table = modbus_data.coils
+    elseif request.func_code == exmodbus.WRITE_SINGLE_HOLDING_REGISTER or request.func_code == exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS then -- 写单个/多个保持寄存器
+        is_write = true
+        data_table = modbus_data.holding_registers
+    else
+        -- 不支持的功能码
+        log.info("exmodbus_test", "不支持的功能码: ", request.func_code)
+        return exmodbus.ILLEGAL_FUNCTION
+    end
+
+    -- 检查数据地址是否有效
+    local end_addr = request.start_addr + request.reg_count - 1
+
+    -- 假设每种寄存器的最大地址是 3 (即 0 - 3)
+    if request.start_addr < 0 or end_addr > 3 then
+        log.info("exmodbus_test", "数据地址超出范围,起始地址为", request.start_addr, "结束地址为", end_addr)
+        return exmodbus.ILLEGAL_DATA_ADDRESS
+    end
+
+    -- 处理读取操作
+    if not is_write then
+        -- 构造响应数据表
+        local response = {}
+        for i = 0, request.reg_count - 1 do
+            local addr = request.start_addr + i
+            response[addr] = data_table[addr]
+        end
+        log.info("exmodbus_test", "读取成功,返回数据: ", table.concat(response, ", "))
+        return response
+    end
+
+    -- 处理写入操作
+    if is_write then
+        -- 执行写入操作
+        for i = 0, request.reg_count - 1 do
+            local addr = request.start_addr + i
+            data_table[addr] = request.data[addr]
+            log.info("exmodbus_test", "写入成功,写入地址: ", addr, "写入数据: ", request.data[addr])
+        end
+        return {} -- 返回空表表示成功
+    end
+end
+
+
+-- 注册主站请求处理回调函数
+rtu_slave:on(callback)
+
+log.info("从站回调函数已注册,开始监听主站请求...")

+ 81 - 0
module/Air8000/demo/modbus/tcp_slave/main.lua

@@ -0,0 +1,81 @@
+--[[
+@module  main
+@summary LuatOS 用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+
+本 demo 演示的核心功能为:
+1、将设备配置为 modbus TCP 从站模式
+2、等待并且应答主站请求
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、设备作为 modbus TCP 从站模式时,仅支持接收 modbus TCP 标准格式的请求报文
+3、进行回应时也需要符合 modbus TCP 标准格式
+
+更多说明参考本目录下的 readme.md 文件;
+]]
+
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+PROJECT = "TCP_SLAVE"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 开启以太网wan(默认使用静态 IP 地址)
+require "netdrv_eth_spi"
+
+-- 加载 TCP 从站应用模块(字段参数方式)
+require "tcp_slave_manage"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 81 - 0
module/Air8000/demo/modbus/tcp_slave/netdrv_eth_spi.lua

@@ -0,0 +1,81 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块,核心业务逻辑为:
+1、打开CH390H芯片供电开关;
+2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+直接使用Air8000开发板硬件测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local exnetif = require "exnetif"
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_ETH then
+        -- 在位置1和2设置自定义的DNS服务器ip地址:
+        -- "223.5.5.5",这个DNS服务器IP地址是阿里云提供的DNS服务器IP地址;
+        -- "114.114.114.114",这个DNS服务器IP地址是国内通用的DNS服务器IP地址;
+        -- 可以加上以下两行代码,在自动获取的DNS服务器工作不稳定的情况下,这两个新增的DNS服务器会使DNS服务更加稳定可靠;
+        -- 如果使用专网卡,不要使用这两行代码;
+        -- 如果使用国外的网络,不要使用这两行代码;
+        socket.setDNS(adapter, 1, "223.5.5.5")
+        socket.setDNS(adapter, 2, "114.114.114.114")
+
+        log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
+    end
+end
+
+local function ip_lose_func(adapter)
+    if adapter == socket.LWIP_ETH then
+        log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+-- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+-- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+-- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
+-- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
+-- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+--此处订阅"IP_READY"和"IP_LOSE"两种消息
+--在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
+--也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 配置SPI外接以太网芯片CH390H的单网卡,exnetif.set_priority_order使用的网卡编号为socket.LWIP_ETH
+-- 本demo使用Air8000开发板测试,开发板上的硬件配置为:
+-- GPIO140为CH390H以太网芯片的供电使能控制引脚
+-- 使用spi1,片选引脚使用GPIO12
+-- 如果使用的硬件不是Air8000开发板,根据自己的硬件配置修改以下参数
+local function netdrv_task_func()
+    exnetif.set_priority_order({
+        {
+            ETHERNET = {
+                pwrpin = 140,
+                tp = netdrv.CH390,
+                opts = { spi = 1, cs = 12 },
+                -- 此处设置为静态 IP 地址
+                -- 如果不设置为静态 IP 地址,默认会使用 DHCP 协议动态获取 IP 地址
+                static_ip = {
+                    ipv4 = "192.168.1.183",
+                    mark = "255.255.255.0",
+                    gw = "192.168.1.1"
+                }
+            }
+        }
+    })
+end
+
+sys.taskInit(netdrv_task_func)

+ 130 - 0
module/Air8000/demo/modbus/tcp_slave/readme.md

@@ -0,0 +1,130 @@
+## 演示模块概述
+
+1、main.lua:主程序入口;
+
+2、tcp_slave_manage.lua:TCP 从站应用模块;
+
+3、netdrv_eth_spi.lua:“通过SPI外挂CH390H芯片的以太网卡”驱动模块;
+
+## 演示功能概述
+
+本功能模块演示的内容为:
+
+1、将设备配置为 modbus TCP 从站模式
+
+2、等待并且应答主站请求
+
+
+
+注意事项:
+
+1、该示例程序需要搭配 exmodbus 扩展库使用
+
+2、设备作为 modbus TCP 从站模式时,仅支持接收 modbus TCP 标准格式的请求报文
+
+3、进行回应时也需要符合 modbus TCP 标准格式
+
+## 演示硬件环境
+
+1、Air8000 开发板一块
+
+2、TYPE-C USB数据线一根
+
+3、网线两根(一根开发板使用,一根电脑使用)
+
+![](https://docs.openluat.com/cdn/image/Air8000_tcp1.png)
+
+## 演示软件环境
+
+1、[Luatools下载调试工具](https://docs.openluat.com/air8000/luatos/common/download/)
+
+2、[Air8000 V2018 版本](https://docs.openluat.com/air8000/luatos/firmware/)(理论上最新版本固件也可以,如果使用最新版本的固件不可以,可以烧录 V2018-1 固件对比验证)
+
+3、[摩尔信使(MThings)官网](https://www.gulink.cn/)(用于模拟 modbus 主站设备)
+
+## 演示核心步骤
+
+1、搭建硬件环境
+
+- 将 TYPE-C USB 数据线一端接在 Air8000 开发板上,另一端接在电脑上
+- 将网线一端接在 Air8000 开发板网口上,另一端接在路由器/交换机上
+
+- 将另一根网线一端接在电脑网口上,另一端接在同一个路由器/交换机上
+
+- 参考图见 演示硬件环境
+
+2、在摩尔信使上配置模拟 TCP 主站设备环境
+
+- 点击左上角的 “通道管理” 按钮,在 “通道管理” 窗口点击 “网络通道” 按钮,点击 NET000 通道后面的 “配置” 按钮,在 “网络参数配置” 窗口配置网络参数,操作流程如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/40.png)
+
+- 点击左上角的 “添加设备”按钮,在 “添加设备” 窗口对通信设备参数进行配置,配置好后点击 “添加” 按钮,左侧栏即为添加后的效果,操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/30.png)
+
+- 点击左侧的第一个主站(我这里显示为 “NET000-001”),点击中上部分的 “新增数据” 按钮,在 “新增数据配置” 窗口将 “数据条数” 、“区块” 、“起始数据地址” 按照下图中所示进行配置,最后点击 “确定” 按钮,此时便成功新增线圈 0-2,操作流程图如下,此时按照刚才操作,依次分别创建离散输入 0-2、保持寄存器 0-2、输入寄存器 0-2
+
+  ![](https://docs.openluat.com/cdn/image/MThings/31.png)
+
+- 此时在摩尔信使上的配置操作已经完成,如果需要在摩尔信使上查看报文,那么操作流程图如下:
+
+  ![](https://docs.openluat.com/cdn/image/MThings/32.png)
+
+4、打开 Luatools 工具,根据要求烧录本次所需要的内核固件和脚本代码
+
+5、烧录成功后,自动开机运行
+
+6、此时需要等待客户端连接,连接成功后 Luatools 工具上的日志如下:
+
+```
+[2025-12-09 14:53:09.670][000000154.965] I/user.exmodbus TCP 从站已启动,监听端口: 6000
+```
+
+7、如果摩尔信使一直没有连接成功,则需要对网络通道进行重启,鼠标右击左上角 “通道” 下方的按钮,点击 “配置参数” 后会弹出 “网络参数配置” 窗口,此时直接点击确定,通道便已经重启,操作流程如下:
+
+![](https://docs.openluat.com/cdn/image/MThings/33.png)
+
+8、如下图所示,在摩尔信使上鼠标右击第一个主站,然后点击 “启动轮询”,此时上位机便会模拟主站设备开始执行轮询请求操作
+
+![](https://docs.openluat.com/cdn/image/MThings/34.png)
+
+9、如下图所示,如果需要修改轮询的间隔时间或者其他参数,先将滑动条滑到右边,然后鼠标左键双击对应参数即可修改
+
+![](https://docs.openluat.com/cdn/image/MThings/35.png)
+
+10、开启轮询后 Luatools 工具与摩尔信使上的日志如下:
+
+```
+[2025-12-09 15:06:14.069][000000631.817] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:14.071][000000631.817] I/user.exmodbus_test 读取成功,返回数据:  0, 0
+[2025-12-09 15:06:17.100][000000634.844] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:17.102][000000634.844] I/user.exmodbus_test 读取成功,返回数据:  1, 1
+[2025-12-09 15:06:20.118][000000637.858] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:20.125][000000637.859] I/user.exmodbus_test 读取成功,返回数据:  201, 202
+[2025-12-09 15:06:23.141][000000640.881] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:23.146][000000640.881] I/user.exmodbus_test 读取成功,返回数据:  101, 102
+[2025-12-09 15:06:26.169][000000643.914] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:26.171][000000643.915] I/user.exmodbus_test 读取成功,返回数据:  0, 0
+[2025-12-09 15:06:29.194][000000646.936] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:06:29.198][000000646.937] I/user.exmodbus_test 读取成功,返回数据:  1, 1
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/36.png)
+
+11、如下图所示,如果需要执行写入请求,需要先在执行可写操作的对应区块行的指令处鼠标左键双击填入要写入的数值,然后在鼠标右键双击该数值,最后点击下发写指令
+
+![](https://docs.openluat.com/cdn/image/MThings/37.png)
+
+12、执行写入请求后 Luatools 工具与摩尔信使上的日志如下:
+
+```
+[2025-12-09 15:11:48.599][000000966.338] I/user.exmodbus_test tcp_slave 收到主站请求
+[2025-12-09 15:11:48.601][000000966.339] I/user.exmodbus_test 写入成功,写入地址:  0 写入数据:  123
+```
+
+![](https://docs.openluat.com/cdn/image/MThings/38.png)
+
+13、如下图所示,在摩尔信使上鼠标右击第一个主站,然后点击 “停止轮询”,此时上位机便不会再执行轮询请求操作
+
+![](https://docs.openluat.com/cdn/image/MThings/39.png)

+ 139 - 0
module/Air8000/demo/modbus/tcp_slave/tcp_slave_manage.lua

@@ -0,0 +1,139 @@
+--[[
+@module  tcp_slave_manage
+@summary TCP 从站应用模块
+@version 1.0
+@date    2025.12.12
+@author  马梦阳
+@usage
+本功能模块演示的内容为:
+1、将设备配置为 modbus TCP 从站模式
+2、等待并且应答主站请求
+
+注意事项:
+1、该示例程序需要搭配 exmodbus 扩展库使用
+2、设备作为 modbus TCP 从站模式时,仅支持接收 modbus TCP 标准格式的请求报文
+3、进行回应时也需要符合 modbus TCP 标准格式
+
+本文件没有对外接口,直接在 main.lua 中 require "tcp_slave_manage" 就可以加载运行;
+]]
+
+local exmodbus = require("exmodbus")
+
+
+-- 创建 TCP 从站配置参数
+-- 说明:创建 TCP 从站时只需要配置如下参数即可
+local tcp_slave_config = {
+    -- 网络配置参数
+    mode = exmodbus.TCP_SLAVE, -- 通信模式:TCP 从站
+    adapter = socket.LWIP_ETH, -- 网卡 ID:LwIP 协议栈的以太网卡
+    port = 6000,               -- 本地端口号:6000(主站:服务器端口;从站:本地端口)
+}
+
+
+-- 当前从站地址(ID 号)
+local SLAVE_ID = 1
+
+
+-- 寄存器映射表(按类型组织)
+local modbus_data = {
+    coils = {},            -- 线圈,可读可写,布尔值 (0/1)
+    inputs = {},           -- 输入状态,只读,布尔值 (0/1)
+    input_registers = {},  -- 输入寄存器,只读,16 位无符号整数
+    holding_registers = {} -- 保持寄存器,可读可写,16 位无符号整数
+}
+
+
+-- 初始化一些默认值,便于测试
+for i = 0, 3 do
+    modbus_data.coils[i] = 0
+    modbus_data.inputs[i] = 1
+    modbus_data.input_registers[i] = 100 + i
+    modbus_data.holding_registers[i] = 200 + i
+end
+
+
+-- 创建 TCP 从站实例
+local tcp_slave = exmodbus.create(tcp_slave_config)
+
+-- 判断从站是否创建成功
+if not tcp_slave then
+    log.info("exmodbus_test", "tcp_slave 创建失败")
+else
+    log.info("exmodbus_test", "tcp_slave 创建成功, 从站 ID 为", SLAVE_ID)
+end
+
+
+-- 定义主站请求处理回调函数
+local function callback(request)
+    log.info("exmodbus_test", "tcp_slave 收到主站请求")
+
+    -- 检查从站 ID 是否匹配
+    if request.slave_id ~= SLAVE_ID then
+        log.info("exmodbus_test", "从站 ID 不匹配,请求从站 ID 为", request.slave_id, ",当前从站 ID 为", SLAVE_ID)
+        return nil
+    end
+
+    -- 根据功能码和寄存器类型,匹配对应的数据表
+    local data_table = nil
+    local is_write = false -- 标记是否为写操作
+
+    -- 检查请求的功能码是否支持
+    if request.func_code == exmodbus.READ_COILS then -- 读线圈
+        data_table = modbus_data.coils
+    elseif request.func_code == exmodbus.READ_DISCRETE_INPUTS then -- 读离散输入
+        data_table = modbus_data.inputs
+    elseif request.func_code == exmodbus.READ_HOLDING_REGISTERS then -- 读保持寄存器
+        data_table = modbus_data.holding_registers
+    elseif request.func_code == exmodbus.READ_INPUT_REGISTERS then -- 读输入寄存器
+        data_table = modbus_data.input_registers
+    elseif request.func_code == exmodbus.WRITE_SINGLE_COIL or request.func_code == exmodbus.WRITE_MULTIPLE_COILS then -- 写单个/多个线圈
+        is_write = true
+        data_table = modbus_data.coils
+    elseif request.func_code == exmodbus.WRITE_SINGLE_HOLDING_REGISTER or request.func_code == exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS then -- 写单个/多个保持寄存器
+        is_write = true
+        data_table = modbus_data.holding_registers
+    else
+        -- 不支持的功能码
+        log.info("exmodbus_test", "不支持的功能码: ", request.func_code)
+        return exmodbus.ILLEGAL_FUNCTION
+    end
+
+    -- 检查数据地址是否有效
+    local end_addr = request.start_addr + request.reg_count - 1
+
+    -- 假设每种寄存器的最大地址是 3 (即 0 - 3)
+    if request.start_addr < 0 or end_addr > 3 then
+        log.info("exmodbus_test", "数据地址超出范围,起始地址为", request.start_addr, "结束地址为", end_addr)
+        return exmodbus.ILLEGAL_DATA_ADDRESS
+    end
+
+    -- 处理读取操作
+    if not is_write then
+        -- 构造响应数据表
+        local response = {}
+        for i = 0, request.reg_count - 1 do
+            local addr = request.start_addr + i
+            response[addr] = data_table[addr]
+        end
+        log.info("exmodbus_test", "读取成功,返回数据: ", table.concat(response, ", "))
+        return response
+    end
+
+    -- 处理写入操作
+    if is_write then
+        -- 执行写入操作
+        for i = 0, request.reg_count - 1 do
+            local addr = request.start_addr + i
+            data_table[addr] = request.data[addr]
+            log.info("exmodbus_test", "写入成功,写入地址: ", addr, "写入数据: ", request.data[addr])
+        end
+        return {} -- 返回空表表示成功
+    end
+end
+
+
+-- 注册主站请求处理回调函数
+tcp_slave:on(callback)
+
+
+log.info("从站回调函数已注册,开始监听主站请求...")

+ 56 - 105
module/Air8000/demo/onewire/main.lua

@@ -1,114 +1,65 @@
+--[[
+@module  main
+@summary OneWire综合演示项目主文件(单传感器 + 多传感器)
+@version 001.000.000
+@date    2025.11.25
+@author  王棚嶙
+@usage
+本演示项目整合单DS18B20和多DS18B20传感器功能:
+1. 单传感器模式:GPIO2默认OneWire功能、硬件通道0模式、CRC校验、3秒间隔连续监测
+2. 多传感器模式:引脚30/98切换、PWR_KEY按键控制、电源管理、2秒间隔双路监测
+3. 完整的OneWire API接口演示、错误处理、设备检测、温度报警
+]]
 
--- LuaTools需要PROJECT和VERSION这两个信息
-PROJECT = "onewiredemo"
-VERSION = "1.0.0"
+-- 项目信息
+PROJECT = "onewire_demo"
+VERSION = "001.000.000"
 
--- sys库是标配
-sys = require("sys")
 
--- 以 log.info("ABC", "DEF", 123) 为例, 假设该代码位于main.lua的12行
--- 调试风格1, 添加额外的调试信息
--- I/main.lua:12 ABC DEF 123
-log.style(1)
 
---[[
-接线说明:
-   DS18B20    Air8000
-1. GND    -> GND
-2. VDD    -> 3.3V
-3. DATA    -> GPIO2
-
-注意:
-1. ONEWIRE功能支持在4个引脚使用, 但硬件通道只有一个, 默认是GPIO2
-2. 如需切换到其他脚, 参考Air780EPM目录下的onewire_multi_18b20_swich_read
-]]
 
-local function read_ds18b20(id)
-    local tbuff = zbuff.create(10)
-    local succ,crc8c,range,t
-    local rbuff = zbuff.create(9)
-    --如果有多个DS18B20,需要带上ID
-    tbuff:write(0x55)
-    tbuff:copy(nil, id)
-    tbuff:write(0xb8)
-    --如果只有1个DS18B20,就用无ID方式
-    --tbuff:write(0xcc,0xb8)
-    while true do
-        tbuff[tbuff:used() - 1] = 0x44
-        succ = onewire.tx(0, tbuff, false, true, true)
-        if not succ then
-            return
-        end
-        while true do
-            succ = onewire.reset(0, true)
-            if not succ then
-                return
-            end
-            if onewire.bit(0) > 0 then
-                log.info("温度转换完成")
-                break
-            end
-            sys.wait(10)
-        end
-        tbuff[tbuff:used() - 1] = 0xbe
-        succ = onewire.tx(0, tbuff, false, true, true)
-        if not succ then
-            return
-        end
-        succ,rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
-        crc8c = crypto.crc8(rbuff:toStr(0,8), 0x31, 0, true)
-        if crc8c == rbuff[8] then
-            range = (rbuff[4] >> 5) & 0x03
-            -- rbuff[0] = 0xF8
-            -- rbuff[1] = 0xFF
-            t = rbuff:query(0,2,false,true)
-            t = t * (5000 >> range)
-            t = t / 10000
-            log.info(t)
-        else
-            log.info("RAM DATA CRC校验不对",  mcu.x32(crc8c), mcu.x32(rbuff[8]))
-            return
-        end
-        sys.wait(500)
-    end
-end
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
 
-local function test_ds18b20()
-    local succ,rx_data
-    local id = zbuff.create(8)
-
-    local crc8c
-    onewire.init(0)
-    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
-    while true do
-        id:set() --清空id
-        succ,rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
-        if succ then
-            if id[0] == 0x28 then
-                crc8c = crypto.crc8(id:query(0,7), 0x31, 0, true)
-                if crc8c == id[7] then
-                    log.info("探测到DS18B20", id:query(0, 7):toHex())
-                    read_ds18b20(id)
-                    log.info("DS18B20离线,重新探测")
-                else
-                    log.info("ROM ID CRC校验不对",  mcu.x32(crc8c), mcu.x32(id[7]))
-                end
-            else
-                log.info("ROM ID不正确", mcu.x32(id[0]))
-            end
-        end
-        log.info("没有检测到DS18B20, 5秒后重试")
-        sys.wait(5000)
-    end
-end
 
-if onewire then
-    sys.taskInit(test_ds18b20)
-else
-    log.info("no onewire")
+--添加硬狗防止程序卡死
+if wdt then
+    wdt.init(9000)--初始化watchdog设置为9s
+    sys.timerLoopStart(wdt.feed, 3000)--3s喂一次狗
 end
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 在加载以下两个功能的时候建议分别打开,避免同时初始化OneWire总线,导致资源冲突
+-- 单设备模式:使用GPIO2默认OneWire功能
+-- 双设备模式:GPIO2默认 + 引脚54复用
+
+-- 加载单传感器应用模块
+-- require("onewire_single_app")
+
+-- 加载多传感器应用模块
+require("onewire_multi_app")
+
 
--- 用户代码已结束---------------------------------------------
--- 结尾总是这一句
-sys.run()
--- sys.run()之后后面不要加任何语句!!!!!
+-- 启动系统主循环
+sys.run()

+ 367 - 0
module/Air8000/demo/onewire/onewire_multi_app.lua

@@ -0,0 +1,367 @@
+--[[
+@module  onewire_multi_app
+@summary OneWire多DS18B20温度传感器应用演示模块(30和98切换版本)
+@version 1.0.0
+@date    2025.11.25
+@author  王棚嶙
+@usage
+本模块演示多DS18B20温度传感器的完整功能:
+1. 双传感器切换控制(引脚30和98)
+2. 电源管理(GPIO控制)
+3. 按键切换传感器
+4. 双路温度同时监测
+5. 使用引脚复用功能(pins.setup)
+]]
+
+log.info("onewire_multi_app", "多传感器模块版本: 1.0.0")
+
+-- 设置所有GPIO引脚电压为3.3V,确保DS18B20传感器正常供电
+pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
+
+-- GPIO20控制传感器电源使能,确保DS18B20供电正常
+gpio.setup(20, 1)
+
+-- 硬件配置(双设备模式:支持引脚30和98切换)
+local onewire_pin = 98
+local switchover_pin = gpio.PWR_KEY
+
+-- DS18B20命令定义
+local CMD_CONVERT_T = 0x44
+local CMD_READ_SCRATCHPAD = 0xBE
+local CMD_READ_ROM = 0x33
+
+-- 全局状态变量
+local pwr_key_pressed = false
+
+-- PWR_KEY按键中断处理函数
+-- 功能:处理引脚切换按键事件,设置标志位供主循环查询
+local function handle_pwr_key_interrupt()
+    pwr_key_pressed = true
+    log.info("onewire_multi_app", "切换按键被按下")
+end
+
+-- 初始化硬件配置
+local function init_hardware()
+    log.info("onewire_multi_app", "初始化硬件配置...")
+    
+    -- 配置PWR_KEY按键,使用上升沿触发并添加防抖
+    gpio.debounce(switchover_pin, 100)
+    gpio.setup(switchover_pin, handle_pwr_key_interrupt, gpio.PULLUP, gpio.RISING)
+    
+    -- 初始配置当前引脚为ONEWIRE功能
+    pins.setup(onewire_pin, "ONEWIRE")
+    
+    log.info("onewire_multi_app", "硬件初始化完成")
+    log.info("onewire_multi_app", "初始引脚: 引脚" .. onewire_pin .. " (ONEWIRE功能)")
+    log.info("onewire_multi_app", "切换按键: PWR_KEY")
+    log.info("onewire_multi_app", "支持引脚: 98 和 30 循环切换")
+    log.info("onewire_multi_app", "电源控制: GPIO20(已设置为高电平)")
+    
+    return true
+end
+
+
+-- 时序要求:DS18B20上电后需要稳定时间,100ms延时确保电源稳定
+-- 技术背景:DS18B20在电源切换后需要tREC(恢复时间)完成内部初始化
+-- 实际测试:无延时可能导致设备检测失败或温度读取异常
+-- 建议值:最小50ms,推荐100ms以确保可靠性
+local function power_stabilization_delay()
+    log.info("onewire_multi_app", "电源稳定延时(确保DS18B20内部电路就绪)")
+    sys.wait(100)  -- DS18B20 tREC恢复时间,最小50ms,推荐100ms
+end
+
+-- 单总线分时使用引脚切换(同一条总线,分时复用)
+-- 核心逻辑:使用GPIO98和GPIO30两个引脚连接同一条OneWire总线,实现分时复用
+-- 应用场景:当需要在同一总线上分时访问不同设备时使用
+-- 技术原理:通过切换总线连接引脚,实现同一条物理总线的分时使用
+-- 切换效果:
+-- - GPIO98:当前时间段连接设备A(ROM ID: 28-9F-C4-93-00-00-00-14)
+-- - GPIO30:切换到时间段连接设备B(ROM ID: 28-59-F2-53-00-00-00-14)
+-- 注意:这不是多总线并行,而是单总线的分时复用策略
+local function switch_onewire_pin()
+    log.info("onewire_multi_app", "切换OneWire引脚...")
+    
+    -- 关闭当前OneWire总线
+    onewire.deinit(0)
+    
+    
+    -- 分时复用切换逻辑
+    -- 技术原理:将当前不使用的引脚配置为GPIO功能并输出高电平
+    -- 目的:确保非活动设备处于高阻态,避免干扰当前连接的设备
+    -- 电气特性:GPIO设置为开漏输出模式,高电平由上拉电阻提供
+    if onewire_pin == 98 then
+        -- 从98切换到30
+        -- 将PIN98配置为GPIO3功能,不再作为OneWire使用
+        log.info("onewire_multi_app", "将PIN98配置为GPIO3", pins.setup(98, "GPIO3"))
+        -- 设置GPIO3为高电平输出(开漏模式,高电平由上拉电阻提供)
+        log.info("onewire_multi_app", "将GPIO3设置为高电平输出(OneWire总线空闲状态)", gpio.setup(3, 1))
+        onewire_pin = 30
+        log.info("onewire_multi_app", "切换到引脚30")
+    else
+        -- 从30切换到98
+        -- 将PIN30配置为GPIO2功能,不再作为OneWire使用
+        log.info("onewire_multi_app", "将PIN30配置为GPIO2", pins.setup(30, "GPIO2"))
+        -- 设置GPIO2为高电平输出(开漏模式,高电平由上拉电阻提供)
+        log.info("onewire_multi_app", "将GPIO2设置为高电平输出(OneWire总线空闲状态)", gpio.setup(2, 1))
+        onewire_pin = 98
+        log.info("onewire_multi_app", "切换到引脚98")
+    end
+    
+    log.info("onewire_multi_app", "当前使用引脚:", onewire_pin)
+    
+    -- 配置新引脚为ONEWIRE功能
+    -- 分时复用原理:将选中的引脚配置为OneWire功能,连接到对应设备
+    -- 连接过程:先断开之前的设备连接,再连接新的设备
+    -- 电气特性:确保当前连接的设备具有完整的OneWire通信能力
+    log.info("onewire_multi_app", "将引脚" .. onewire_pin .. "配置为ONEWIRE功能", pins.setup(onewire_pin, "ONEWIRE"))
+     
+    log.info("onewire_multi_app", "引脚切换完成,当前使用: 引脚" .. onewire_pin)
+end
+
+-- 初始化OneWire总线
+local function init_onewire_bus()
+    log.info("onewire_multi_app", "初始化OneWire总线,通道: 0")
+    
+    -- 配置当前引脚
+    pins.setup(onewire_pin, "ONEWIRE")
+    
+    -- 初始化OneWire总线
+    onewire.init(0)
+    
+    -- 配置DS18B20标准时序参数
+    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
+    
+    log.info("onewire_multi_app", "OneWire总线初始化完成,通道: 0,引脚:" .. onewire_pin)
+    
+    return true
+end
+
+-- 检测DS18B20设备是否存在(分时复用场景)
+-- 分时逻辑:在当前连接的引脚上发送复位脉冲,检测该设备响应
+-- 单总线场景:只有当前连接的引脚上的设备会响应复位脉冲
+-- 返回值:true表示当前引脚连接的设备响应,false表示无设备响应
+local function detect_ds18b20_device()
+    log.info("onewire_multi_app", "检测DS18B20设备,引脚: " .. onewire_pin)
+    
+    -- 发送复位脉冲并检测设备
+    local present = onewire.reset(0, true)
+    
+    if present then
+        log.info("onewire_multi_app", "检测到DS18B20设备响应")
+        return true
+    else
+        log.warn("onewire_multi_app", "未检测到DS18B20设备响应")
+        return false
+    end
+end
+
+-- 读取DS18B20温度(单总线分时复用)
+-- 核心流程:读ROM ID → 选设备 → 温度转换 → 读数据 → CRC校验
+local function read_ds18b20_temperature()
+    log.info("onewire_multi_app", "开始读取DS18B20温度,引脚: " .. onewire_pin)
+    
+    local tbuff = zbuff.create(10)
+    local succ, crc8c, range, t
+    local rbuff = zbuff.create(9)
+    
+    -- 读取设备ROM ID(每个设备唯一)
+    log.info("onewire_multi_app", "读取设备ROM ID(64位唯一标识)")
+    
+    local id = zbuff.create(8)
+    id:set()
+    
+    succ, rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "读取ROM ID失败")
+        return nil
+    end
+    
+    -- 检查设备类型码(DS18B20应为0x28)
+    if id[0] ~= 0x28 then
+        log.warn("onewire_multi_app", "非DS18B20设备,类型码:", mcu.x32(id[0]))
+        return nil
+    end
+    
+    -- CRC校验设备ID
+    crc8c = crypto.crc8(id:query(0, 7), 0x31, 0, true)
+    if crc8c ~= id[7] then
+        log.warn("onewire_multi_app", "ROM ID CRC校验不对", 
+                "计算值:", mcu.x32(crc8c), "期望值:", mcu.x32(id[7]))
+        log.info("onewire_multi_app", "完整ROM ID:", id:query(0, 7):toHex())
+        return nil
+    end
+    
+    log.info("onewire_multi_app", "ROM ID校验成功:", id:query(0, 7):toHex())
+    
+    -- 通过MATCH ROM选择设备(确保只选中目标设备)
+    log.info("onewire_multi_app", "开始温度转换(通过ROM匹配选择设备)")
+    
+    -- 构建命令缓冲区:MATCH ROM(0x55) + 目标设备ROM ID + 温度转换命令(0x44)
+    -- 0x55是MATCH ROM命令,后面必须跟64位目标设备的ROM ID
+    tbuff:write(0x55)     -- MATCH ROM命令
+    tbuff:copy(nil, id)  -- 复制64位ROM ID(确保选择正确的设备)
+    tbuff:write(0xb8)
+    tbuff[tbuff:used() - 1] = 0x44  -- CONVERT T温度转换命令
+    
+    succ = onewire.tx(0, tbuff, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "发送温度转换命令失败")
+        return nil
+    end
+
+    -- 第三步:等待转换完成
+    log.info("onewire_multi_app", "等待温度转换完成")
+    
+    -- 等待一段时间让转换完成
+    sys.wait(750)
+    
+    -- 发送复位脉冲检查设备
+    succ = onewire.reset(0, true)
+    if not succ then
+        log.warn("onewire_multi_app", "等待转换完成时设备未响应")
+        return nil
+    end
+    
+    -- 检查转换是否完成
+    if onewire.bit(0) > 0 then
+        log.info("onewire_multi_app", "温度转换完成")
+    end
+    
+    -- 第四步:读取温度数据
+    log.info("onewire_multi_app", "读取温度数据")
+    
+    -- 构建读取命令:匹配ROM(0x55) + ROM ID + 读取暂存器命令(0xBE)
+    tbuff[tbuff:used() - 1] = 0xbe
+    succ = onewire.tx(0, tbuff, false, true, true)
+    if not succ then
+        log.warn("onewire_multi_app", "发送读取命令失败")
+        return nil
+    end
+    
+    -- 接收9字节温度数据
+    succ, rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
+    if not succ then
+        log.warn("onewire_multi_app", "温度数据接收失败")
+        return nil
+    end
+    
+    -- 第五步:CRC校验和温度计算
+    log.info("onewire_multi_app", "CRC校验和温度计算")
+    
+    -- CRC校验
+    crc8c = crypto.crc8(rbuff:toStr(0, 8), 0x31, 0, true)
+    if crc8c == rbuff[8] then
+        -- 计算温度值
+        range = (rbuff[4] >> 5) & 0x03
+        t = rbuff:query(0, 2, false, true)
+        t = t * (5000 >> range)
+        t = t / 10000
+        
+        -- 范围检查
+        if t >= -55.0 and t <= 125.0 then
+            log.info("onewire_multi_app", "温度读取成功:", string.format("%.2f°C", t))
+            return t
+        else
+            log.warn("onewire_multi_app", "温度值超出有效范围:", t)
+            return nil
+        end
+    else
+        log.warn("onewire_multi_app", "温度数据CRC校验不对", 
+                "计算值:", mcu.x32(crc8c), "期望值:", mcu.x32(rbuff[8]))
+        return nil
+    end
+end
+
+-- 简化版温度读取(用于快速测试)
+local function quick_read_ds18b20()
+    log.info("onewire_multi_app", "快速读取温度,引脚: " .. onewire_pin)
+    
+    -- 首先检测设备是否存在
+    if not detect_ds18b20_device() then
+        return nil
+    end
+    
+    -- 使用完整读取函数
+    return read_ds18b20_temperature()
+end
+
+-- 单总线分时复用主函数(同一条总线,分时访问不同设备)
+local function multi_sensor_app_main()
+    log.info("onewire_multi_app", "启动双传感器应用(引脚30和98)")
+    
+    -- 初始化硬件
+    if not init_hardware() then
+        log.error("onewire_multi_app", "硬件初始化失败,任务无法启动")
+        return
+    end
+    
+    -- 初始化OneWire总线(GPIO20已在init_hardware中设置为高电平供电)
+    init_onewire_bus()
+    
+    -- 电源稳定延时:确保DS18B20内部电路就绪
+    power_stabilization_delay()
+    
+    -- 检测设备
+    local device_present = detect_ds18b20_device()
+    
+    if not device_present then
+        log.error("onewire_multi_app", "未检测到设备响应")
+        log.warn("onewire_multi_app", "硬件连接提示:")
+        log.warn("onewire_multi_app", "1. 传感器连接引脚98或30")
+        log.warn("onewire_multi_app", "2. 确保GPIO31/GPIO2已设置为高电平供电")
+        log.warn("onewire_multi_app", "3. 确保4.7kΩ上拉电阻正确安装")
+        log.warn("onewire_multi_app", "4. 检查传感器VDD、GND、DQ连接")
+        -- 关闭OneWire总线
+        onewire.deinit(0)
+        return
+    end
+    
+    log.info("onewire_multi_app", "开始双传感器连续监测...")
+    log.info("onewire_multi_app", "按PWR_KEY按键可切换引脚(30和98)")
+    
+    -- 主循环:按键切换设备,分时读取温度
+    local read_count = 0
+    local success_count = 0
+    
+    while true do
+        read_count = read_count + 1
+        
+        -- 检查按键状态
+        if pwr_key_pressed then
+            pwr_key_pressed = false
+            switch_onewire_pin()
+            
+            -- 重新初始化OneWire总线
+            init_onewire_bus()
+        end
+        
+        log.info("onewire_multi_app", "第" .. read_count .. "次读取,引脚:" .. onewire_pin)
+        
+        -- 尝试读取温度
+        local temperature = read_ds18b20_temperature()
+        
+        if temperature then
+            success_count = success_count + 1
+            log.info("onewire_multi_app", "引脚" .. onewire_pin .. "温度:", 
+                    string.format("%.2f°C", temperature), 
+                    "成功率:", string.format("%.1f%%", success_count/read_count*100))
+            
+            -- 简单的温度报警逻辑
+            if temperature > 30 then
+                log.warn("onewire_multi_app", "温度偏高:", string.format("%.2f°C", temperature))
+            elseif temperature < 10 then
+                log.warn("onewire_multi_app", "温度偏低:", string.format("%.2f°C", temperature))
+            end
+        else
+            log.warn("onewire_multi_app", "本次读取失败")
+            log.info("onewire_multi_app", "成功率:", string.format("%.1f%%", success_count/read_count*100))
+        end
+        
+        -- 等待下一次读取
+        sys.wait(2000)
+    end
+end
+log.info("onewire_multi_app", "双传感器应用模块加载完成(30和98切换)")
+-- 启动多传感器应用任务
+sys.taskInit(multi_sensor_app_main)
+

+ 203 - 0
module/Air8000/demo/onewire/onewire_single_app.lua

@@ -0,0 +1,203 @@
+--[[
+@module  onewire_single_app
+@summary OneWire单DS18B20温度传感器应用演示模块(GPIO2默认模式)
+@version 1.0.0
+@date    2025.11.25
+@author  王棚嶙
+@usage
+本模块演示单DS18B20温度传感器的完整功能:
+1. 使用GPIO2默认OneWire功能
+2. 硬件通道0模式,无需引脚复用
+3. 优化的时序参数和错误处理
+4. 连续温度监测
+5. 完整的OneWire API接口演示
+]]
+
+
+log.info("onewire_single_app", "单传感器模块版本:1.0.0")
+
+-- 设置所有GPIO引脚电压为3.3V,确保DS18B20传感器正常供电
+pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
+
+-- DS18B20命令定义
+local CMD_CONVERT_T = 0x44
+local CMD_READ_SCRATCHPAD = 0xBE
+local CMD_SKIP_ROM = 0xCC
+local CMD_READ_ROM = 0x33
+
+-- 单传感器应用主函数
+local function single_sensor_app_main()
+    log.info("onewire_single_app", "启动单传感器应用")
+    
+    -- 初始化OneWire总线(使用硬件通道0模式)
+    log.info("onewire_single_app", "初始化OneWire总线...")
+    onewire.init(0)
+    onewire.timing(0, false, 0, 500, 500, 15, 240, 70, 1, 15, 10, 2)
+    log.info("onewire_single_app", "OneWire总线初始化完成,使用GPIO2默认引脚")
+    
+ 
+    
+    -- 检测DS18B20设备
+    log.info("onewire_single_app", "检测DS18B20设备...")
+    
+    local succ, rx_data
+    local id = zbuff.create(8)
+    local crc8c
+    
+    -- 清空ID缓冲区
+    id:set()
+    
+    -- 读取设备ROM ID(使用手动配置的引脚)
+    succ, rx_data = onewire.rx(0, 8, 0x33, id, false, true, true)
+    
+    local detected = false
+    local device_id = nil
+    
+    if succ then
+        -- 检查家族码(DS18B20为0x28)
+        if id[0] == 0x28 then
+            -- CRC校验
+            crc8c = crypto.crc8(id:query(0,7), 0x31, 0, true)
+            if crc8c == id[7] then
+                log.info("onewire_single_app", "探测到DS18B20", id:query(0, 7):toHex())
+                detected = true
+                device_id = id
+            else
+                log.warn("onewire_single_app", "ROM ID CRC校验不对", mcu.x32(crc8c), mcu.x32(id[7]))
+            end
+        else
+            log.warn("onewire_single_app", "ROM ID不正确", mcu.x32(id[0]))
+        end
+    else
+        log.warn("onewire_single_app", "未检测到DS18B20设备,请检查硬件连接")
+        log.info("onewire_single_app", "硬件连接提示:")
+        log.info("onewire_single_app", "1. DS18B20 DATA引脚 -> GPIO2 (默认OneWire功能)")
+        log.info("onewire_single_app", "2. 确保上拉电阻4.7kΩ连接DATA到3.3V")
+        log.info("onewire_single_app", "3. 使用硬件通道0模式,无需引脚复用配置")
+    end
+    
+    if not detected then
+        log.warn("onewire_single_app", "设备检测失败,任务无法启动")
+        log.info("onewire_single_app", "单传感器应用启动完成")
+        onewire.deinit(0)
+        return
+    end
+    
+    log.info("onewire_single_app", "开始连续温度监测...")
+    
+    -- 读取DS18B20温度数据(单总线单设备模式)
+    -- 与多传感器模式的对比:
+    -- - 单传感器:使用SKIP ROM(0xCC)直接通信,无需ROM ID
+    -- - 多传感器:使用MATCH ROM(0x55)选择设备,需要目标ROM ID
+    -- 
+    -- 单设备读取流程:
+    -- 1. SKIP ROM:发送0xCC命令,跳过ROM ID识别
+    -- 2. 温度转换:发送CONVERT T(0x44)启动温度转换
+    -- 3. 读取数据:发送READ SCRATCHPAD(0xBE)读取温度数据
+    -- 4. CRC校验:验证数据完整性
+    -- 
+    -- 优势:通信简单高效,无需设备寻址
+    -- 限制:只能用于总线上只有一个设备的场景
+    local function read_temperature(dev_id)
+        local tbuff = zbuff.create(10)
+        local rbuff = zbuff.create(9)
+        local succ, crc8c, range, t
+        
+        -- 发送SKIP ROM命令(0xCC) - 跳过ROM识别,直接与设备通信
+        -- 工作原理:所有设备都会响应SKIP ROM命令,无需发送64位ROM ID
+        -- 适用场景:总线上只有一个设备,无需设备寻址和选择
+        -- 优势:通信效率高,无需传输ROM ID,简化通信流程
+        -- 风险:如果总线上有多个设备,所有设备会同时响应,造成冲突
+        tbuff:write(0xcc)
+        
+        -- 发送温度转换命令
+        tbuff[tbuff:used() - 1] = 0x44
+        succ = onewire.tx(0, tbuff, false, true, true)
+        if not succ then
+            log.warn("onewire_single_app", "发送温度转换命令失败")
+            return nil
+        end
+        
+        -- 等待转换完成(使用位检测)
+        local conversion_complete = false
+        local max_wait = 100
+        local wait_count = 0
+        
+        while wait_count < max_wait do
+            succ = onewire.reset(0, true)
+            if not succ then
+                log.warn("onewire_single_app", "等待转换完成时设备未响应")
+                return nil
+            end
+            if onewire.bit(0) > 0 then
+                log.info("onewire_single_app", "温度转换完成")
+                conversion_complete = true
+                break
+            end
+            sys.wait(10)
+            wait_count = wait_count + 1
+        end
+        
+        if not conversion_complete then
+            log.warn("onewire_single_app", "温度转换超时")
+            return nil
+        end
+        
+        -- 读取温度数据
+        tbuff[tbuff:used() - 1] = 0xBE
+        succ = onewire.tx(0, tbuff, false, true, true)
+        if not succ then
+            log.warn("onewire_single_app", "发送读取命令失败")
+            return nil
+        end
+        
+        succ, rx_data = onewire.rx(0, 9, nil, rbuff, false, false, false)
+        if not succ or rbuff:used() ~= 9 then
+            log.warn("onewire_single_app", "温度数据读取失败")
+            return nil
+        end
+        
+        -- CRC校验
+        crc8c = crypto.crc8(rbuff:toStr(0,8), 0x31, 0, true)
+        if crc8c == rbuff[8] then
+            range = (rbuff[4] >> 5) & 0x03
+            t = rbuff:query(0,2,false,true)
+            t = t * (5000 >> range)
+            t = t / 10000
+            log.info("onewire_single_app", "温度读取成功:", string.format("%.2f°C", t))
+            return t
+        else
+            log.warn("onewire_single_app", "RAM DATA CRC校验不对", mcu.x32(crc8c), mcu.x32(rbuff[8]))
+            return nil
+        end
+    end
+    
+    -- 主循环 - 连续温度监测
+    while true do
+        local temperature = read_temperature(device_id)
+        
+        if temperature then
+            -- 简单的温度报警逻辑(示例)
+            if temperature > 30 then
+                log.warn("onewire_single_app", "温度偏高:", string.format("%.2f°C", temperature))
+            elseif temperature < 10 then
+                log.warn("onewire_single_app", "温度偏低:", string.format("%.2f°C", temperature))
+            else
+                log.info("onewire_single_app", "温度正常:", string.format("%.2f°C", temperature))
+            end
+        else
+            log.warn("onewire_single_app", "本次读取失败,继续下一次")
+        end
+        
+        -- 等待下一次读取
+        sys.wait(3000)
+    end
+    
+    log.info("onewire_single_app", "单传感器连续读取任务结束")
+    log.info("onewire_single_app", "单传感器应用启动完成")
+end
+
+log.info("onewire_single_app", "单传感器应用模块加载完成")
+
+-- 启动单传感器应用任务
+sys.taskInit(single_sensor_app_main)

+ 213 - 0
module/Air8000/demo/onewire/readme.md

@@ -0,0 +1,213 @@
+> 王棚嶙
+
+# OneWire综合演示项目
+
+## 功能模块介绍
+
+本demo演示了完整的DS18B20温度传感器OneWire单总线协议实现。项目采用模块化架构,分别实现单传感器和多传感器应用场景。
+
+1、main.lua:主程序入口 <br> 
+2、onewire_single_app.lua:演示单传感器功能模块(GPIO2默认OneWire功能,硬件通道0模式,3秒间隔连续监测)<br> 
+3、onewire_multi_app.lua:演示多传感器功能模块(引脚98/30切换,PWR_KEY按键控制,2秒间隔双路监测)<br> 
+
+## 演示功能概述
+
+###  主程序入口模块 (main.lua)
+
+- 初始化项目信息和版本号
+- 初始化看门狗,并定时喂狗
+- 启动一个循环定时器,每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
+- 加载onewire_single_app模块(通过require "onewire_single_app")
+- 加载onewire_multi_app模块(通过require "onewire_multi_app")
+
+### 单传感器模式 (onewire_single_app.lua)
+- 使用GPIO2默认OneWire功能,硬件通道0模式,无需引脚复用
+- 完整的CRC8数据校验机制,确保数据可靠性
+- 设备自动识别和ROM验证,支持设备类型检测
+- 3秒间隔连续温度监测,实时温度报警功能
+- zbuff缓冲区优化,提高数据传输效率
+
+
+### 多传感器模式 (onewire_multi_app.lua - 单总线多设备演示)
+
+
+**单总线多设备挂载原理**:
+1. **物理连接**:所有DS18B20的VDD、GND、DQ引脚分别并联到同一组单总线
+2. **设备识别**:每个DS18B20出厂时烧录了全球唯一的64位ROM ID
+3. **总线扫描**:主机发送SEARCH ROM(0xF0)命令发现总线上的所有设备
+4. **设备选择**:通过MATCH ROM(0x55)命令+目标设备ROM ID选择特定设备通信
+5. **分时操作**:每次只与一个设备通信,避免总线冲突
+
+**分时复用测试逻辑**(2秒切换一次):
+- **前2秒**:使用总线端A设备(引脚98,ROM ID: 28-9F-C4-93-00-00-00-14)
+- **按PWR_KEY后2秒**:切换使用总线端B设备(引脚30,ROM ID: 28-59-F2-53-00-00-00-14)
+- **循环切换**:按键一次切换一个设备,实现同一条总线的分时使用
+
+**核心测试流程**:
+1. 初始化当前引脚的OneWire总线
+2. 发送SEARCH ROM命令扫描总线上的设备
+3. 读取并验证设备的64位ROM ID(家族码+序列号+CRC)
+4. 使用MATCH ROM(0x55)命令选择目标设备
+5. 发送温度转换命令(0x44)并等待完成
+6. 读取温度数据并进行CRC校验
+7. 输出设备ROM ID、温度值、读取成功率
+
+
+
+## 演示硬件环境
+1、Air8000A核心板一块
+
+2、TYPE-C USB数据线一根
+
+3、ds18b20传感器两个
+
+4、Air8000A核心板和数据线的硬件接线方式为
+
+- Air8000A核心板通过TYPE-C USB口供电;(核心板USB旁边的开关拨到on一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+5、Air8000A核心板和ds18b20传感器接线方式
+
+### 单传感器连接
+
+|   Air8000A核心板     |    DS18B20传感器    |
+| --------------- | -------------------|
+|    VDD_EXT         |         VCC        |
+|    GPIO2     |         DQ         |
+|    GND          |         GND        |
+
+连接图:
+
+![image](https://docs.openluat.com/air8000/luatos/app/driver/onewire/image/b1e471848d3181dcd822dc93c62d660d.jpg)
+
+
+
+### 多传感器连接
+
+
+|   Air8000A核心板     |    DS18B20传感器1    |
+| --------------- | -------------------|
+|    VDD_EXT         |         VCC        |
+|    GPIO2     |         DQ         |
+|    任意GND          |         GND        |
+
+|   Air8000A核心板     |    DS18B20传感器2    |
+| --------------- | -------------------|
+|    GPIO20         |         VCC        |
+|    GPIO3     |         DQ         |
+|    任意GND          |         GND        |
+
+连接图:
+
+![image](https://docs.openluat.com/air8000/luatos/app/driver/onewire/image/8a1b2ebfc3749f8d5e84e40a78a31aa4.jpg)
+
+## 演示软件环境
+
+1、Luatools下载调试工具:https://docs.openluat.com/air780epm/common/Luatools/
+
+2、内核固件版本:https://docs.openluat.com/air8000/luatos/firmware/
+
+## 演示核心步骤
+1、搭建好硬件环境
+
+2、通过Luatools将demo与固件烧录到核心板或开发板中
+
+3、烧录好后,板子开机将会在Luatools上看到如下打印:
+
+```lua
+(1)单传感器演示
+[2025-11-25 15:14:59.660][000000000.387] I/user.onewire_single_app 单传感器模块版本:1.0.0
+[2025-11-25 15:14:59.683][000000000.387] I/user.onewire_single_app 单传感器应用模块加载完成
+[2025-11-25 15:14:59.705][000000000.387] I/user.onewire_single_app 启动单传感器应用
+[2025-11-25 15:14:59.721][000000000.388] I/user.onewire_single_app 初始化OneWire总线...
+[2025-11-25 15:14:59.737][000000000.388] I/user.onewire_single_app OneWire总线初始化完成,使用GPIO2默认引脚
+[2025-11-25 15:14:59.755][000000000.388] I/user.onewire_single_app 检测DS18B20设备...
+[2025-11-25 15:14:59.769][000000000.395] I/user.onewire_single_app 探测到DS18B20 2859F253000000 14
+[2025-11-25 15:14:59.793][000000000.396] I/user.onewire_single_app 开始连续温度监测...
+[2025-11-25 15:14:59.817][000000000.405] I/user.onewire_single_app 温度转换完成
+[2025-11-25 15:14:59.842][000000000.418] I/user.onewire_single_app 温度读取成功: 26.12°C
+[2025-11-25 15:14:59.858][000000000.419] I/user.onewire_single_app 温度正常: 26.12°C
+[2025-11-25 15:15:00.727][000000003.428] I/user.onewire_single_app 温度转换完成
+[2025-11-25 15:15:00.751][000000003.441] I/user.onewire_single_app 温度读取成功: 26.12°C
+[2025-11-25 15:15:00.766][000000003.442] I/user.onewire_single_app 温度正常: 26.12°C
+[2025-11-25 15:15:03.755][000000006.451] I/user.onewire_single_app 温度转换完成
+[2025-11-25 15:15:03.771][000000006.464] I/user.onewire_single_app 温度读取成功: 26.12°C
+[2025-11-25 15:15:03.788][000000006.465] I/user.onewire_single_app 温度正常: 26.12°C
+[2025-11-25 15:15:06.775][000000009.474] I/user.onewire_single_app 温度转换完成
+[2025-11-25 15:15:06.794][000000009.487] I/user.onewire_single_app 温度读取成功: 26.12°C
+[2025-11-25 15:15:06.817][000000009.488] I/user.onewire_single_app 温度正常: 26.12°C
+[2025-11-25 15:15:09.800][000000012.497] I/user.onewire_single_app 温度转换完成
+[2025-11-25 15:15:09.812][000000012.510] I/user.onewire_single_app 温度读取成功: 26.12°C
+[2025-11-25 15:15:09.832][000000012.511] I/user.onewire_single_app 温度正常: 26.12°C
+[2025-11-25 15:15:12.830][000000015.520] I/user.onewire_single_app 温度转换完成
+[2025-11-25 15:15:12.842][000000015.533] I/user.onewire_single_app 温度读取成功: 26.12°C
+[2025-11-25 15:15:12.856][000000015.534] I/user.onewire_single_app 温度正常: 26.12°C
+
+(2)单总线分时复用演示
+[2025-11-25 15:10:27.765][000000000.393] I/user.onewire_multi_app 多传感器模块版本: 1.0.0
+[2025-11-25 15:10:27.826][000000000.394] I/user.onewire_multi_app 双传感器应用模块加载完成(30和98切换)
+[2025-11-25 15:10:27.910][000000000.394] I/user.onewire_multi_app 启动双传感器应用(引脚30和98)
+[2025-11-25 15:10:27.968][000000000.394] I/user.onewire_multi_app 初始化硬件配置...
+[2025-11-25 15:10:28.023][000000000.395] I/user.onewire_multi_app 硬件初始化完成
+[2025-11-25 15:10:28.067][000000000.395] I/user.onewire_multi_app 初始引脚: 引脚98 (ONEWIRE功能)
+[2025-11-25 15:10:28.115][000000000.396] I/user.onewire_multi_app 切换按键: PWR_KEY
+[2025-11-25 15:10:28.174][000000000.396] I/user.onewire_multi_app 支持引脚: 98 和 30 循环切换
+[2025-11-25 15:10:28.235][000000000.396] I/user.onewire_multi_app 电源控制: GPIO31/GPIO2 (已设置为高电平)
+[2025-11-25 15:10:28.284][000000000.396] I/user.onewire_multi_app 电源控制: 开启
+[2025-11-25 15:10:28.312][000000000.497] I/user.onewire_multi_app 初始化OneWire总线,通道: 0
+[2025-11-25 15:10:28.350][000000000.508] I/user.onewire_multi_app OneWire总线初始化完成,通道: 0,引脚:98
+[2025-11-25 15:10:28.468][000000000.709] I/user.onewire_multi_app 检测DS18B20设备,引脚: 98
+[2025-11-25 15:10:28.502][000000000.710] I/user.onewire_multi_app 检测到DS18B20设备响应
+[2025-11-25 15:10:28.531][000000000.710] I/user.onewire_multi_app 开始双传感器连续监测...
+[2025-11-25 15:10:28.561][000000000.710] I/user.onewire_multi_app 按PWR_KEY按键可切换引脚(30和98)
+[2025-11-25 15:10:28.592][000000000.711] I/user.onewire_multi_app 第1次读取,引脚:98
+[2025-11-25 15:10:28.625][000000000.711] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 98
+[2025-11-25 15:10:28.653][000000000.711] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-25 15:10:28.685][000000000.718] I/user.onewire_multi_app ROM ID校验成功: 289FC493000000 14
+[2025-11-25 15:10:28.719][000000000.719] I/user.onewire_multi_app 开始温度转换
+[2025-11-25 15:10:28.747][000000000.726] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-25 15:10:28.777][000000001.478] I/user.onewire_multi_app 温度转换完成
+[2025-11-25 15:10:28.800][000000001.478] I/user.onewire_multi_app 读取温度数据
+[2025-11-25 15:10:28.828][000000001.491] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-25 15:10:28.860][000000001.492] I/user.onewire_multi_app 温度读取成功: 25.75°C
+[2025-11-25 15:10:28.890][000000001.492] I/user.onewire_multi_app 引脚98温度: 25.75°C 成功率: 100.0%
+[2025-11-25 15:10:29.029][000000003.493] I/user.onewire_multi_app 第2次读取,引脚:98
+[2025-11-25 15:10:29.063][000000003.493] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 98
+[2025-11-25 15:10:29.094][000000003.493] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-25 15:10:29.124][000000003.500] I/user.onewire_multi_app ROM ID校验成功: 289FC493000000 14
+[2025-11-25 15:10:29.158][000000003.501] I/user.onewire_multi_app 开始温度转换
+[2025-11-25 15:10:29.192][000000003.508] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-25 15:10:29.220][000000004.260] I/user.onewire_multi_app 温度转换完成
+[2025-11-25 15:10:29.251][000000004.260] I/user.onewire_multi_app 读取温度数据
+[2025-11-25 15:10:29.281][000000004.273] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-25 15:10:29.308][000000004.274] I/user.onewire_multi_app 温度读取成功: 25.81°C
+[2025-11-25 15:10:29.342][000000004.274] I/user.onewire_multi_app 引脚98温度: 25.81°C 成功率: 100.0%
+
+
+(3)单总线分时复用按键切换演示
+[2025-11-25 15:10:34.504][000000010.718] I/user.onewire_multi_app 切换按键被按下
+[2025-11-25 15:10:35.624][000000011.838] I/user.onewire_multi_app 切换OneWire引脚...
+[2025-11-25 15:10:35.647][000000011.859] I/user.onewire_multi_app 将PIN984配置为GPIO3 true
+[2025-11-25 15:10:35.677][000000011.859] I/user.onewire_multi_app 将GPIO3设置为高电平输出 function: 0C7F4648
+[2025-11-25 15:10:35.698][000000011.859] I/user.onewire_multi_app 切换到引脚30
+[2025-11-25 15:10:35.721][000000011.860] I/user.onewire_multi_app 当前使用引脚: 30
+[2025-11-25 15:10:35.745][000000011.860] I/user.onewire_multi_app 将引脚30配置为ONEWIRE功能 true
+[2025-11-25 15:10:35.769][000000011.880] I/user.onewire_multi_app 引脚切换完成,当前使用: 引脚30
+[2025-11-25 15:10:36.167][000000012.380] I/user.onewire_multi_app 初始化OneWire总线,通道: 0
+[2025-11-25 15:10:36.189][000000012.391] I/user.onewire_multi_app OneWire总线初始化完成,通道: 0,引脚:30
+[2025-11-25 15:10:36.275][000000012.490] I/user.onewire_multi_app 第5次读取,引脚:30
+[2025-11-25 15:10:36.296][000000012.491] I/user.onewire_multi_app 开始读取DS18B20温度,引脚: 30
+[2025-11-25 15:10:36.315][000000012.491] I/user.onewire_multi_app 读取设备ROM ID
+[2025-11-25 15:10:36.335][000000012.498] I/user.onewire_multi_app ROM ID校验成功: 2859F253000000 14
+[2025-11-25 15:10:36.361][000000012.499] I/user.onewire_multi_app 开始温度转换
+[2025-11-25 15:10:36.390][000000012.506] I/user.onewire_multi_app 等待温度转换完成
+[2025-11-25 15:10:37.038][000000013.258] I/user.onewire_multi_app 温度转换完成
+[2025-11-25 15:10:37.063][000000013.258] I/user.onewire_multi_app 读取温度数据
+[2025-11-25 15:10:37.084][000000013.271] I/user.onewire_multi_app CRC校验和温度计算
+[2025-11-25 15:10:37.107][000000013.272] I/user.onewire_multi_app 温度读取成功: 26.00°C
+[2025-11-25 15:10:37.130][000000013.272] I/user.onewire_multi_app 引脚30温度: 26.00°C 成功率: 100.0%
+
+
+
+```

+ 71 - 0
module/Air8000/project/Air8000A_CH390_1/main.lua

@@ -0,0 +1,71 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑 
+@version 1.0
+@date    2025.12.10
+@author  王城钧
+@usage
+本demo演示的核心功能为:
+1.设置多网融合功能,以太网提供网络供wifi和以太网设备上网
+更多说明参考本目录下的readme.md文件
+]]
+--[[
+必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
+PROJECT:项目名,ascii string类型
+        可以随便定义,只要不使用,就行
+VERSION:项目版本号,ascii string类型
+        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
+            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
+            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
+        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+PROJECT = "ethernet_out_ethernet_in_wifi_in"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+
+
+-- 开启多网融合功能
+require "netif_app"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 66 - 0
module/Air8000/project/Air8000A_CH390_1/netif_app.lua

@@ -0,0 +1,66 @@
+--[[
+@module  netif_app
+@summary netif_app 网络管理模块,开启多网融合功能,以太网提供网络供wifi和以太网设备上网
+@version 1.0
+@date    2025.12.10
+@author  王城钧
+@usage
+本文件为网络管理模块,核心业务逻辑为:
+1.设置多网融合功能,以太网提供网络供wifi和以太网设备上网
+2、http测试以太网网络
+本文件没有对外接口,直接在main.lua中require "netif_app"就可以加载运行;
+]]
+
+exnetif = require "exnetif"
+
+function netif_app_task_func()
+    local res
+    --设置多网融合功能,以太网提供网络供wifi设备上网
+    res = exnetif.setproxy(socket.LWIP_AP, socket.LWIP_ETH, {
+            ssid = "test2",                     -- AP热点名称(string),网卡包含wifi时填写
+            password = "HZ88888888",            -- AP热点密码(string),网卡包含wifi时填写
+            -- ap_opts = {                      -- AP模式下配置项(选填参数)
+            --     hidden = false,              -- 是否隐藏SSID, 默认false,不隐藏
+            --     max_conn = 4 },              -- 最大客户端数量, 默认4
+            -- channel = 6,                     -- AP建立的通道(选填参数), 默认6
+            -- adapter_addr = "192.168.5.1",    -- 自定义LWIP_AP网卡的ip地址(选填),需要自定义ip和网关ip时填写
+            -- adapter_gw = { 192, 168, 5, 1 }, -- 自定义LWIP_AP网卡的网关地址(选填),需要自定义ip和网关ip时填写
+            main_adapter = {                    -- 提供网络的网卡开启参数
+                ethpower_en = 16,              -- 以太网模块的pwrpin引脚(gpio编号)
+                tp = netdrv.CH390,              -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                opts = {spi = 1, cs = 18}
+            }
+        })
+    
+    if res then
+        log.info("exnetif", "setproxy success")
+    else
+        log.info("开启失败,请检查配置项是否正确,日志中是否打印了错误信息")
+    end
+
+    --设置多网融合功能,以太网提供网络供以太网设备上网
+    res = exnetif.setproxy(socket.LWIP_USER1, socket.LWIP_ETH, {
+            ethpower_en = 17,                   -- 以太网模块的pwrpin引脚(gpio编号)
+            tp = netdrv.CH390,                  -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+            opts = {spi = 1, cs = 19},           -- 外挂方式,需要额外的参数(选填参数),仅spi方式外挂以太网时需要填写。
+            main_adapter = {                    -- 提供网络的网卡开启参数
+                ethpower_en = 16,              -- 以太网模块的pwrpin引脚(gpio编号)
+                tp = netdrv.CH390,              -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                opts = {spi = 1, cs = 18}
+            }
+        })
+    if res then
+        log.info("exnetif", "setproxy success")
+    else
+        log.info("开启失败,请检查配置项是否正确,日志中是否打印了错误信息")
+    end
+
+    -- 每5秒进行HTTPS连接测试,实时监测以太网网络连接状态, 仅供测试需要,量产不需要,用来判断当前网络是否可用,需要的话可以打开注释
+    while 1 do
+        local code, headers, body = http.request("GET", "https://httpbin.air32.cn/bytes/2048", nil, nil, {adapter=socket.LWIP_ETH,timeout=5000,debug=false}).wait()
+        log.info("http执行结果", code, headers, body and #body)
+        sys.wait(5000)
+    end
+end
+
+sys.taskInit(netif_app_task_func)

+ 250 - 0
module/Air8000/project/Air8000A_CH390_1/pins_air8000.json

@@ -0,0 +1,250 @@
+{
+  "model": "Air8000",
+  "pins": [
+    [
+      1,
+      "USB_BOOT",
+      ""
+    ],
+    [
+      2,
+      "VBUS",
+      ""
+    ],
+    [
+      14,
+      "PWR_KEY",
+      ""
+    ],
+    [
+      16,
+      "GPIO19",
+      ""
+    ],
+    [
+      17,
+      "GPIO18",
+      ""
+    ],
+    [
+      18,
+      "I2S_BCLK",
+      ""
+    ],
+    [
+      19,
+      "I2S_LRCK",
+      ""
+    ],
+    [
+      20,
+      "I2S_DIN",
+      ""
+    ],
+    [
+      21,
+      "I2S_DOUT",
+      ""
+    ],
+    [
+      22,
+      "I2S_MCLK",
+      ""
+    ],
+    [
+      23,
+      "GPIO20",
+      ""
+    ],
+    [
+      24,
+      "GPIO21",
+      ""
+    ],
+    [
+      25,
+      "LCD_CLK",
+      ""
+    ],
+    [
+      26,
+      "LCD_CS",
+      ""
+    ],
+    [
+      27,
+      "LCD_RST",
+      ""
+    ],
+    [
+      28,
+      "LCD_SDA",
+      ""
+    ],
+    [
+      29,
+      "LCD_RS",
+      ""
+    ],
+    [
+      30,
+      "GPIO2",
+      ""
+    ],
+    [
+      31,
+      "GPIO1",
+      ""
+    ],
+    [
+      35,
+      "CAN_STB",
+      ""
+    ],
+    [
+      36,
+      "CAN_TXD",
+      ""
+    ],
+    [
+      37,
+      "CAN_RXD",
+      ""
+    ],
+    [
+      38,
+      "SPI1_SCLK",
+      ""
+    ],
+    [
+      39,
+      "SPI1_MISO",
+      ""
+    ],
+    [
+      40,
+      "SPI1_MOSI",
+      ""
+    ],
+    [
+      41,
+      "SPI1_CS",
+      ""
+    ],
+    [
+      43,
+      "WAKEUP6",
+      ""
+    ],
+    [
+      44,
+      "WAKEUP0",
+      ""
+    ],
+    [
+      46,
+      "DBG_TXD",
+      ""
+    ],
+    [
+      47,
+      "DBG_RXD",
+      ""
+    ],
+    [
+      48,
+      "UART11_RXD",
+      ""
+    ],
+    [
+      49,
+      "UART11_TXD",
+      ""
+    ],
+    [
+      52,
+      "GPIO153",
+      ""
+    ],
+    [
+      53,
+      "GPIO147",
+      ""
+    ],
+    [
+      54,
+      "GPIO146",
+      ""
+    ],
+    [
+      55,
+      "GPIO141",
+      ""
+    ],
+    [
+      56,
+      "GPIO140",
+      ""
+    ],
+    [
+      59,
+      "UART12_RXD",
+      ""
+    ],
+    [
+      60,
+      "UART12_TXD",
+      ""
+    ],
+    [
+      66,
+      "I2C1_SCL",
+      ""
+    ],
+    [
+      67,
+      "I2C1_SDA",
+      ""
+    ],
+    [
+      73,
+      "GPIO162",
+      ""
+    ],
+    [
+      74,
+      "GPIO164",
+      ""
+    ],
+    [
+      80,
+      "I2C0_SCL",
+      ""
+    ],
+    [
+      81,
+      "I2C0_SDA",
+      ""
+    ],
+    [
+      82,
+      "GPIO17",
+      ""
+    ],
+    [
+      83,
+      "GPIO16",
+      ""
+    ],
+    [
+      96,
+      "GPIO160",
+      ""
+    ],
+    [
+      98,
+      "GPIO3",
+      ""
+    ]
+  ]
+}

+ 58 - 0
module/Air8000/project/Air8000A_CH390_1/readme.md

@@ -0,0 +1,58 @@
+## 功能模块介绍
+
+1、main.lua:主程序入口;
+
+2、netif_app::网络管理模块,开启多网融合功能,以太网提供网络供以太网和wifi设备上网;
+
+3、pins_air8000:管脚配置文件;
+
+## 演示功能概述
+
+1、开启多网融合模式,以太网连接外部网络,生成WiFi热点为WiFi终端设备提供接入,支持以太网lan模式为其他以太网设备提供接入
+
+2、​网络监控​,每5秒进行HTTPS连接测试,实时监测以太网网络的连接状态
+
+## 演示硬件环境
+
+![](https://docs.openLuat.com/cdn/image/8000双网口板子.jpg)
+
+1、Air8000双网口开发板一块+可上网的sim卡一张+4g天线一根+wifi天线一根+网线两根:
+
+- sim卡插入开发板的sim卡槽
+
+- 天线装到开发板上
+
+- 第一根网线一端插入开发板网口,另外一端连接可以上外网的路由器网口
+
+- 第二根网线一段插入开发板网口,另一端连接需供网设备
+
+2、TYPE-C USB数据线一根 ,Air8000双网口开发板和数据线的硬件接线方式为:
+
+- Air8000双网口开发板通过TYPE-C USB口供电;(外部供电/USB供电 拨动开关 拨到 USB供电一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8000 V2018版本固件](https://docs.openluat.com/air8000/luatos/firmware/)(理论上,2025年7月26日之后发布的固件都可以)
+
+## 演示核心步骤
+
+1、搭建好硬件环境,按接线图连接硬件,
+
+2、按需修改WiFi热点配置(在netif_app.lua中):
+ssid = "AP热点名称"
+password = "AP热点密码"
+
+4、烧录内核固件和本项目的Lua脚本:main.lua:主程序入口,netif_app.lua:网络管理模块,pins_air8000:管脚配置文件
+
+5、启动设备,观察日志输出:
+
+```lua
+[INFO] exnetif setproxy success
+[INFO] http执行结果 200 ... 
+```
+
+6、其他设备通过wifi或以太网接入Air8000,其他设备都能正常上网,则表示验证成功。

+ 66 - 0
olddemo/camera/rtsp_usb/main.lua

@@ -0,0 +1,66 @@
+-- LuaTools需要PROJECT和VERSION这两个信息
+PROJECT = "usb_cam_rtsp"
+VERSION = "1.0.0"
+
+local camera_id = camera.USB
+
+local usb_camera_table = {
+    id = camera_id,
+    sensor_width = 640,
+    sensor_height = 480,
+    usb_port = 1
+}
+
+sys.taskInit(function()
+    wlan.init()
+    wlan.connect("luatos1234", "12341234", 1)
+
+    sys.waitUntil("IP_READY")
+
+    camera.config(0, camera.CONF_UVC_FPS, 15)
+    socket.sntp() -- 这个必须有
+    sys.wait(200)
+    result = camera.init(usb_camera_table)
+    log.info("摄像头初始化", result)
+    if result ~= 0 then
+        log.info("摄像头初始化失败,停止运行")
+        return
+    end
+    camera.start(camera_id)
+
+    -- local rtspc = rtsp.create("rtsp://180.152.6.34:554/stream2live/089dd7d5_04d4_467a_852a_66cbbfc5a9c3_0001")
+    local rtspc = rtsp.create("rtsp://192.168.1.10:554/stream2live/089dd7d5_04d4_467a_852a_66cbbfc5a9c3_0001")
+    rtspc:setCallback(function(state, ...)
+        if state == rtsp.STATE_CONNECTED then
+            log.info("rtsp", "已连接到推流服务器")
+        elseif state == rtsp.STATE_PUBLISHING then
+            log.info("rtsp", "已开始推流")
+        elseif state == rtsp.STATE_ERROR then
+            log.info("rtsp", "出错:", ...)
+        end
+    end)
+    log.info("开始连接到推流服务器...")
+    sys.wait(100)
+    rtspc:connect()
+    sys.wait(300)
+
+    -- 开始处理
+    log.info("rtsp", "开始推流...")
+    rtspc:start() -- 已自动调用 camera.capture(camera_id, "rtsp", 1)
+    
+    while 1 do
+        --- 打印一下内存状态
+        sys.wait(30*1000)
+        log.info("lua", rtos.meminfo())
+        log.info("sys", rtos.meminfo("sys"))
+        log.info("psram", rtos.meminfo("psram"))
+        sys.wait(2000)
+    end
+    -- #################################################
+
+end)
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 1 - 23
olddemo/iperf/air8101/main.lua

@@ -2,28 +2,6 @@
 PROJECT = "wifidemo"
 VERSION = "1.0.0"
 
--- 引入必要的库文件(lua编写), 内部库不需要require
-sys = require("sys")
-require("sysplus")
-
---[[
-本demo演示AP的配网实例
-1. 启动后, 会创建一个 luatos_ + mac地址的热点
-2. 热点密码是 12341234
-3. 热点网关是 192.168.4.1, 同时也是配网网页的ip
-4. http://192.168.4.1
-]]
-
-
-if wdt then
-    --添加硬狗防止程序卡死,在支持的设备上启用这个功能
-    wdt.init(9000)--初始化watchdog设置为9s
-    sys.timerLoopStart(wdt.feed, 3000)--3s喂一次狗
-end
-
--- 初始化LED灯, 开发板上左右2个led分别是gpio12/gpio13
-local LEDA= gpio.setup(12, 0, gpio.PULLUP)
--- local LEDB= gpio.setup(13, 0, gpio.PULLUP)
 
 local scan_result = {}
 
@@ -36,7 +14,7 @@ sys.taskInit(function()
     sys.wait(100)
     iperf.client(socket.LWIP_STA, "47.94.236.172")
 
-    sys.wait(30*1000)
+    sys.wait(90*1000)
     iperf.abort()
 end)
 

+ 386 - 0
script/libs/exmodbus.lua

@@ -0,0 +1,386 @@
+--[[
+@module exmodbus
+@summary exmodbus 控制Modbus RTU/ASCII/TCP主站/从站通信
+@version 1.0
+@date    2025.
+@author  马梦阳
+@usage
+本文件的对外接口有 5 个:
+1、exmodbus.create(config):创建 modbus 主站/从站,支持 RTU、ASCII、TCP 三种通信模式
+2、modbus:read(config):主站向从站发起读取请求(仅适用于 RTU、ASCII、TCP 主站模式)
+3、modbus:write(config):主站向从站发起写入请求(仅适用于 RTU、ASCII、TCP 主站模式)
+4、modbus:destroy():销毁 modbus 主站/从站实例对象
+5、modbus:on(callback):从站注册回调接口,用于处理主站发起的请求(仅适用于 RTU、ASCII、TCP 从站模式)
+]]
+local exmodbus = {}
+
+-- 定义通信模式常量
+exmodbus.RTU_MASTER = 0   -- RTU 主站模式
+exmodbus.RTU_SLAVE = 1    -- RTU 从站模式
+exmodbus.ASCII_MASTER = 2 -- ASCII 主站模式
+exmodbus.ASCII_SLAVE = 3  -- ASCII 从站模式
+exmodbus.TCP_MASTER = 4   -- TCP 主站模式
+exmodbus.TCP_SLAVE = 5    -- TCP 从站模式
+
+-- 定义数据类型常量
+exmodbus.COIL_STATUS = 0      -- 线圈状态
+exmodbus.INPUT_STATUS = 1     -- 离散输入状态
+exmodbus.HOLDING_REGISTER = 4 -- 保持寄存器
+exmodbus.INPUT_REGISTER = 3   -- 输入寄存器
+
+-- 定义操作类型常量
+exmodbus.READ_COILS = 0x01                       -- 读线圈状态
+exmodbus.READ_DISCRETE_INPUTS = 0x02             -- 读离散输入状态
+exmodbus.READ_HOLDING_REGISTERS = 0x03           -- 读保持寄存器
+exmodbus.READ_INPUT_REGISTERS = 0x04             -- 读输入寄存器
+exmodbus.WRITE_SINGLE_COIL = 0x05                -- 写单个线圈状态
+exmodbus.WRITE_SINGLE_HOLDING_REGISTER = 0x06    -- 写单个保持寄存器
+exmodbus.WRITE_MULTIPLE_HOLDING_REGISTERS = 0x10 -- 写多个保持寄存器
+exmodbus.WRITE_MULTIPLE_COILS = 0x0F             -- 写多个线圈状态
+
+-- 定义响应结果常量
+exmodbus.STATUS_SUCCESS = 0      -- 收到响应数据且数据有效
+exmodbus.STATUS_DATA_INVALID = 1 -- 收到响应数据但数据损坏/校验失败
+exmodbus.STATUS_EXCEPTION = 2    -- 收到标准异常响应码
+exmodbus.STATUS_TIMEOUT = 3      -- 超时未收到响应
+
+-- 异常响应码常量
+exmodbus.ILLEGAL_FUNCTION = 0x01           -- 不支持请求的功能码
+exmodbus.ILLEGAL_DATA_ADDRESS = 0x02       -- 请求的数据地址无效或超出范围
+exmodbus.ILLEGAL_DATA_VALUE = 0x03         -- 请求的数据值无效
+exmodbus.SLAVE_DEVICE_FAILURE = 0x04       -- 从站在执行操作时发生内部错误
+exmodbus.ACKNOWLEDGE = 0x05                -- 请求已接受,但需要长时间处理
+exmodbus.SLAVE_DEVICE_BUSY = 0x06          -- 从站正忙,无法处理请求
+exmodbus.NEGATIVE_ACKNOWLEDGE = 0x07       -- 无法执行编程功能
+exmodbus.MEMORY_PARITY_ERROR = 0x08        -- 内存奇偶校验错误
+exmodbus.GATEWAY_PATH_UNAVAILABLE = 0x0A   -- 网关路径不可用
+exmodbus.GATEWAY_TARGET_NO_RESPONSE = 0x0B -- 网关目标设备无响应
+
+-- 全局队列与调度器;
+local request_queue = {}
+local next_request_id = 1
+local scheduler_started = false
+
+-- 生成唯一请求 ID;
+local function gen_request_id()
+    local id = next_request_id
+    next_request_id = next_request_id + 1
+    -- 确保请求 ID 在 32 位有符号整数范围内;
+    if next_request_id == 0x7FFFFFFF then next_request_id = 1 end
+    return id
+end
+
+-- 处理队列中的请求;
+local function process_request_queue()
+    while true do
+        if #request_queue > 0 then
+            local req = table.remove(request_queue, 1)
+            local instance = req.instance
+            local config = req.config
+            local is_read = req.is_read
+            local req_id = req.request_id
+
+            local result
+            if is_read then
+                result = instance:read_internal(config)
+            else
+                result = instance:write_internal(config)
+            end
+
+            sys.publish("exmodbus/resp/" .. req_id, result)
+        else
+            sys.waitUntil("start_scheduler")
+        end
+    end
+end
+
+-- 启动调度器;
+local function start_scheduler()
+    if scheduler_started then return end
+    scheduler_started = true
+    sys.taskInit(process_request_queue)
+end
+
+-- 入队请求并等待响应;(内部使用)
+function exmodbus.enqueue_request(instance, config, is_read)
+    -- 生成唯一请求 ID;
+    local req_id = gen_request_id()
+
+    -- 检查队列是否为空;
+    -- 如果为空,先入队,然后发布主题告知调度器开始处理;
+    -- 如果不为空,则直接入队,不用告知调度器;
+    if #request_queue == 0 then
+        -- 入队请求;
+        table.insert(request_queue, {
+            instance = instance,
+            config = config,
+            is_read = is_read,
+            request_id = req_id
+        })
+
+        sys.publish("start_scheduler")
+    else
+        -- 入队请求;
+        table.insert(request_queue, {
+            instance = instance,
+            config = config,
+            is_read = is_read,
+            request_id = req_id
+        })
+    end
+
+    -- 启动调度器;
+    start_scheduler()
+    local ok, result = sys.waitUntil("exmodbus/resp/" .. req_id)
+
+    return result
+end
+
+--[[
+创建一个新的实例;
+@api exmodbus.create(config)
+@param config table 配置参数表,包含以下字段:
+    mode number 通信模式,必须是 exmodbus 模块定义的常量(如 exmodbus.RTU_MASTER)
+    uart_id number 串口 ID,uart0 写 0,uart1 写 1,以此类推
+    baud_rate number 波特率
+    data_bits number 数据位
+    stop_bits number 停止位
+    parity_bits number 校验位
+    byte_order number 字节顺序
+    rs485_dir_gpio number RS485 方向转换 GPIO 引脚
+    rs485_dir_rx_level number RS485 接收方向电平
+    adapter number 网卡 ID
+    ip_address string 服务器 IP 地址
+    port number 服务器端口号
+    is_udp boolean 是否使用 UDP 协议
+    is_tls boolean 是否使用加密传输
+    keep_idle number 连接空闲多长时间后,开始发送第一个 keepalive 探针报文,单位:秒
+    keep_interval number 发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针,单位:秒
+    keep_cnt number 总共发送多少次探针后,如果依然没有回复,则判断连接已断开
+    server_cert string TCP 模式下的服务器 CA 证书数据,UDP 模式下的 PSK
+    client_cert string TCP 模式下的客户端证书数据,UDP 模式下的 PSK-ID
+    client_key string TCP 模式下的客户端私钥加密数据
+    client_password string TCP 模式下的客户端私钥口令数据
+@return table/nil 成功时返回实例对象,失败时返回 nil
+@usage
+RTU/ASCII 通信模式:
+local config = {
+    mode = exmodbus.RTU_MASTER, -- 通信模式:RTU 主站
+    uart_id = 1,                -- 串口 ID:uart1
+    baud_rate = 115200,         -- 波特率:115200
+    data_bits = 8,              -- 数据位:8
+    stop_bits = 1,              -- 停止位:1
+    parity_bits = uart.None,    -- 校验位:无校验
+    byte_order = uart.LSB,      -- 字节顺序:小端序
+    rs485_dir_gpio = 23,        -- RS485 方向转换 GPIO 引脚
+    rs485_dir_rx_level = 0      -- RS485 接收方向电平:0 为低电平,1 为高电平
+}
+local rtu_master = exmodbus.create(config)
+
+TCP 通信模式:
+local config = {
+    mode = exmodbus.TCP_MASTER, -- 通信模式:TCP 主站
+    adapter = socket.LWIP_ETH,  -- 网卡 ID:LwIP 协议栈的以太网卡
+    ip_address = "192.168.1.100", -- 服务器 IP 地址:192.168.1.100(主站:服务器 IP;从站:本地 IP,从站可以不用填此参数)
+    port = 502,                 -- 服务器端口号:502(主站:服务器端口;从站:本地端口)
+    is_udp = false,             -- 是否使用 UDP 协议:不使用 UDP 协议,false/nil 表示使用 TCP 协议
+    is_tls = false,             -- 是否使用加密传输:不使用加密传输,false/nil 表示不使用加密
+    keep_idle = 300,            -- 连接空闲多长时间后,开始发送第一个 keepalive 探针报文:300 秒
+    keep_interval = 10,         -- 发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针:10 秒
+    keep_cnt = 3,               -- 总共发送多少次探针后,如果依然没有回复,则判断连接已断开:3 次
+    server_cert = nil,          -- TCP 模式下的服务器 CA 证书数据,UDP 模式下的 PSK:如果客户端不需要验证服务器证书,则设为 nil 或空着
+    client_cert = nil,          -- TCP 模式下的客户端证书数据,UDP 模式下的 PSK-ID:如果服务器不需要验证客户端证书,则设为 nil 或空着
+    client_key = nil,           -- TCP 模式下的客户端私钥加密数据:如果服务器不需要验证客户端私钥,则设为 nil 或空着
+    client_password = nil       -- TCP 模式下的客户端私钥口令数据:如果服务器不需要验证客户端私钥口令,则设为 nil 或空着
+}
+local tcp_master = exmodbus.create(config)
+--]]
+function exmodbus.create(config)
+    -- 检查配置参数是否有效;
+    if not config or type(config) ~= "table" then
+        log.error("exmodbus", "配置必须是表格类型")
+        return false
+    end
+
+    -- 根据通信模式加载对应的模块;
+    if config.mode == exmodbus.RTU_MASTER or config.mode == exmodbus.RTU_SLAVE or
+        config.mode == exmodbus.ASCII_MASTER or config.mode == exmodbus.ASCII_SLAVE then
+        local result, mod = pcall(require, "exmodbus_rtu_ascii")
+        if not result then
+            log.error("exmodbus", "加载 RTU/ASCII 模块失败")
+            return false
+        end
+        return mod.create(config, exmodbus, gen_request_id)
+    elseif config.mode == exmodbus.TCP_MASTER or config.mode == exmodbus.TCP_SLAVE then
+        local result, mod = pcall(require, "exmodbus_tcp")
+        if not result then
+            log.error("exmodbus", "加载 TCP 模块失败")
+            return false
+        end
+        return mod.create(config, exmodbus, gen_request_id)
+    else
+        log.error("exmodbus", "通信模式不支持")
+        return false
+    end
+end
+
+--[[
+主站向从站发送读取请求(仅适用于 RTU、ASCII、TCP 主站模式)
+@api modbus:read(config)
+@param config table 配置参数表,包含以下字段:
+    slave_id number 从站 ID
+    reg_type number 寄存器类型
+    start_addr number 寄存器起始地址
+    reg_count number 寄存器数量
+    raw_request string 原始请求帧
+    timeout number 超时时间,单位:毫秒
+@return table 包含以下字段:
+    status number 响应结果状态码,参考 exmodbus 模块定义的常量(如 exmodbus.STATUS_SUCCESS)
+    execption_code number 异常码,仅在 status 为 exmodbus.STATUS_EXCEPTION 时有效
+    data table 寄存器数值,仅在 status 为 exmodbus.STATUS_SUCCESS 时有效,包含以下字段
+        [start_addr] number 寄存器数值,索引为寄存器地址,值为寄存器数值
+        ...
+    raw_response string 原始响应帧
+@usage
+用户在传入 config 参数时,有 原始帧 和 字段参数 两种方式
+1. 原始帧方式
+local read_config = {
+    raw_request = "010300000002C40B", -- 原始请求帧:01 03 00 00 00 02 C4 0B(读取保持寄存器 0x0000 开始的 2 个寄存器)
+    timeout = 1000                    -- 超时时间:1000 毫秒
+}
+local result = modbus:read(read_config)
+if result.status == exmodbus.STATUS_SUCCESS then
+    log.info("exmodbus_test", "读取成功,原始响应帧: ", table.concat(result.raw_response, ", "))
+elseif result.status == exmodbus.STATUS_TIMEOUT then
+    log.error("exmodbus_test", "读取请求超时")
+else
+    log.error("exmodbus_test", "读取失败")
+end
+
+2. 字段参数方式
+local read_config = {
+    slave_id = 1,                         -- 从站 ID:1
+    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+    start_addr = 0x0000,                  -- 寄存器起始地址:0
+    reg_count = 0x0002,                   -- 寄存器数量:2
+    timeout = 1000                        -- 超时时间:1000 毫秒
+}
+
+local result = modbus:read(read_config)
+-- 根据返回状态处理结果
+if result.status == exmodbus.STATUS_SUCCESS then
+    -- 数据解析:
+    log.info("exmodbus_test", "成功读取到从站 1 保持寄存器 0-2 的值,寄存器 0 数值:", result.data[result.start_addr],
+        ",寄存器 1 数值:", result.data[result.start_addr + 1])
+elseif result.status == exmodbus.STATUS_DATA_INVALID then
+    log.info("exmodbus_test", "收到从站 1 的响应数据但数据损坏/校验失败")
+elseif result.status == exmodbus.STATUS_EXCEPTION then
+    log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", result.execption_code)
+elseif result.status == exmodbus.STATUS_TIMEOUT then
+    log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
+end
+--]]
+-- 该接口在各个子文件中,此处仅用作注释
+-- function modbus:read(config) end
+
+
+--[[
+主站向从站发送写入请求(仅适用于 RTU、ASCII、TCP 主站模式)
+@api modbus:write(config)
+@param config table 配置参数表,包含以下字段:
+    slave_id number 从站 ID
+    reg_type number 寄存器类型
+    start_addr number 寄存器起始地址
+    reg_count number 寄存器数量
+    data table 寄存器数值,包含以下字段:
+        [start_addr] number 寄存器数值,索引为寄存器地址,值为寄存器数值
+        ...
+    force_multiple boolean 是否强制使用写多个功能码进行写入单个寄存器操作
+    raw_request string 原始请求帧
+    timeout number 超时时间,单位:毫秒
+@return table 包含以下字段:
+    status number 响应结果状态码,参考 exmodbus 模块定义的常量(如 exmodbus.STATUS_SUCCESS)
+    execption_code number 异常码,仅在 status 为 exmodbus.STATUS_EXCEPTION 时有效
+    raw_response string 原始响应帧
+@usage
+用户在传入 config 参数时,有 原始帧 和 字段参数 两种方式
+1. 原始帧方式
+local write_config = {
+    raw_request = "011000000002007B01592471", -- 原始请求帧:01 10 00 00 00 02 00 7B 01 59 24 71(写入保持寄存器 0x0000 开始的 2 个寄存器,值为 0x007B 和 0x0159)
+    timeout = 1000                            -- 超时时间:1000 毫秒
+}
+local result = modbus:write(write_config)
+if result.status == exmodbus.STATUS_SUCCESS then
+    log.info("exmodbus_test", "写入成功,原始响应帧: ", table.concat(result.raw_response, ", "))
+elseif result.status == exmodbus.STATUS_TIMEOUT then
+    log.error("exmodbus_test", "写入请求超时")
+else
+    log.error("exmodbus_test", "写入失败")
+end
+
+2. 字段参数方式
+local write_config = {
+    slave_id = 1,                         -- 从站 ID:1
+    reg_type = exmodbus.HOLDING_REGISTER, -- 寄存器类型:保持寄存器
+    start_addr = 0x0000,                  -- 寄存器起始地址:0
+    reg_count = 0x0002,                   -- 寄存器数量:2
+    data = {
+        [0x0000] = 0x007B,                -- 寄存器 0 数值:0x007B
+        [0x0001] = 0x0159,                -- 寄存器 1 数值:0x0159
+    },
+    timeout = 1000                        -- 超时时间:1000 毫秒
+}
+
+local result = modbus:write(write_config)
+-- 根据返回状态处理结果
+if result.status == exmodbus.STATUS_SUCCESS then
+    log.info("exmodbus_test", "成功写入从站 1 保持寄存器 0-2 的值")
+elseif result.status == exmodbus.STATUS_DATA_INVALID then
+    log.info("exmodbus_test", "收到从站 1 的响应数据但数据损坏/校验失败")
+elseif result.status == exmodbus.STATUS_EXCEPTION then
+    log.info("exmodbus_test", "收到从站 1 的 modbus 标准异常响应,异常码为", result.execption_code)
+elseif result.status == exmodbus.STATUS_TIMEOUT then
+    log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
+end
+--]]
+-- 该接口在各个子文件中,此处仅用作注释
+-- function modbus:write(config) end
+
+
+--[[
+销毁 modbus 主站/从站实例对象
+@api modbus:destroy()
+@return nil
+@usage
+modbus:destroy()
+--]]
+-- 该接口在各个子文件中,此处仅用作注释
+-- function modbus:destroy() end
+
+
+--[[
+从站注册回调接口,用于处理主站发起的请求(仅适用于 RTU、ASCII、TCP 从站模式)
+@api modbus:on(callback)
+@param callback function 回调函数,格式为:
+    function callback(request)
+        -- 用户代码
+    end
+    该回调函数接收 requset 一个参数,该参数为 table 类型,包含以下字段:
+        slave_id number 从站 ID
+        func_code number 功能码
+        reg_type number 寄存器类型
+        start_addr number 寄存器起始地址
+        reg_count number 寄存器数量
+        data table 寄存器数值,包含以下字段:
+            [start_addr] number 寄存器数值,索引为寄存器地址,值为寄存器数值
+            ...
+@return nil
+@usage
+function callback(request)
+    -- 用户处理代码
+end
+--]]
+-- 该接口在各个子文件中,此处仅用作注释
+-- modbus:on(callback)
+
+return exmodbus

+ 1071 - 0
script/libs/exmodbus_rtu_ascii.lua

@@ -0,0 +1,1071 @@
+-- 定义类结构;
+local modbus = {}                            -- 定义 modbus 实例的元表;
+modbus.__index = modbus                      -- 定义 modbus 实例的索引元方法,用于访问实例的属性;
+modbus.__metatable = "instance is protected" -- 定义 modbus 实例的元表,防止外部修改;
+
+-- 模块级变量:依赖注入的引用;
+local exmodbus_ref -- 主模块引用,用于访问enqueue_request等核心功能;
+local gen_id_func  -- ID生成函数引用,用于生成唯一请求ID;
+
+-- 创建 modbus 实例的构造函数;
+function modbus:new(config)
+    local obj = {
+        mode = config.mode,                             -- 通信模式
+        uart_id = config.uart_id,                       -- 串口 ID
+        baud_rate = config.baud_rate,                   -- 波特率
+        data_bits = config.data_bits,                   -- 数据位
+        stop_bits = config.stop_bits,                   -- 停止位
+        parity_bits = config.parity_bits,               -- 校验位
+        byte_order = config.byte_order,                 -- 字节序
+        rs485_dir_gpio = config.rs485_dir_gpio,         -- RS485 方向控制 GPIO 引脚
+        rs485_dir_rx_level = config.rs485_dir_rx_level, -- RS485 方向控制接收电平
+    }
+
+    -- 串口是否已初始化;
+    obj.uart_initialized = false
+    -- 当前等待的主题;
+    obj.current_wait_request_id = nil
+    -- 从站请求处理回调函数;
+    obj.slaveHandler = nil
+
+    -- 设置原表;
+    setmetatable(obj, modbus)
+    -- 返回实例;
+    return obj
+end
+
+-- 解析 Modbus RTU 请求帧(从站使用);
+local function parse_rtu_request(frame)
+    -- 校验请求帧长度是否至少为 4 字节(包含从站地址、功能码和 CRC);
+    local frame_len = #frame
+    if frame_len < 4 then
+        return nil
+    end
+
+    -- 仅校验 CRC(格式基础校验);
+    local calc_crc = crypto.crc16_modbus(frame:sub(1, -3))
+    local recv_crc = string.byte(frame, -2) + bit.lshift(string.byte(frame, -1), 8)
+    if calc_crc ~= recv_crc then
+        -- log.warn("exmodbus", "请求帧 CRC 校验失败")
+        return nil
+    end
+
+    -- 提取从站地址和功能码;
+    local slave_id = string.byte(frame, 1)
+    local func_code = string.byte(frame, 2)
+
+    -- 所有字段尽可能提取,即使值可能非法;
+    local request_data = {
+        slave_id = slave_id,
+        func_code = func_code,
+        reg_type = nil,
+        start_addr = nil,
+        reg_count = nil,
+        data = {},
+    }
+
+    -- 读请求和单写请求;
+    -- 校验请求帧长度是否为 8 字节(包含从站地址、功能码、起始地址、寄存器数量/寄存器值和 CRC);
+    if frame_len == 8 then
+        request_data.start_addr = bit.lshift(string.byte(frame, 3), 8) + string.byte(frame, 4)
+        request_data.reg_count = bit.lshift(string.byte(frame, 5), 8) + string.byte(frame, 6)
+
+        -- 写单个线圈;
+        if func_code == exmodbus_ref.WRITE_SINGLE_COIL then
+            local coil_val = bit.lshift(string.byte(frame, 5), 8) + string.byte(frame, 6)
+            request_data.reg_count = 1
+            request_data.data[request_data.start_addr] = (coil_val == 0xFF00) and 1 or 0
+        -- 写单个保持寄存器;
+        elseif func_code == exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER then
+            local reg_val = bit.lshift(string.byte(frame, 5), 8) + string.byte(frame, 6)
+            request_data.reg_count = 1
+            request_data.data[request_data.start_addr] = reg_val
+        end
+    -- 多写请求;
+    -- 校验请求帧长度是否至少为 9 字节(包含从站地址、功能码、起始地址、寄存器数量、字节数量、数据和 CRC);
+    elseif frame_len >= 9 then
+        request_data.start_addr = bit.lshift(string.byte(frame, 3), 8) + string.byte(frame, 4)
+        request_data.reg_count = bit.lshift(string.byte(frame, 5), 8) + string.byte(frame, 6)
+
+        -- 写多个线圈;
+        if func_code == exmodbus_ref.WRITE_MULTIPLE_COILS then
+            for i = 0, request_data.reg_count - 1 do
+                local byte_idx = 8 + math.floor(i / 8)
+                if byte_idx > frame_len - 2 then break end -- 防止越界;
+                local bit_idx = i % 8
+                local byte_val = string.byte(frame, byte_idx)
+                local bit_val = bit.band(byte_val, bit.lshift(1, bit_idx)) ~= 0 and 1 or 0
+                request_data.data[request_data.start_addr + i] = bit_val
+            end
+        -- 写多个保持寄存器;
+        elseif func_code == exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS then
+            for i = 0, request_data.reg_count - 1 do
+                local pos = 8 + i * 2
+                if pos + 1 > frame_len - 2 then break end -- 防止越界;
+                local val = bit.lshift(string.byte(frame, pos), 8) + string.byte(frame, pos + 1)
+                request_data.data[request_data.start_addr + i] = val
+            end
+        end
+    end
+
+    -- 对于读请求,data 为 nil,由用户处理读逻辑;
+    if not request_data.data and (
+        func_code == exmodbus_ref.READ_COILS or
+        func_code == exmodbus_ref.READ_DISCRETE_INPUTS or
+        func_code == exmodbus_ref.READ_HOLDING_REGISTERS or
+        func_code == exmodbus_ref.READ_INPUT_REGISTERS
+    ) then
+        request_data.data = nil -- request_data.data 保持 nil,由用户处理读逻辑;
+    end
+
+    return request_data
+end
+
+-- 构建 Modbus RTU 响应帧(从站使用);
+local function build_rtu_response(request, user_return)
+    local slave_id = request.slave_id
+    local func_code = request.func_code
+
+    -- 用户返回异常码 -> 异常响应;
+    if type(user_return) == "number" then
+        local exception_code = user_return
+        local frame = string.char(slave_id, bit.bor(func_code, 0x80), exception_code)
+        local crc = crypto.crc16_modbus(frame)
+        return frame .. string.char(crc & 0xFF, (crc >> 8) & 0xFF)
+    end
+
+    -- 用户返回表 -> 正常响应;
+    if type(user_return) ~= "table" then
+        log.error("exmodbus", "从站回调必须返回 table 或 number,实际类型: ", type(user_return))
+        return nil
+    end
+
+    local data_bytes = ""
+
+    -- 处理读线圈和读离散输入响应;
+    if func_code == exmodbus_ref.READ_COILS or func_code == exmodbus_ref.READ_DISCRETE_INPUTS then
+        local reg_count = request.reg_count
+
+        -- 校验 reg_count 是否有效;
+        if not reg_count or reg_count <= 0 then
+            log.error("exmodbus", "请求中 reg_count 无效")
+            return nil
+        end
+
+        local byte_count = math.ceil(reg_count / 8)
+        local values = {}
+
+        for i = 0, reg_count - 1 do
+            local addr = request.start_addr + i
+            local bit_val = user_return[addr]
+            if bit_val == nil then
+                log.error("exmodbus", "读线圈/离散输入回调未返回地址 ", addr, " 的数据")
+                return nil
+            end
+            if bit_val ~= 0 and bit_val ~= 1 then
+                log.error("exmodbus", "地址 ", addr, " 的值必须为 0 或 1,实际: ", bit_val)
+                return nil
+            end
+
+            local byte_idx = math.floor(i / 8)
+            if not values[byte_idx] then values[byte_idx] = 0 end
+            if bit_val == 1 then
+                values[byte_idx] = bit.bor(values[byte_idx], bit.lshift(1, i % 8))
+            end
+        end
+
+        data_bytes = string.char(byte_count)
+        for i = 0, byte_count - 1 do
+            data_bytes = data_bytes .. string.char(values[i] or 0)
+        end
+    -- 处理读保持寄存器和读输入寄存器响应;
+    elseif func_code == exmodbus_ref.READ_HOLDING_REGISTERS or func_code == exmodbus_ref.READ_INPUT_REGISTERS then
+        local reg_count = request.reg_count
+        -- 校验 reg_count 是否有效;
+        if not reg_count or reg_count <= 0 then
+            log.error("exmodbus", "请求中 reg_count 无效")
+            return nil
+        end
+
+        local values = ""
+        for i = 0, reg_count - 1 do
+            local addr = request.start_addr + i
+            local val = user_return[addr]
+            if val == nil then
+                log.error("exmodbus", "读保持寄存器/输入寄存器回调未返回地址 ", addr, " 的数据")
+                return nil
+            end
+            if type(val) ~= "number" or val ~= math.floor(val) or val < 0 or val > 65535 then
+                log.error("exmodbus", "地址 ", addr, " 的值必须为 0~65535 的整数,实际: ", val)
+                return nil
+            end
+            values = values .. string.char((val >> 8) & 0xFF, val & 0xFF)
+        end
+        data_bytes = string.char(#values) .. values
+    -- 处理写单个线圈响应;
+    elseif func_code == exmodbus_ref.WRITE_SINGLE_COIL then
+        local addr = request.start_addr
+        -- 校验 start_addr 是否有效;
+        if addr == nil then
+            log.error("exmodbus", "请求中 start_addr 无效")
+            return nil
+        end
+        local coil_val = (request.data and request.data[addr]) or 0
+        local resp_val = (coil_val ~= 0) and 0xFF00 or 0x0000
+        data_bytes = string.char(
+            (addr >> 8) & 0xFF, addr & 0xFF,
+            (resp_val >> 8) & 0xFF, resp_val & 0xFF
+        )
+    -- 处理写单个保持寄存器响应;
+    elseif func_code == exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER then
+        local addr = request.start_addr
+        -- 校验 start_addr 是否有效;
+        if addr == nil then
+            log.error("exmodbus", "请求中 start_addr 无效")
+            return nil
+        end
+        local reg_val = (request.data and request.data[addr]) or 0
+        -- 校验 reg_val 是否有效;
+        if type(reg_val) ~= "number" or reg_val ~= math.floor(reg_val) or reg_val < 0 or reg_val > 65535 then
+            log.error("exmodbus", "地址 ", addr, " 的值必须为 0~65535 的整数,实际: ", reg_val)
+            return nil
+        end
+        data_bytes = string.char(
+            (addr >> 8) & 0xFF, addr & 0xFF,
+            (reg_val >> 8) & 0xFF, reg_val & 0xFF
+        )
+    -- 处理写多个线圈/保持寄存器响应;
+    elseif func_code == exmodbus_ref.WRITE_MULTIPLE_COILS or func_code == exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS then
+        local start_addr = request.start_addr
+        local reg_count = request.reg_count
+        -- 校验 start_addr 和 reg_count 是否有效;
+        if not start_addr or not reg_count or reg_count <= 0 then
+            log.error("exmodbus", "请求中 start_addr 或 reg_count 无效")
+            return nil
+        end
+        data_bytes = string.char(
+            (start_addr >> 8) & 0xFF, start_addr & 0xFF,
+            (reg_count >> 8) & 0xFF, reg_count & 0xFF
+        )
+    -- 处理未知功能码,视为错误;
+    else
+        log.error("exmodbus", "不支持的功能码,且未返回异常码: ", func_code)
+        return nil
+    end
+
+    local frame = string.char(slave_id, func_code) .. data_bytes
+    local crc = crypto.crc16_modbus(frame)
+    return frame .. string.char(crc & 0xFF, (crc >> 8) & 0xFF)
+end
+
+-- 初始化串口;
+local function init_uart(instance)
+    -- 检查串口是否已被初始化;
+    if instance.uart_initialized then
+        log.warn("exmodbus", "串口 ", instance.uart_id, " 已经初始化,无需重复初始化")
+        return true
+    end
+
+    -- 配置串口参数,并开启串口功能;
+    local result = uart.setup(
+        instance.uart_id,           -- 串口 ID
+        instance.baud_rate,         -- 波特率
+        instance.data_bits,         -- 数据位
+        instance.stop_bits,         -- 停止位
+        instance.parity_bits,       -- 校验位
+        instance.byte_order,        -- 字节序
+        nil,                        -- 缓冲区大小
+        instance.rs485_dir_gpio,    -- RS485 方向控制 GPIO 引脚
+        instance.rs485_dir_rx_level -- RS485 方向控制接收电平
+    )
+    -- 检查串口是否初始化成功;
+    -- 成功时返回 0,其他返回值表示失败;
+    if result ~= 0 then
+        log.error("exmodbus", "串口 ", instance.uart_id, " 初始化失败")
+        return false
+    end
+
+    -- 定义发送完成回调函数;
+    -- 当串口发送完成时,发布一个主题,通知其他任务;
+    local function on_sent(uart_id)
+        sys.publish("exmodbus/sent/" .. uart_id, true)
+    end
+
+    -- 定义接收完成回调函数;
+    -- 当串口接收完成时,对接收数据进行处理;
+    -- 处理成功时,发布一个主题,通知其他任务;
+    -- 处理失败时,不做任何处理;
+    local function on_receive(uart_id, data_len)
+        local data = uart.read(uart_id, data_len)
+        if not data or #data == 0 then return end
+
+        -- 处理 RTU 主站模式下的接收数据;
+        if instance.mode == exmodbus_ref.RTU_MASTER then
+            -- 校验等待主题是否存在;
+            if instance.current_wait_request_id then
+                -- 发布主题,通知其他任务;
+                sys.publish("exmodbus/rtu_resp/" .. instance.current_wait_request_id, data)
+                -- 发布后,清除等待主题;
+                instance.current_wait_request_id = nil
+                return
+            end
+        -- 处理 RTU 从站模式下的接收数据;
+        elseif instance.mode == exmodbus_ref.RTU_SLAVE then
+            -- 解析 RTU 请求帧;
+            local request = parse_rtu_request(data)
+            if request then
+                -- 广播地址(0)不响应;
+                if request.slave_id == 0 then
+                    -- 调用回调以允许用户记录或处理广播命令(如写寄存器);
+                    if instance.slaveHandler then
+                        instance.slaveHandler(request)
+                        -- 注意:即使回调返回数据,也不发送响应;
+                    end
+                    -- 广播请求处理完毕,不回复;
+                    return
+                end
+                if instance.slaveHandler then
+                    local user_return = instance.slaveHandler(request)
+                    local response_frame = build_rtu_response(request, user_return)
+                    if response_frame then
+                        uart.write(uart_id, response_frame)
+                    else
+                        log.error("exmodbus", "构建响应帧失败,从站地址:", request.slave_id)
+                    end
+                else
+                    log.warn("exmodbus", "收到主站请求,但未注册回调函数")
+                end
+            else
+                log.debug("exmodbus", "无效 RTU 请求帧(CRC 或格式错误)")
+            end
+            return
+        end
+    end
+
+    -- 注册发送完成和接收完成回调函数;
+    uart.on(instance.uart_id, "sent", on_sent)
+    uart.on(instance.uart_id, "receive", on_receive)
+
+    -- 初始化成功,设置标志位为 true;
+    instance.uart_initialized = true
+    log.info("exmodbus", "串口 " .. instance.uart_id .. " 初始化成功,波特率 " .. instance.baud_rate)
+    return true
+end
+
+-- 创建一个新的实例;
+local function create(config, exmodbus, gen_request_id)
+    exmodbus_ref = exmodbus
+    gen_id_func = gen_request_id
+
+    -- 创建一个新的实例;
+    local instance = modbus:new(config)
+    -- 检查实例是否创建成功;
+    if not instance then
+        log.error("exmodbus", "创建 Modbus 实例失败")
+        return false
+    end
+
+    -- 初始化串口;
+    local result = init_uart(instance)
+    -- 检查串口初始化结果;
+    if not result then
+        -- 销毁已创建的实例,释放资源;
+        instance:destroy()
+        return false
+    end
+
+    -- 返回实例;
+    return instance
+end
+
+-- 销毁已创建的实例,释放资源;
+function modbus:destroy()
+    -- 检查实例是否已被销毁;
+    if not self then
+        log.error("exmodbus", "实例对象已被销毁,无需重复销毁")
+        return
+    end
+
+    -- 关闭串口;
+    if self.uart_initialized then
+        uart.close(self.uart_id)
+        uart.on(self.uart_id, "sent", nil)
+        uart.on(self.uart_id, "receive", nil)
+    end
+
+    -- 释放GPIO资源;
+    if self.rs485_dir_gpio then
+        gpio.close(self.rs485_dir_gpio)
+    end
+
+    -- 销毁已创建的实例;
+    setmetatable(self, nil)
+
+    log.info("exmodbus", "实例对象已销毁")
+end
+
+-- 构建 Modbus RTU 帧的函数,支持读取和写入操作;(主站使用)
+local function build_rtu_frame(opt_type, config)
+    -- 参数验证;
+    if not config or type(config) ~= "table" then
+        log.error("exmodbus", "配置必须是表格类型")
+        return false
+    end
+
+    -- 验证必要参数;
+    if not config.slave_id then
+        log.error("exmodbus", "缺少必要参数: slave_id")
+        return false
+    end
+
+    if not config.reg_type then
+        log.error("exmodbus", "缺少必要参数: reg_type")
+        return false
+    end
+
+    if not config.start_addr then
+        log.error("exmodbus", "缺少必要参数: start_addr")
+        return false
+    end
+
+    if not config.reg_count then
+        log.error("exmodbus", "缺少必要参数: reg_count")
+        return false
+    end
+
+    if opt_type == "write" then
+        if not config.data then
+            log.error("exmodbus", "缺少写入请求必要参数: data")
+            return false
+        end
+    end
+
+    -- 参数范围验证;
+    if type(config.slave_id) ~= "number" or config.slave_id < 1 or config.slave_id > 247 then
+        log.error("exmodbus", "从站地址必须在 1-247 范围内")
+        return false
+    end
+
+    if type(config.start_addr) ~= "number" or config.start_addr < 0 or config.start_addr > 65535 then
+        log.error("exmodbus", "起始地址必须在 0-65535 范围内")
+        return false
+    end
+
+    if config.reg_type ~= exmodbus_ref.COIL_STATUS and config.reg_type ~= exmodbus_ref.INPUT_STATUS and
+        config.reg_type ~= exmodbus_ref.HOLDING_REGISTER and config.reg_type ~= exmodbus_ref.INPUT_REGISTER then
+        log.error("exmodbus", "无效的寄存器类型: " .. tostring(config.reg_type))
+        return false
+    end
+
+    -- 根据操作类型和寄存器类型确定功能码;
+    local function_code
+    if opt_type == "write" then
+        -- 校验每一个地址是否有数据,且数据是否为数字类型;
+        for i = 0, config.reg_count - 1 do
+            local addr = config.start_addr + i
+            if config.data[addr] == nil then
+                log.error("exmodbus", "缺少寄存器数据", "address:", addr)
+                return false
+            end
+            if type(config.data[addr]) ~= "number" then
+                log.error("exmodbus", "寄存器数据必须是数字类型", "address:", addr)
+                return false
+            end
+        end
+
+        -- 判断是否强制使用写多个功能码;
+        local use_multiple = config.force_multiple
+
+        if config.reg_count == 1 then
+            -- 写入单个线圈或单个保持寄存器;
+            if not use_multiple then -- 使用写单个功能码;
+                if config.reg_type == exmodbus_ref.COIL_STATUS then
+                    function_code = exmodbus_ref.WRITE_SINGLE_COIL
+                elseif config.reg_type == exmodbus_ref.HOLDING_REGISTER then
+                    function_code = exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER
+                end
+            else -- 使用写多个功能码;
+                if config.reg_type == exmodbus_ref.COIL_STATUS then
+                    function_code = exmodbus_ref.WRITE_MULTIPLE_COILS
+                elseif config.reg_type == exmodbus_ref.HOLDING_REGISTER then
+                    function_code = exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS
+                end
+            end
+        elseif config.reg_count > 1 then
+            -- 写入多个线圈或寄存器;
+            if config.reg_type == exmodbus_ref.COIL_STATUS then
+                function_code = exmodbus_ref.WRITE_MULTIPLE_COILS
+            elseif config.reg_type == exmodbus_ref.HOLDING_REGISTER then
+                function_code = exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS
+            end
+        end
+    elseif opt_type == "read" then
+        -- 读线圈状态;
+        if config.reg_type == exmodbus_ref.COIL_STATUS then
+            function_code = exmodbus_ref.READ_COILS
+        -- 读离散输入状态;
+        elseif config.reg_type == exmodbus_ref.INPUT_STATUS then
+            function_code = exmodbus_ref.READ_DISCRETE_INPUTS
+        -- 读保持寄存器;
+        elseif config.reg_type == exmodbus_ref.HOLDING_REGISTER then
+            function_code = exmodbus_ref.READ_HOLDING_REGISTERS
+        -- 读输入寄存器;
+        elseif config.reg_type == exmodbus_ref.INPUT_REGISTER then
+            function_code = exmodbus_ref.READ_INPUT_REGISTERS
+        end
+    end
+
+    local data_bytes
+    -- 功能码 0x01 和 0x02:读取线圈状态和离散输入状态;
+    if function_code == exmodbus_ref.READ_COILS or function_code == exmodbus_ref.READ_DISCRETE_INPUTS then
+        -- 验证数量范围;
+        if config.reg_count < 1 or config.reg_count > 2000 then
+            log.error("exmodbus", "线圈/离散输入读取数量超出范围: " .. config.reg_count .. " (范围: 1-2000)")
+            return false
+        end
+        
+        -- 构建数据部分(起始地址 + 数量)(大端序);
+        data_bytes = string.char(
+            (config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF,
+            (config.reg_count >> 8) & 0xFF, config.reg_count & 0xFF
+        )
+
+    -- 功能码 0x03 和 0x04:读取保持寄存器和输入寄存器;
+    elseif function_code == exmodbus_ref.READ_HOLDING_REGISTERS or function_code == exmodbus_ref.READ_INPUT_REGISTERS then
+        -- 验证数量范围;
+        if config.reg_count < 1 or config.reg_count > 125 then
+            log.error("exmodbus", "寄存器读取数量超出范围: " .. config.reg_count .. " (范围: 1-125)")
+            return false
+        end
+
+        -- 构建数据部分(起始地址 + 数量)(字节序为大端序);
+        data_bytes = string.char(
+            (config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF,
+            (config.reg_count >> 8) & 0xFF, config.reg_count & 0xFF
+        )
+
+    -- 功能码 0x05:写入单个线圈;
+    elseif function_code == exmodbus_ref.WRITE_SINGLE_COIL then
+        -- 写入单个线圈,值必须是 0xFF00 (ON) 或 0x0000 (OFF);
+        local value = config.data[config.start_addr] ~= 0 and 0xFF00 or 0x0000
+        
+        -- 构建数据部分(起始地址 + 值)(字节序为大端序);
+        data_bytes = string.char(
+            (config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF,
+            (value >> 8) & 0xFF, value & 0xFF
+        )
+
+    -- 功能码 0x06:写入单个保持寄存器;
+    elseif function_code == exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER then
+        -- 写入单个保持寄存器;
+        local value = config.data[config.start_addr]
+
+        -- 验证寄存器值范围(16 位无符号整数);
+        if value < 0 or value > 65535 or value ~= math.floor(value) then
+            log.error("exmodbus", "寄存器值必须是 0~65535 范围内的整数,实际值: ", value)
+            return false
+        end
+        
+        -- 构建数据部分(起始地址 + 值)(字节序为大端序);
+        data_bytes = string.char(
+            (config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF,
+            (value >> 8) & 0xFF, value & 0xFF
+        )
+
+    -- 功能码 0x0F:写入多个线圈;
+    elseif function_code == exmodbus_ref.WRITE_MULTIPLE_COILS then
+        -- 验证数量范围;
+        if config.reg_count < 1 or config.reg_count > 1968 then
+            log.error("exmodbus", "线圈写入数量超出范围: " .. config.reg_count .. " (范围: 1-1968)")
+            return false
+        end
+
+        -- 计算字节数;
+        local byte_count = math.ceil(config.reg_count / 8)
+        local values_bytes = ""
+
+        -- 构建线圈数据(字节序为大端序);
+        for i = 0, byte_count - 1 do
+            local byte_value = 0
+            -- 遍历当前字节的 8 个位;
+            for j = 0, 7 do
+                local bit_index = i * 8 + j -- 计算当前比特在整个线圈序列中的全局索引(从 0 开始);
+                -- 检查当前比特是否在有效范围内;
+                if bit_index < config.reg_count then
+                    local addr = config.start_addr + bit_index -- 根据起始地址和全局索引计算实际的线圈地址;
+                    local bit_val = config.data[addr] -- 获取当前线圈的状态值(0 或 1);
+                    if bit_val ~= nil and bit_val ~= 0 then
+                        byte_value = byte_value | (1 << j) -- 如果状态为 1,则将当前位设置为 1;
+                    end
+                end
+            end
+            values_bytes = values_bytes .. string.char(byte_value)
+        end
+
+        -- 构建数据部分(起始地址 + 数量 + 字节数 + 线圈数据)(字节序为大端序);
+        data_bytes = string.char(
+            (config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF,
+            (config.reg_count >> 8) & 0xFF, config.reg_count & 0xFF,
+            byte_count
+        ) .. values_bytes
+
+    -- 功能码 0x10:写入多个保持寄存器;
+    elseif function_code == exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS then
+        -- 验证数量范围;
+        if config.reg_count < 1 or config.reg_count > 123 then
+            log.error("exmodbus", "寄存器写入数量超出范围: " .. config.reg_count .. " (范围: 1-123)")
+            return false
+        end
+
+        -- 计算字节数;
+        local byte_count = config.reg_count * 2
+        local values_bytes = ""
+
+        -- 构建寄存器数据(字节序为大端序);
+        for i = 0, config.reg_count - 1 do
+            local addr = config.start_addr + i
+            local value = config.data[addr]
+            values_bytes = values_bytes .. string.char(
+                (value >> 8) & 0xFF, value & 0xFF
+            )
+        end
+
+        -- 构建数据部分(起始地址 + 数量 + 字节数 + 寄存器数据)(字节序为大端序);
+        data_bytes = string.char(
+            (config.start_addr >> 8) & 0xFF, config.start_addr & 0xFF,
+            (config.reg_count >> 8) & 0xFF, config.reg_count & 0xFF,
+            byte_count
+        ) .. values_bytes
+
+    -- 未知功能码;
+    else
+        log.error("exmodbus", "不支持的功能码构建: " .. function_code)
+        return false
+    end
+
+    -- 构建 Modbus RTU 帧(从站地址 + 功能码 + 数据);
+    local frame = string.char(config.slave_id, function_code) .. data_bytes
+
+    -- 计算 CRC16 校验并添加到帧末尾(小端序);
+    local crc = crypto.crc16_modbus(frame)
+    frame = frame .. string.char(crc & 0xFF, (crc >> 8) & 0xFF)
+
+    return frame, function_code
+end
+
+-- 发送 Modbus 请求并等待响应;
+local function sendRequest_waitResponse(instance, request_frame, config)
+    -- 生成唯一请求ID;
+    local req_id = gen_id_func()
+    instance.current_wait_request_id = req_id
+
+    -- 执行发送请求;
+    uart.write(instance.uart_id, request_frame)
+
+    -- 等待发送完成;
+    local sent_ok = sys.waitUntil("exmodbus/sent/" .. instance.uart_id, 200)
+    if not sent_ok then
+        log.error("exmodbus", "数据发送失败")
+        instance.current_wait_request_id = nil
+        return false, nil
+    end
+
+    -- -- 显示发送的HEX数据;
+    -- local hex_str = ""
+    -- for i = 1, #request_frame do
+    --     hex_str = hex_str .. string.format("%02X ", string.byte(request_frame, i))
+    -- end
+    -- log.info("exmodbus", "发送请求命令成功, HEX: " .. hex_str:sub(1, -2))
+
+    -- 等待接收响应;
+    local ok, response = sys.waitUntil("exmodbus/rtu_resp/" .. req_id, config.timeout or 1000)
+
+    -- 清除当前等待的请求ID;
+    instance.current_wait_request_id = nil
+
+    -- 显示接收的HEX数据;
+    if ok then
+        -- hex_str = ""
+        -- for i = 1, #response do
+        --     hex_str = hex_str .. string.format("%02X ", string.byte(response, i))
+        -- end
+        -- log.info("exmodbus", "接收响应成功, HEX: " .. hex_str:sub(1, -2))
+        return true, response
+    else
+        -- log.error("exmodbus", "接收响应失败或超时")
+        return false, nil
+    end
+end
+
+-- 解析 Modbus RTU 响应报文(主站使用);
+local function parse_rtu_response(response, config, function_code)
+    -- 定义返回数据结构;
+    local return_data = {
+        status = false,
+        execption_code = nil,
+        data = {},
+    }
+
+    -- 验证响应是否为空;
+    if not response or #response == 0 then
+        log.error("exmodbus", "响应报文为空")
+        return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+        return return_data
+    end
+
+    -- 验证响应长度(最小长度:从站地址 + 功能码 + CRC = 4 字节);
+    if not response or #response < 4 then
+        log.error("exmodbus", "响应报文长度不足")
+        return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+        return return_data
+    end
+
+    -- 提取响应中的字段;
+    local actual_slave_id = string.byte(response, 1)
+    local actual_function_code = string.byte(response, 2)
+    local response_length = #response
+
+    -- 验证从站地址是否匹配;
+    if actual_slave_id ~= config.slave_id then
+        log.error("exmodbus", "从站地址不匹配,期望:", config.slave_id, "实际:", actual_slave_id)
+        return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+        return return_data
+    end
+
+    -- 检查是否为异常响应(功能码最高位为 1);
+    if bit.band(actual_function_code, 0x80) ~= 0 then
+        -- 异常响应格式:从站地址(1 字节) + 功能码(1 字节) + 异常码(1 字节) + CRC(2 字节);
+        if response_length ~= 5 then
+            log.error("exmodbus", "异常响应报文长度不正确,期望: 5 字节,实际:", response_length, "字节")
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 提取异常码(第 3 字节);
+        local exception_code = string.byte(response, 3)
+        log.error("exmodbus", "接收到 Modbus 异常响应,功能码:", actual_function_code, "异常码:", exception_code)
+
+        return_data.status = exmodbus_ref.STATUS_EXCEPTION
+        return_data.execption_code = exception_code
+        return return_data
+    end
+
+    -- 验证功能码是否匹配;
+    if actual_function_code ~= function_code then
+        log.error("exmodbus", "功能码不匹配,期望:", function_code, "实际:", actual_function_code)
+        return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+        return return_data
+    end
+
+    -- 根据不同的功能码解析数据;
+    local parsed_data = {}
+
+    -- 功能码 0x01 和 0x02:读取线圈状态和离散输入状态;
+    if function_code == exmodbus_ref.READ_COILS or function_code == exmodbus_ref.READ_DISCRETE_INPUTS then
+        -- 提取数据部分(不包括CRC);
+        local data_length = string.byte(response, 3)
+        local data_start_pos = 4
+        local data_end_pos = response_length - 2 -- 减去CRC长度
+
+        -- 验证数据长度是否正确;
+        -- 注意:这里只验证响应报文中声明的数据长度与实际数据长度是否一致;
+        if data_end_pos - data_start_pos + 1 ~= data_length then
+            log.error("exmodbus", "数据长度不匹配,期望:", data_length, "实际:", data_end_pos - data_start_pos + 1)
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 验证字节数是否足够表示指定数量的位;
+        local expected_bytes = math.ceil(config.reg_count / 8)
+        if data_length < expected_bytes then
+            log.error("exmodbus", "数据字节数不足,无法表示所有位")
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 解析位数据;
+        for i = 0, config.reg_count - 1 do
+            local modbus_addr = config.start_addr + i           -- 计算当前位对应的 Modbus 地址;
+            local byte_pos = data_start_pos + math.floor(i / 8) -- 计算当前位对应的字节位置;
+            local bit_pos = i % 8                               -- 计算当前位对应的位位置;
+            local byte_value = string.byte(response, byte_pos)
+            parsed_data[modbus_addr] = bit.band(byte_value, bit.lshift(1, bit_pos)) ~= 0 and 1 or 0
+        end
+
+    -- 功能码 0x03 和 0x04:读取保持寄存器和输入寄存器;
+    elseif function_code == exmodbus_ref.READ_HOLDING_REGISTERS or function_code == exmodbus_ref.READ_INPUT_REGISTERS then
+        -- 提取数据部分(不包括CRC);
+        local data_length = string.byte(response, 3)
+        local data_start_pos = 4
+        local data_end_pos = response_length - 2 -- 减去CRC长度
+
+        -- 验证数据长度是否正确;
+        if data_end_pos - data_start_pos + 1 ~= data_length then
+            log.error("exmodbus", "数据长度不匹配,期望:", data_length, "实际:", data_end_pos - data_start_pos + 1)
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 验证字节数是否足够表示指定数量的寄存器;
+        local expected_bytes = config.reg_count * 2
+        if data_length < expected_bytes then
+            log.error("exmodbus", "数据字节数不足,无法表示所有寄存器")
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 解析寄存器数据(大端序);
+        for i = 0, config.reg_count - 1 do
+            local modbus_addr = config.start_addr + i -- 计算当前寄存器对应的 Modbus 地址;
+            local reg_pos = data_start_pos + i * 2    -- 计算当前寄存器对应的字节位置;
+            parsed_data[modbus_addr] = bit.lshift(string.byte(response, reg_pos), 8) + string.byte(response, reg_pos + 1)
+        end
+
+    -- 功能码 0x05:写入单个线圈;
+    elseif function_code == exmodbus_ref.WRITE_SINGLE_COIL then
+        -- 写入单个线圈响应格式:从站地址(1 字节) + 功能码(1 字节) + 线圈地址(2 字节) + 线圈值(2 字节) + CRC(2 字节);
+        if response_length ~= 8 then
+            log.error("exmodbus", "写入单个线圈响应报文长度不正确")
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 解析线圈地址和值;
+        local coil_addr = bit.lshift(string.byte(response, 3), 8) + string.byte(response, 4)
+        local coil_value = bit.lshift(string.byte(response, 5), 8) + string.byte(response, 6)
+
+        -- 验证地址是否匹配请求;
+        if config.start_addr and coil_addr ~= config.start_addr then
+            log.error("exmodbus", "线圈地址不匹配,期望:", config.start_addr, "实际:", coil_addr)
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 线圈值应该是 0x0000(OFF) 或 0xFF00(ON);
+        local normalized_value = (coil_value == 0x0000) and 0 or 1
+        parsed_data[coil_addr] = normalized_value
+
+    -- 功能码 0x06:写入单个保持寄存器;
+    elseif function_code == exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER then
+        -- 写入单个保持寄存器响应格式:从站地址(1 字节) + 功能码(1 字节) + 寄存器地址(2 字节) + 寄存器值(2 字节) + CRC(2 字节);
+        if response_length ~= 8 then
+            log.error("exmodbus", "写入单个保持寄存器响应报文长度不正确")
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 解析寄存器地址和值;
+        local reg_addr = bit.lshift(string.byte(response, 3), 8) + string.byte(response, 4)
+        local reg_value = bit.lshift(string.byte(response, 5), 8) + string.byte(response, 6)
+
+        -- 验证地址是否匹配请求;
+        if config.start_addr and reg_addr ~= config.start_addr then
+            log.error("exmodbus", "单个保持寄存器地址不匹配,期望:", config.start_addr, "实际:", reg_addr)
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        parsed_data[reg_addr] = reg_value
+
+    -- 功能码 0x0F:写入多个线圈;
+    elseif function_code == exmodbus_ref.WRITE_MULTIPLE_COILS then
+        -- 写入多个线圈响应格式:从站地址(1 字节) + 功能码(1 字节) + 起始地址(2 字节) + 线圈数量(2 字节) + CRC(2 字节);
+        if response_length ~= 8 then
+            log.error("exmodbus", "写入多个线圈响应报文长度不正确")
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 解析起始地址和线圈数量;
+        local start_addr = bit.lshift(string.byte(response, 3), 8) + string.byte(response, 4)
+        local coil_count = bit.lshift(string.byte(response, 5), 8) + string.byte(response, 6)
+
+        -- 验证地址和数量是否匹配请求;
+        if config.start_addr and start_addr ~= config.start_addr then
+            log.error("exmodbus", "线圈起始地址不匹配,期望:", config.start_addr, "实际:", start_addr)
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        if config.reg_count and coil_count ~= config.reg_count then
+            log.error("exmodbus", "线圈数量不匹配,期望:", config.reg_count, "实际:", coil_count)
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 在返回数据中记录操作成功的起始地址和数量;
+        parsed_data.start_addr = start_addr
+        parsed_data.count = coil_count
+
+    -- 功能码 0x10:写入多个保持寄存器;
+    elseif function_code == exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS then
+        -- 写入多个保持寄存器响应格式:从站地址(1 字节) + 功能码(1 字节) + 起始地址(2 字节) + 寄存器数量(2 字节) + CRC(2 字节);
+        if response_length ~= 8 then
+            log.error("exmodbus", "写入多个保持寄存器响应报文长度不正确")
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 解析起始地址和寄存器数量;
+        local start_addr = bit.lshift(string.byte(response, 3), 8) + string.byte(response, 4)
+        local reg_count = bit.lshift(string.byte(response, 5), 8) + string.byte(response, 6)
+
+        -- 验证地址和数量是否匹配请求;
+        if config.start_addr and start_addr ~= config.start_addr then
+            log.error("exmodbus", "寄存器起始地址不匹配,期望:", config.start_addr, "实际:", start_addr)
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        if config.reg_count and reg_count ~= config.reg_count then
+            log.error("exmodbus", "寄存器数量不匹配,期望:", config.reg_count, "实际:", reg_count)
+            return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+            return return_data
+        end
+
+        -- 在返回数据中记录操作成功的起始地址和数量;
+        parsed_data.start_addr = start_addr
+        parsed_data.count = reg_count
+
+    -- 未知功能码;
+    else
+        log.error("exmodbus", "不支持的功能码解析:", function_code)
+        return_data.status = exmodbus_ref.STATUS_DATA_INVALID
+        return return_data
+    end
+
+    -- 成功解析响应数据;
+    -- log.info("exmodbus", "响应解析成功,功能码:", function_code, "数据:", json.encode(parsed_data))
+    return_data.status = exmodbus_ref.STATUS_SUCCESS
+    return_data.data = parsed_data
+    return return_data
+end
+
+-- 主站读取请求函数;(内部使用)
+function modbus:read_internal(config)
+    -- 处理响应结果;
+    local parsed_data = {}
+
+    -- 检查通信模式是否有效;
+    if self.mode == exmodbus_ref.RTU_MASTER then
+
+        -- 检查是否同时指定了 slave_id 和 raw_request;
+        if config.slave_id and config.raw_request then
+            log.error("exmodbus", "禁止同时指定 slave_id 和 raw_request")
+            return false
+        end
+
+        -- 用户传入字段式请求帧;
+        if config.slave_id then
+            -- 构建 Modbus RTU 帧;
+            local request_frame, function_code = build_rtu_frame("read", config)
+            if not request_frame then
+                parsed_data.status = exmodbus_ref.STATUS_DATA_INVALID
+                return parsed_data
+            end
+
+            -- 发送请求并等待响应;
+            local result, response = sendRequest_waitResponse(self, request_frame, config)
+            if not result then
+                parsed_data.status = exmodbus_ref.STATUS_TIMEOUT
+            else
+                -- 解析响应数据;
+                parsed_data = parse_rtu_response(response, config, function_code)
+            end
+
+        -- 用户传入原始请求帧;
+        elseif config.raw_request then
+            -- 发送请求并等待响应;
+            local result, response = sendRequest_waitResponse(self, config.raw_request, config)
+            if not result then
+                parsed_data.status = exmodbus_ref.STATUS_TIMEOUT
+            else
+                -- 直接返回响应结果和原始响应数据;
+                parsed_data.status = exmodbus_ref.STATUS_SUCCESS
+                parsed_data.raw_response = response
+            end
+        end
+
+        return parsed_data
+    else
+        log.error("exmodbus", "通信模式不支持")
+        return false
+    end
+end
+
+-- 主站写入请求的函数;
+function modbus:write_internal(config)
+
+    -- 处理响应结果;
+    local parsed_data = {}
+
+    -- 检查通信模式是否有效;
+    if self.mode == exmodbus_ref.RTU_MASTER then
+
+        -- 检查是否同时指定了 slave_id 和 raw_request;
+        if config.slave_id and config.raw_request then
+            log.error("exmodbus", "禁止同时指定 slave_id 和 raw_request")
+            return false
+        end
+
+        -- 用户传入字段式请求帧;
+        if config.slave_id then
+            -- 构建 Modbus RTU 帧;
+            local request_frame, function_code = build_rtu_frame("write", config)
+            if not request_frame then
+                parsed_data.status = exmodbus_ref.STATUS_DATA_INVALID
+                return parsed_data
+            end
+
+            -- 发送请求并等待响应;
+            local result, response = sendRequest_waitResponse(self, request_frame, config)
+            if not result then
+                -- log.error("exmodbus", "接收响应失败或超时")
+                parsed_data.status = exmodbus_ref.STATUS_TIMEOUT
+            else
+                -- 解析响应数据;
+                parsed_data = parse_rtu_response(response, config, function_code)
+            end
+
+        -- 用户传入原始请求帧;
+        elseif config.raw_request then
+            -- 发送请求并等待响应;
+            local result, response = sendRequest_waitResponse(self, config.raw_request, config)
+            if not result then
+                parsed_data.status = exmodbus_ref.STATUS_TIMEOUT
+            else
+                -- 直接返回响应结果和原始响应数据;
+                parsed_data.status = exmodbus_ref.STATUS_SUCCESS
+                parsed_data.raw_response = response
+            end
+        end
+
+        return parsed_data
+    else
+        log.error("exmodbus", "通信模式不支持")
+        return false
+    end
+end
+
+-- 主站读取请求的函数;
+function modbus:read(config)
+    return exmodbus_ref.enqueue_request(self, config, true)
+end
+
+-- 主站写入请求的函数;
+function modbus:write(config)
+    return exmodbus_ref.enqueue_request(self, config, false)
+end
+
+-- 注册从站请求处理回调函数;
+function modbus:on(callback)
+    if type(callback) ~= "function" then
+        log.error("exmodbus", "on(callback) 的参数必须是一个函数")
+        return false
+    end
+    self.slaveHandler = callback
+    log.info("exmodbus", "已注册从站请求处理回调函数")
+    return true
+end
+
+return { create = create }

+ 479 - 0
script/libs/exmodbus_tcp.lua

@@ -0,0 +1,479 @@
+-- 定义类结构;
+local modbus = {}                            -- 定义 modbus 实例的元表;
+modbus.__index = modbus                      -- 定义 modbus 实例的索引元方法,用于访问实例的属性;
+modbus.__metatable = "instance is protected" -- 定义 modbus 实例的元表,防止外部修改;
+
+-- 模块级变量:依赖注入的引用;
+local exmodbus_ref -- 主模块引用,用于访问enqueue_request等核心功能;
+local gen_id_func  -- ID生成函数引用,用于生成唯一请求ID;
+
+-- Modbus TCP 协议头长度
+local MODBUS_TCP_HEADER_LEN = 7
+
+local libnet = require "libnet"
+
+-- 创建 modbus 实例的构造函数;
+function modbus:new(config, TASK_NAME)
+    local obj = {
+        mode = config.mode,                       -- 通信模式
+        adapter = config.adapter,                 -- 网络适配器
+        ip_address = config.ip_address,           -- IP 地址
+        port = config.port,                       -- 端口号
+        is_udp = config.is_udp,                   -- 是否使用 UDP 协议
+        is_tls = config.is_tls,                   -- 是否使用 TLS 加密
+        keep_idle = config.keep_idle,             -- 连接空闲多长时间后,开始发送第一个 keepalive 探针报文(秒)
+        keep_interval = config.keep_interval,     -- 发送第一个探针后,如果没收到 ACK 回复,间隔多久再发送下一个探针(秒)
+        keep_cnt = config.keep_cnt,               -- 总共发送多少次探针后,如果依然没有回复,则判定连接已断开
+        server_cert = config.server_cert,         -- TCP模式下的服务器ca证书数据,UDP模式下的PSK
+        client_cert = config.client_cert,         -- TCP模式下的客户端证书数据,UDP模式下的PSK-ID
+        client_key = config.client_key,           -- TCP模式下的客户端私钥加密数据
+        client_password = config.client_password, -- TCP模式下的客户端私钥口令数据
+    }
+
+    -- 从站请求处理回调函数;
+    obj.slaveHandler = nil
+    -- 任务名称
+    obj.TASK_NAME = TASK_NAME
+    -- 接收数据缓冲区
+    obj.recv_buff = nil
+
+    -- 设置原表;
+    setmetatable(obj, modbus)
+    -- 返回实例;
+    return obj
+end
+
+-- 解析 TCP 请求帧(从站使用)
+local function parse_tcp_request(data)
+    -- 检查请求帧长度,至少包含 MBAP 头和功能码
+    if #data < MODBUS_TCP_HEADER_LEN + 1 then
+        log.error("exmodbus", "请求帧长度不足")
+        return nil, "请求帧长度不足"
+    end
+
+    -- 解析 MBAP 头(事务标识符(2)、协议标识符(2)、数据长度(2)、从站地址(1))
+    local transaction_id = string.unpack(">H", data, 1)
+    local protocol_id = string.unpack(">H", data, 3)
+    local length = string.unpack(">H", data, 5)
+    local slave_id = string.unpack("B", data, 7)
+
+    -- 检查数据长度是否与实际长度匹配
+    if #data ~= 6 + length then
+        log.error("exmodbus", "数据长度与实际长度不匹配")
+        return nil, "数据长度与实际长度不匹配"
+    end
+
+    -- 检查协议 ID(Modbus TCP 协议 ID 必须为 0)
+    if protocol_id ~= 0 then
+        log.error("exmodbus", "无效的协议 ID")
+        return nil, "无效的协议 ID"
+    end
+
+    -- 解析功能码
+    local func_code = string.unpack("B", data, 8)
+    local request = {
+        transaction_id = transaction_id,
+        protocol_id = protocol_id,
+        length = length,
+        slave_id = slave_id,
+        func_code = func_code,
+        reg_type = nil,
+        start_addr = nil,
+        reg_count = nil,
+        data = {},
+    }
+
+    -- 根据功能码解析请求内容
+    if func_code == exmodbus_ref.READ_COILS or func_code == exmodbus_ref.READ_DISCRETE_INPUTS then
+        -- 读线圈或离散输入
+        request.reg_type = func_code == exmodbus_ref.READ_COILS and exmodbus_ref.COIL_STATUS or exmodbus_ref.DISCRETE_INPUT_STATUS
+        request.start_addr = string.unpack(">H", data, 9)
+        request.reg_count = string.unpack(">H", data, 11)
+    elseif func_code == exmodbus_ref.READ_HOLDING_REGISTERS or func_code == exmodbus_ref.READ_INPUT_REGISTERS then
+        -- 读保持寄存器或输入寄存器
+        request.reg_type = func_code == exmodbus_ref.READ_HOLDING_REGISTERS and exmodbus_ref.HOLDING_REGISTER or exmodbus_ref.INPUT_REGISTER
+        request.start_addr = string.unpack(">H", data, 9)
+        request.reg_count = string.unpack(">H", data, 11)
+    elseif func_code == exmodbus_ref.WRITE_SINGLE_COIL then
+        -- 写单个线圈
+        request.reg_type = exmodbus_ref.COIL_STATUS
+        request.start_addr = string.unpack(">H", data, 9)
+        request.reg_count = 1
+        local value = string.unpack(">H", data, 11)
+        request.data = { [request.start_addr] = value == 0xFF00 and 1 or 0 }
+    elseif func_code == exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER then
+        -- 写单个寄存器
+        request.reg_type = exmodbus_ref.HOLDING_REGISTER
+        request.start_addr = string.unpack(">H", data, 9)
+        request.reg_count = 1
+        local value = string.unpack(">H", data, 11)
+        request.data = { [request.start_addr] = value }
+    elseif func_code == exmodbus_ref.WRITE_MULTIPLE_COILS then
+        -- 写多个线圈
+        request.reg_type = exmodbus_ref.COIL_STATUS
+        request.start_addr = string.unpack(">H", data, 9)
+        request.reg_count = string.unpack(">H", data, 11)
+        -- local byte_count = string.unpack("B", data, 13)
+        request.data = {}
+        for i = 0, request.reg_count - 1 do
+            local byte_pos = 13 + 1 + math.floor(i / 8)
+            local bit_pos = i % 8
+            local byte_value = string.unpack("B", data, byte_pos)
+            local bit_value = bit.band(byte_value, bit.lshift(1, bit_pos)) > 0 and 1 or 0
+            request.data[request.start_addr + i] = bit_value
+        end
+    elseif func_code == exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS then
+        -- 写多个寄存器
+        request.reg_type = exmodbus_ref.HOLDING_REGISTER
+        request.start_addr = string.unpack(">H", data, 9)
+        request.reg_count = string.unpack(">H", data, 11)
+        -- local byte_count = string.unpack("B", data, 13)
+        request.data = {}
+        for i = 0, request.reg_count - 1 do
+            local value = string.unpack(">H", data, 13 + 1 + i * 2)
+            request.data[request.start_addr + i] = value
+        end
+    else
+        log.error("exmodbus", "不支持的功能码:", func_code)
+    end
+
+    return request
+end
+
+-- 构建 Modbus TCP 响应帧(从站使用);
+local function build_tcp_response(request, user_return)
+    local slave_id = request.slave_id
+    local func_code = request.func_code
+
+    -- 用户返回异常码 -> 异常响应;
+    if type(user_return) == "number" then
+        local exception_code = user_return
+        local response_payload = string.char(slave_id, bit.bor(func_code, 0x80), exception_code)
+        
+        -- 构建完整的 TCP 响应帧
+        local length = #response_payload
+        local response = string.pack(">H", request.transaction_id) .. -- 事务 ID
+            string.pack(">H", 0) ..                                   -- 协议 ID
+            string.pack(">H", length) ..                              -- 长度
+            response_payload
+        
+        return response
+    end
+
+    -- 用户返回表 -> 正常响应;
+    if type(user_return) ~= "table" then
+        log.error("exmodbus", "从站回调必须返回 table 或 number,实际类型: ", type(user_return))
+        return nil
+    end
+
+    local response_payload = ""
+
+    -- 处理读线圈和读离散输入响应;
+    if func_code == exmodbus_ref.READ_COILS or func_code == exmodbus_ref.READ_DISCRETE_INPUTS then
+        local reg_count = request.reg_count
+
+        -- 校验 reg_count 是否有效;
+        if not reg_count or reg_count <= 0 then
+            log.error("exmodbus", "请求中 reg_count 无效")
+            return nil
+        end
+
+        local byte_count = math.ceil(reg_count / 8)
+        local values = {}
+
+        for i = 0, reg_count - 1 do
+            local addr = request.start_addr + i
+            local bit_val = user_return[addr]
+            if bit_val == nil then
+                log.error("exmodbus", "读线圈/离散输入回调未返回地址 ", addr, " 的数据")
+                return nil
+            end
+            if bit_val ~= 0 and bit_val ~= 1 then
+                log.error("exmodbus", "地址 ", addr, " 的值必须为 0 或 1,实际: ", bit_val)
+                return nil
+            end
+
+            local byte_idx = math.floor(i / 8)
+            if not values[byte_idx] then values[byte_idx] = 0 end
+            if bit_val == 1 then
+                values[byte_idx] = bit.bor(values[byte_idx], bit.lshift(1, i % 8))
+            end
+        end
+
+        response_payload = string.char(slave_id, func_code, byte_count)
+        for i = 0, byte_count - 1 do
+            response_payload = response_payload .. string.char(values[i] or 0)
+        end
+    -- 处理读保持寄存器和读输入寄存器响应;
+    elseif func_code == exmodbus_ref.READ_HOLDING_REGISTERS or func_code == exmodbus_ref.READ_INPUT_REGISTERS then
+        local reg_count = request.reg_count
+        -- 校验 reg_count 是否有效;
+        if not reg_count or reg_count <= 0 then
+            log.error("exmodbus", "请求中 reg_count 无效")
+            return nil
+        end
+
+        local values = ""
+        for i = 0, reg_count - 1 do
+            local addr = request.start_addr + i
+            local val = user_return[addr]
+            if val == nil then
+                log.error("exmodbus", "读保持寄存器/输入寄存器回调未返回地址 ", addr, " 的数据")
+                return nil
+            end
+            if type(val) ~= "number" or val ~= math.floor(val) or val < 0 or val > 65535 then
+                log.error("exmodbus", "地址 ", addr, " 的值必须为 0~65535 的整数,实际: ", val)
+                return nil
+            end
+            values = values .. string.char((val >> 8) & 0xFF, val & 0xFF)
+        end
+        response_payload = string.char(slave_id, func_code, #values) .. values
+    -- 处理写单个线圈响应;
+    elseif func_code == exmodbus_ref.WRITE_SINGLE_COIL then
+        local addr = request.start_addr
+        -- 校验 start_addr 是否有效;
+        if addr == nil then
+            log.error("exmodbus", "请求中 start_addr 无效")
+            return nil
+        end
+        local coil_val = (request.data and request.data[addr]) or 0
+        local resp_val = (coil_val ~= 0) and 0xFF00 or 0x0000
+        response_payload = string.char(slave_id, func_code) ..
+            string.char((addr >> 8) & 0xFF, addr & 0xFF,
+            (resp_val >> 8) & 0xFF, resp_val & 0xFF)
+    -- 处理写单个保持寄存器响应;
+    elseif func_code == exmodbus_ref.WRITE_SINGLE_HOLDING_REGISTER then
+        local addr = request.start_addr
+        -- 校验 start_addr 是否有效;
+        if addr == nil then
+            log.error("exmodbus", "请求中 start_addr 无效")
+            return nil
+        end
+        local reg_val = (request.data and request.data[addr]) or 0
+        -- 校验 reg_val 是否有效;
+        if type(reg_val) ~= "number" or reg_val ~= math.floor(reg_val) or reg_val < 0 or reg_val > 65535 then
+            log.error("exmodbus", "地址 ", addr, " 的值必须为 0~65535 的整数,实际: ", reg_val)
+            return nil
+        end
+        response_payload = string.char(slave_id, func_code) ..
+            string.char((addr >> 8) & 0xFF, addr & 0xFF,
+            (reg_val >> 8) & 0xFF, reg_val & 0xFF)
+    -- 处理写多个线圈/保持寄存器响应;
+    elseif func_code == exmodbus_ref.WRITE_MULTIPLE_COILS or func_code == exmodbus_ref.WRITE_MULTIPLE_HOLDING_REGISTERS then
+        local start_addr = request.start_addr
+        local reg_count = request.reg_count
+        -- 校验 start_addr 和 reg_count 是否有效;
+        if not start_addr or not reg_count or reg_count <= 0 then
+            log.error("exmodbus", "请求中 start_addr 或 reg_count 无效")
+            return nil
+        end
+        response_payload = string.char(slave_id, func_code) ..
+            string.char((start_addr >> 8) & 0xFF, start_addr & 0xFF,
+            (reg_count >> 8) & 0xFF, reg_count & 0xFF)
+    -- 处理未知功能码,视为错误;
+    else
+        log.error("exmodbus", "不支持的功能码,且未返回异常码: ", func_code)
+        return nil
+    end
+
+    -- 构建完整的 TCP 响应帧
+    local length = #response_payload  -- 长度包含从站ID
+    local response = string.pack(">H", request.transaction_id) .. -- 事务 ID
+        string.pack(">H", 0) ..                                   -- 协议 ID
+        string.pack(">H", length) ..                              -- 长度(包含从站ID)
+        response_payload                                          -- 从站ID + PDU数据
+
+    return response
+end
+
+-- TCP 从站接收数据处理函数;
+local function tcp_receiver(netc, instance)
+    -- 如果数据接收缓冲区还没有申请过空间,则先申请内存空间
+    if instance.recv_buff == nil then
+        instance.recv_buff = zbuff.create(1024)
+    end
+
+    -- 循环从内核的缓冲区读取接收到的数据
+    while true do
+        -- 从内核的缓冲区中读取数据到 instance.recv_buff 中
+        local succ, param = socket.rx(netc, instance.recv_buff)
+
+        -- 读取数据失败
+        if not succ then
+            log.info("exmodbus", "读取数据失败,已接收数据长度", param)
+            return false
+        end
+
+        -- 如果读取到了数据
+        if instance.recv_buff:used() > 0 then
+            -- log.info("exmodbus", "已接收数据长度", instance.recv_buff:used())
+            
+            -- 读取数据
+            local data = instance.recv_buff:query()
+            
+            -- 解析 TCP 请求帧
+            local request, err = parse_tcp_request(data)
+            if request then
+                -- 广播地址(0)不响应;
+                if request.slave_id == 0 then
+                    -- 调用回调以允许用户记录或处理广播命令(如写寄存器);
+                    if instance.slaveHandler then
+                        instance.slaveHandler(request)
+                        -- 注意:即使回调返回数据,也不发送响应;
+                    end
+                    -- 广播请求处理完毕,清除对应的报文数据
+                    local expected_len = request.length + MODBUS_TCP_HEADER_LEN - 1
+                    instance.recv_buff:del(0, expected_len)
+                    -- log.info("exmodbus", "广播请求处理完毕,清除报文长度:", expected_len)
+                    -- 广播请求处理完毕,不回复;
+                    break
+                end
+                if instance.slaveHandler then
+                    local user_return = instance.slaveHandler(request)
+                    local response = build_tcp_response(request, user_return)
+                    if response then
+                        libnet.tx(instance.TASK_NAME, 0, netc, response)
+                        sys.sendMsg(instance.TASK_NAME, socket.EVENT, 0)
+                    else
+                        log.error("exmodbus", "构建响应帧失败,从站地址:", request.slave_id)
+                    end
+                    
+                    -- 清除当前请求数据
+                    local expected_len = request.length + MODBUS_TCP_HEADER_LEN - 1
+                    instance.recv_buff:del(0, expected_len)
+                    -- log.info("exmodbus", "请求处理完毕,清除报文长度:", expected_len)
+                else
+                    log.warn("exmodbus", "收到主站请求,但未注册回调函数")
+                    -- 清除当前请求数据
+                    local expected_len = request.length + MODBUS_TCP_HEADER_LEN - 1
+                    instance.recv_buff:del(0, expected_len)
+                    log.info("exmodbus", "清除报文长度:", expected_len)
+                end
+            else
+                if err == "请求帧长度不足" then
+                    -- 请求帧长度不足,等待更多数据
+                    -- log.info("exmodbus", "请求帧长度不足,等待更多数据")
+                    break
+                elseif err == "数据长度与实际长度不匹配" then
+                    -- 数据长度与实际长度不匹配,清空缓冲区
+                    -- log.warn("exmodbus", "数据长度与实际长度不匹配,清空缓冲区")
+                    instance.recv_buff:del()
+                    break
+                elseif err == "协议 ID 错误" then
+                    -- 协议 ID 错误,清空缓冲区
+                    -- log.warn("exmodbus", "协议 ID 错误,清空缓冲区")
+                    instance.recv_buff:del()
+                    break
+                end
+            end
+        else
+            -- 没有数据可读
+            break
+        end
+    end
+
+    return true
+end
+
+local function tcp_slave_main_task_func(instance)
+    local netc = nil
+    local result, param
+
+    while true do
+        -- 创建 TCP 服务器
+        netc = socket.create(instance.adapter, instance.TASK_NAME)
+        if not netc then
+            log.error("exmodbus", "创建 TCP 服务器失败")
+            goto EXCEPTION_PROC
+        end
+
+        -- 配置服务器
+        result = socket.config(netc, instance.port)
+        if not result then
+            log.error("exmodbus", "配置 TCP 服务器失败")
+            goto EXCEPTION_PROC
+        end
+
+        -- 监听端口
+        result = libnet.listen(instance.TASK_NAME, 0, netc)
+        if not result then
+            log.error("exmodbus", "监听端口失败")
+            goto EXCEPTION_PROC
+        end
+
+        log.info("exmodbus", "TCP 从站已启动,监听端口:", instance.port)
+
+        -- 处理连接和数据
+        while true do
+            -- 处理接收数据
+            if not tcp_receiver(netc, instance) then
+                log.info("exmodbus", "接收数据处理失败")
+                break
+            end
+
+            -- 等待事件
+            result, param = libnet.wait(instance.TASK_NAME, 0, netc)
+            if not result then
+                log.info("exmodbus", "客户端断开连接")
+                break
+            end
+        end
+
+        -- 异常处理
+        ::EXCEPTION_PROC::
+
+        -- 关闭连接
+        if netc then
+            libnet.close(instance.TASK_NAME, 5000, netc)
+            socket.release(netc)
+            netc = nil
+        end
+
+        -- 等待 5 秒后重试
+        sys.wait(5000)
+    end
+end
+
+-- 创建一个新的实例;
+local function create(config, exmodbus, gen_request_id)
+    exmodbus_ref = exmodbus
+    gen_id_func = gen_request_id
+    local TASK_NAME = "exmodbus_tcp_task_"..gen_id_func()
+
+    -- 创建一个新的实例;
+    local instance = modbus:new(config, TASK_NAME)
+    -- 检查实例是否创建成功;
+    if not instance then
+        log.error("exmodbus", "创建 Modbus 实例失败")
+        return false
+    end
+
+    -- 启动任务
+    sys.taskInitEx(tcp_slave_main_task_func, TASK_NAME, nil, instance)
+
+    -- 返回实例;
+    return instance
+end
+
+function modbus:destroy()
+    -- 停止任务
+    sys.taskDel(self.TASK_NAME)
+    -- 释放缓冲区
+    if self.recv_buff then
+        self.recv_buff:free()
+        self.recv_buff = nil
+    end
+end
+
+-- 注册从站请求处理回调函数;
+function modbus:on(callback)
+    if type(callback) ~= "function" then
+        log.error("exmodbus", "on(callback) 的参数必须是一个函数")
+        return false
+    end
+    self.slaveHandler = callback
+    log.info("exmodbus", "已注册从站请求处理回调函数")
+    return true
+end
+
+return { create = create }

+ 1 - 1
script/libs/libfota.lua

@@ -138,7 +138,7 @@ fota升级
 @return nil 无返回值
 ]]
 function libfota.request(cbFnc,ota_url,storge_location, len, param1,ota_port,libfota_timeout,server_cert, client_cert, client_key, client_password, show_otaurl)
-    sys.taskInit(fota_task, cbFnc,storge_location, len, param1,ota_url, ota_port,libfota_timeout or 30000,server_cert, client_cert, client_key, client_password, show_otaurl)
+    sys.taskInit(fota_task, cbFnc,storge_location, len, param1,ota_url, ota_port,libfota_timeout or 180000,server_cert, client_cert, client_key, client_password, show_otaurl)
 end
 
 return libfota