Browse Source

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

马亚丹 5 months ago
parent
commit
5c6e384a15
84 changed files with 4950 additions and 715 deletions
  1. 0 1
      components/airlink/binding/luat_lib_airlink.c
  2. 3 1
      components/airlink/include/luat_airlink.h
  3. 1 1
      components/airlink/src/devinfo/luat_airlink_devinfo_wlan.c
  4. 0 1
      components/airlink/src/driver/luat_airlink_drv_gpio.c
  5. 0 2
      components/airlink/src/driver/luat_airlink_drv_mobile.c
  6. 82 6
      components/airlink/src/exec/luat_airlink_cmd_exec_gpio.c
  7. 0 2
      components/airlink/src/exec/luat_airlink_cmd_exec_info.c
  8. 0 3
      components/airlink/src/exec/luat_airlink_cmd_exec_mobile.c
  9. 33 2
      components/airlink/src/luat_airlink.c
  10. 4 3
      components/airlink/src/luat_airlink_cmds.c
  11. 1 2
      components/airlink/src/luat_airlink_fota.c
  12. 46 154
      components/airlink/src/task/luat_airlink_spi_master_task.c
  13. 15 11
      components/airlink/src/task/luat_airlink_task.c
  14. 10 20
      components/bluetooth/drv/luat_drv_ble_port.c
  15. 0 2
      components/bluetooth/drv/luat_drv_bt_port.c
  16. 0 2
      components/drv/src/luat_drv_wlan.c
  17. 2 2
      components/gmssl/src/gf128.c
  18. 1 1
      components/gmssl/src/sm2_elgamal.c
  19. 0 6
      components/gtfont/luat_gtfont.c
  20. 3 0
      components/lvgl/binding/luat_lib_lvgl.c
  21. 57 2
      components/lvgl/binding/luat_lib_lvgl_font.c
  22. 3 0
      components/lvgl/binding/luat_lvgl_font.h
  23. 162 0
      components/lvgl/font/lv_font_ex.c
  24. 1 1
      components/lvgl/lv_conf.h
  25. 2 1
      components/mobile/luat_lib_mobile.c
  26. 0 1
      components/mreport/src/luat_mreport.c
  27. 1 0
      components/network/netdrv/binding/luat_lib_netdrv.c
  28. 10 1
      components/network/netdrv/include/luat_netdrv_napt.h
  29. 3 3
      components/network/netdrv/src/luat_netdrv_napt.c
  30. 6 3
      components/sfud/luat_lib_sfud.c
  31. 4 4
      components/tp/luat_tp.c
  32. 2 1
      components/u8g2/u8x8_d_stdio.c
  33. 1 1
      components/wlan/luat_lib_wlan.c
  34. 112 8
      lua/src/liolib.c
  35. 0 9
      lua/src/loslib.c
  36. 22 22
      lua/src/lstrlib_exts.c
  37. 1 1
      luat/include/luat_base.h
  38. 13 9
      luat/modules/luat_lib_crypto.c
  39. 0 8
      luat/modules/luat_lib_rtc.c
  40. 5 5
      luat/modules/luat_lib_spi.c
  41. 2 0
      luat/modules/luat_lib_timer.c
  42. 4 5
      module/Air780E/demolib/adxl34x.lua
  43. 4 4
      module/Air780E/demolib/spl06.lua
  44. 81 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirMICROSD_1010/http_download_file.lua
  45. 90 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirMICROSD_1010/main.lua
  46. 52 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirMICROSD_1010/pins_Air780EHM.json
  47. 190 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirMICROSD_1010/readme.md
  48. 333 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirMICROSD_1010/tfcard_app.lua
  49. 16 17
      module/Air780EHM_Air780EHV_Air780EGH/demo/tf_card/readme.md
  50. 4 5
      module/Air780EHM_Air780EHV_Air780EGH/demolib/adxl34x.lua
  51. 4 4
      module/Air780EHM_Air780EHV_Air780EGH/demolib/spl06.lua
  52. 89 89
      module/Air780EPM/demo/780EPM视频教程示例代码/25_RS485实战演练/Radar_485.lua
  53. 4 5
      module/Air780EPM/demolib/adxl34x.lua
  54. 4 4
      module/Air780EPM/demolib/spl06.lua
  55. 78 0
      module/Air8000/demo/accessory_board/AirMICROSD_1010/http_download_file.lua
  56. 96 0
      module/Air8000/demo/accessory_board/AirMICROSD_1010/http_upload_file.lua
  57. 92 0
      module/Air8000/demo/accessory_board/AirMICROSD_1010/main.lua
  58. 54 0
      module/Air8000/demo/accessory_board/AirMICROSD_1010/pins_Air8000.json
  59. 238 0
      module/Air8000/demo/accessory_board/AirMICROSD_1010/readme.md
  60. 331 0
      module/Air8000/demo/accessory_board/AirMICROSD_1010/tfcard_app.lua
  61. 14 14
      module/Air8000/demo/netdrv/ch390/lan_TCP/main.lua
  62. 14 14
      module/Air8000/demo/netdrv/ch390/wan_TCP/main.lua
  63. 0 200
      module/Air8000/demo/record/main.lua
  64. 98 0
      module/Air8000/demo/tf_card/http_upload_file.lua
  65. 6 4
      module/Air8000/demo/tf_card/main.lua
  66. 58 2
      module/Air8000/demo/tf_card/readme.md
  67. 27 0
      module/Air8000/demo/wifi_location/check_wifi.lua
  68. 1 4
      module/Air8000/demo/wifi_location/main.lua
  69. 0 5
      module/Air8000/demo/wlan/AP/main.lua
  70. 0 3
      module/Air8000/demo/wlan/Power_Save/main.lua
  71. 0 3
      module/Air8000/demo/wlan/STA/main.lua
  72. 0 3
      module/Air8000/demo/wlan/wifi_configuration_network_by_ap/main.lua
  73. 6 9
      module/Air8000/demo/wlan/wifi_scan/main.lua
  74. 4 5
      module/Air8000/demolib/adxl34x.lua
  75. 4 4
      module/Air8000/demolib/spl06.lua
  76. 31 0
      module/Air8000/project/整机开发板出厂工程/readme.md
  77. 4 5
      module/Air8101/demolib/adxl34x.lua
  78. 4 4
      module/Air8101/demolib/spl06.lua
  79. 1740 0
      script/libs/exeasyui.lua
  80. 1 1
      script/libs/exfotawifi.lua
  81. 212 0
      script/libs/exlcd.lua
  82. 20 0
      script/libs/exnetif.lua
  83. 308 0
      script/libs/extp.lua
  84. 26 0
      script/libs/screen_data_table.lua

+ 0 - 1
components/airlink/binding/luat_lib_airlink.c

@@ -33,7 +33,6 @@
 extern airlink_statistic_t g_airlink_statistic;
 extern uint32_t g_airlink_spi_task_mode;
 extern uint32_t g_airlink_pause;
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
 extern luat_airlink_irq_ctx_t g_airlink_wakeup_irq_ctx;
 
 /*

+ 3 - 1
components/airlink/include/luat_airlink.h

@@ -79,7 +79,7 @@ void luat_airlink_task_start(void);
 void luat_airlink_print_buff(const char* tag, uint8_t* buff, size_t len);
 void luat_airlink_on_data_recv(uint8_t *data, size_t len);
 
-void airlink_wait_for_slave_reply(size_t timeout_ms);
+void airlink_wait_for_slave_ready(size_t timeout_ms);
 void airlink_transfer_and_exec(uint8_t *txbuff, uint8_t *rxbuff);
 void airlink_wait_and_prepare_data(uint8_t *txbuff);
 
@@ -318,6 +318,8 @@ int luat_airlink_has_wifi(void);
 
 uint32_t luat_airlink_sversion(void);
 
+extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
+
 typedef void (*AIRLINK_DEV_INFO_UPDATE_CB)(void);
 
 #ifdef TYPE_EC718M

+ 1 - 1
components/airlink/src/devinfo/luat_airlink_devinfo_wlan.c

@@ -143,7 +143,7 @@ void luat_airlink_devinfo_init(AIRLINK_DEV_INFO_UPDATE_CB cb)
 {
     send_devinfo_update_evt = cb;
     g_airlink_self_dev_info.tp = 0x01;
-    uint32_t fw_version = 16;
+    uint32_t fw_version = 17;
     memcpy(g_airlink_self_dev_info.wifi.version, &fw_version, sizeof(uint32_t));   // 版本
     g_airlink_wlan_evt_cb = wifi_evt_handler;
     send_devinfo_update_evt();

+ 0 - 1
components/airlink/src/driver/luat_airlink_drv_gpio.c

@@ -54,7 +54,6 @@ int luat_airlink_drv_gpio_set(int pin, int level) {
 
 luat_rtos_semaphore_t g_drv_gpio_sem;
 uint64_t g_drv_gpio_input_level = 0;
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
 int luat_airlink_drv_gpio_get(int pin, int* val) {
     int ret = 0;
     if (pin >= 128) {

+ 0 - 2
components/airlink/src/driver/luat_airlink_drv_mobile.c

@@ -20,8 +20,6 @@
 #define LLOGD(...) 
 luat_airlink_mobile_evt_cb g_airlink_mobile_evt_cb;
 
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
-
 int luat_airlink_mobile_event_callback(LUAT_MOBILE_EVENT_E event, uint8_t index, uint8_t status, void* ptr) {
 	if (g_airlink_mobile_evt_cb) {
 		g_airlink_mobile_evt_cb(event, index, status, ptr);

+ 82 - 6
components/airlink/src/exec/luat_airlink_cmd_exec_gpio.c

@@ -9,6 +9,9 @@
 #include "luat_pm.h"
 #include "luat_gpio.h"
 #include "luat_airlink.h"
+#include "luat_msgbus.h"
+// #include "lstate.h"
+#include "luat_mcu.h"
 
 #define LUAT_LOG_TAG "airlink"
 #include "luat_log.h"
@@ -16,14 +19,73 @@
 #undef LLOGD
 #define LLOGD(...) 
 
+static luat_rtos_queue_t evt_queue;
+static luat_rtos_task_handle airlink_gpio_irq_cb_handle;
 // 前8字节是指令id, 用于返回执行结果, 待定.
 
-static int airlink_gpio_irq_cb(int pin, void* args) {
+__AIRLINK_CODE_IN_RAM__ static int luat_airlink_gpio_irq_cb_task(void *param) {
+    //先接收
+    // LLOGD("处理线程启动");
+    luat_event_t event = {0};
+    luat_airlink_cmd_t* ptr = NULL;
+    // size_t len = 0;
+    luat_rtos_task_sleep(2);
+    while (1) {
+        event.id = 0;
+        // luat_rtos_event_recv(airlink_gpio_irq_cb_handle,1, &event, NULL, LUAT_WAIT_FOREVER);
+        luat_rtos_queue_recv(evt_queue, &event, sizeof(event) ,LUAT_WAIT_FOREVER);
+        if (event.id == 1) { // 收到数据了, 马上处理
+            // LLOGD("收到数据");
+
+            int pin = event.param1;
+            int args = event.param2;
+            ptr = (void*)event.param1;
+            // len = event.param2;
+            if (ptr == NULL) {
+                LLOGW("空指令!");
+                continue;
+            }
+
+            //再发送
+            uint64_t luat_airlink_next_cmd_id = luat_airlink_get_next_cmd_id();
+            airlink_queue_item_t item = {
+                .len = sizeof(luat_gpio_t) + sizeof(luat_airlink_cmd_t) + 8
+            };
+            luat_airlink_cmd_t* cmd = luat_airlink_cmd_new(0x311, sizeof(luat_gpio_t) + 8);
+            if (cmd == NULL) { //  检查命令创建是否成功
+                return -101;
+            }
+            memcpy(cmd->data, &luat_airlink_next_cmd_id, 8);
+            uint8_t* data = cmd->data + 8;
+            data[0] = event.param1;
+            data[1] = event.param2;
+            memcpy(cmd->data + 8, data, 2);
+            // LLOGD("GPIO中断回调!!!参数一:%d 参数二:%d",data[0], data[1]);
+            item.cmd = cmd;
+            luat_airlink_queue_send(LUAT_AIRLINK_QUEUE_CMD, &item);
+        }
+    }
     return 0;
 }
+int airlink_gpio_irq_cb(int pin, void* args) {
+    int res = 1;
+    uint8_t params[2];
+    params[0] = pin;
+    params[1] = luat_gpio_get(params[0]);
+    luat_event_t event = {0};
+    event.id = 1;
+    event.param1 = params[0];
+    event.param2 = params[1];
+    // res = luat_rtos_event_send(airlink_gpio_irq_cb_handle, 1, event.param1, event.param2, 0, 0);
+    luat_rtos_queue_send(evt_queue, &event, sizeof(event), LUAT_WAIT_FOREVER);
+    if (res != 0) {
+        LLOGW("airlink发送消息失败!!! %d", res);
+    }
+    return res;
+}
 
 int luat_airlink_cmd_exec_gpio_setup(luat_airlink_cmd_t* cmd, void* userdata) {
-    luat_gpio_t conf = {0};
+    luat_gpio_t conf = {0}; //  定义并初始化一个luat_gpio_t类型的结构体变量conf,初始值为0
     // 后面是配置参数,是luat_gpio_t结构体
     memcpy(&conf, cmd->data + 8, sizeof(luat_gpio_t));
     if (conf.pin >= 128) {
@@ -36,16 +98,23 @@ int luat_airlink_cmd_exec_gpio_setup(luat_airlink_cmd_t* cmd, void* userdata) {
         return 0;
     }
     #endif
-    LLOGD("收到GPIO配置指令!!! pin %d", conf.pin);
+    LLOGD("收到GPIO配置指令!!! pin %d", conf.pin); //  记录日志:收到GPIO配置指令,并打印引脚号
     if (conf.mode == Luat_GPIO_IRQ) {
+        if (evt_queue == NULL) {
+            luat_rtos_queue_create(&evt_queue, 1 * 1024, sizeof(luat_event_t));
+        }
+        //中断任务
+        if (airlink_gpio_irq_cb_handle == NULL) {
+            luat_rtos_task_create(&airlink_gpio_irq_cb_handle, 1 * 1024, 55, "airlink", luat_airlink_gpio_irq_cb_task, NULL, 1024);
+        }
+        //中断回调函数
         conf.irq_cb = airlink_gpio_irq_cb;
-        conf.irq_args = NULL;
+        conf.irq_args = NULL; 
     }
     else {
         conf.irq_cb = NULL;
     }
     int ret = luat_gpio_setup(&conf);
-    LLOGD("收到GPIO配置指令!!! pin %d ret %d", conf.pin, ret);
     return ret;
 }
 
@@ -60,7 +129,14 @@ int luat_airlink_cmd_exec_gpio_set(luat_airlink_cmd_t* cmd, void* userdata) {
     LLOGD("收到GPIO设置指令!!! pin %d level %d ret %d", params[0], params[1], ret);
     return ret;
 }
-
+int luat_airlink_cmd_exec_gpio_irq_cb(luat_airlink_cmd_t* cmd, void* userdata) {
+    uint8_t params[2];
+    memcpy(params, cmd->data + 8, 2);
+    params[0] += 128;
+    // LLOGD("收到GPIO_irq_cb设置指令!!! pin %d level %d", params[0], params[1]);
+    int ret = luat_gpio_irq_default(params[0], params[1]);
+    return ret;
+}
 
 int luat_airlink_cmd_exec_gpio_get(luat_airlink_cmd_t* reqcmd, void* userdata) {
     LLOGD("收到gpio.get指令!!!");

+ 0 - 2
components/airlink/src/exec/luat_airlink_cmd_exec_info.c

@@ -21,8 +21,6 @@
 #define LUAT_LOG_TAG "airlink"
 #include "luat_log.h"
 
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
-
 __AIRLINK_CODE_IN_RAM__ int luat_airlink_cmd_exec_dev_info(luat_airlink_cmd_t* cmd, void* userdata) {
     luat_airlink_dev_info_t* dev = cmd->data;
     luat_netdrv_t* drv = NULL;

+ 0 - 3
components/airlink/src/exec/luat_airlink_cmd_exec_mobile.c

@@ -15,9 +15,6 @@
 #define LUAT_LOG_TAG "airlink"
 #include "luat_log.h"
 
-
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
-
 int luat_airlink_cmd_exec_mobile_imei(luat_airlink_cmd_t* cmd, void* userdata) {
     int ret = 0;
     uint8_t index = cmd->data[8];

+ 33 - 2
components/airlink/src/luat_airlink.c

@@ -29,7 +29,6 @@ luat_rtos_queue_t airlink_ippkg_queue;
 extern int luat_airlink_start_slave(void);
 extern int luat_airlink_start_master(void);
 extern int luat_airlink_start_uart(void);
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
 luat_airlink_newdata_notify_cb g_airlink_newdata_notify_cb;
 luat_airlink_spi_conf_t g_airlink_spi_conf;
 airlink_statistic_t g_airlink_statistic;
@@ -532,12 +531,17 @@ int luat_airlink_result_send(uint8_t* buff, size_t len) {
 void luat_airlink_wait_ready(void) {
     // Air8000硬等最多200ms, 梁健要加的, 有问题找他
     if (luat_airlink_has_wifi()) {
+        #if defined(LUAT_USE_AIRLINK) && defined(LUAT_USE_AIRLINK_AUTO_MASTER)
+	    LLOGD("open airlink for air8000s");
+	    extern void luat_airlink_master_autostart(void);
+	    luat_airlink_master_autostart();
+        #endif
         // LLOGD("等待Air8000s启动");
 	    size_t count = 0;
         uint64_t tnow = luat_mcu_tick64_ms();
 	    #define AIRLINK_WAIT_MS (5)
         extern uint64_t g_airlink_last_cmd_timestamp;
-	    while (g_airlink_last_cmd_timestamp == 0 && count < 200) {
+	    while (g_airlink_last_cmd_timestamp == 0 && count < 500) {
 		    luat_rtos_task_sleep(AIRLINK_WAIT_MS);
 		    count += AIRLINK_WAIT_MS;
 	    }
@@ -585,3 +589,30 @@ uint32_t luat_airlink_sversion(void) {
     }
     return version;
 }
+
+static void netdrv_airlink_setup(void* params) {
+	(void)params;
+	// 自动新增STA和AP的netdrv
+	// 自动新增STA和AP的netdrv
+	luat_netdrv_conf_t conf = {0};
+	conf.impl = 64;
+	// 注册STA
+	conf.id = NW_ADAPTER_INDEX_LWIP_WIFI_STA;
+	luat_netdrv_setup(&conf);
+
+	// 然后注册AP
+	conf.id = NW_ADAPTER_INDEX_LWIP_WIFI_AP;
+	luat_netdrv_setup(&conf);
+}
+
+void luat_airlink_master_autostart(void) {
+	if (!luat_airlink_has_wifi()){
+		return;
+	}
+	
+	tcpip_callback_with_block(netdrv_airlink_setup, NULL, 1);
+	// 初始化AirLink
+	luat_airlink_init();
+	luat_airlink_task_start();
+	luat_airlink_start(1); // SPI master模式
+}

+ 4 - 3
components/airlink/src/luat_airlink_cmds.c

@@ -58,9 +58,10 @@ CMD_DEFINE(wlan_set_ps);
 CMD_DEFINE(gpio_setup);
 CMD_DEFINE(gpio_set);
 CMD_DEFINE(gpio_get);
-CMD_DEFINE(gpio_get_result);
 CMD_DEFINE(gpio_driver_yhm27xx);
 CMD_DEFINE(gpio_driver_yhm27xx_reqinfo);
+
+CMD_DEFINE(gpio_get_result);
 CMD_DEFINE(gpio_irq_cb);
 
 // UART指令, 0x400开始
@@ -136,8 +137,8 @@ __AIRLINK_CODE_IN_RAM__ const luat_airlink_cmd_reg_t airlink_cmds[] = {
     CMD_REG(0x305, gpio_driver_yhm27xx_reqinfo),
 #endif
 #ifdef LUAT_USE_AIRLINK_EXEC_GPIO_RESP
-    CMD_REG(0x310, gpio_get_result),
-    // CMD_REG(0x311, gpio_irq_cb),
+    CMD_REG(0x310, gpio_get_result), //  注册命令0x310,回调函数为gpio_get_result,用于获取GPIO操作结果
+    CMD_REG(0x311, gpio_irq_cb),
 #endif
 
 #ifdef LUAT_USE_AIRLINK_EXEC_UART

+ 1 - 2
components/airlink/src/luat_airlink_fota.c

@@ -9,7 +9,6 @@
 #include "luat_log.h"
 
 luat_airlink_fota_t *g_airlink_fota;
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
 
 int luat_airlink_fota_init(luat_airlink_fota_t *ctx)
 {
@@ -61,7 +60,7 @@ static void pack_and_send(uint16_t cmd_id, uint8_t *data, size_t len)
     luat_airlink_data_pack(cmd, sizeof(luat_airlink_cmd_t) + len, s_airlink_fota_txbuff);
     // LLOGD("发送sFOTA %d %d", cmd->cmd, cmd->len);
     airlink_transfer_and_exec(s_airlink_fota_txbuff, s_airlink_fota_rxbuff);
-    airlink_wait_for_slave_reply(5000); // 等待从机回复完成接收
+    airlink_wait_for_slave_ready(5000); // 等待从机回复完成接收
 
     memset(s_airlink_fota_txbuff, 0, AIRLINK_SFOTA_BUFF_SIZE);
 }

+ 46 - 154
components/airlink/src/task/luat_airlink_spi_master_task.c

@@ -28,25 +28,24 @@
 #include "platform_def.h"
 #endif
 
+#ifndef __USER_FUNC_IN_RAM__
+#define __USER_FUNC_IN_RAM__
+#endif
+
 extern airlink_statistic_t g_airlink_statistic;
 extern uint32_t g_airlink_pause;
 
 // static uint8_t start;
 // static uint8_t slave_rdy;
 static uint8_t thread_rdy;
-static uint8_t spi_init_done = 0;
+static uint8_t spi_rdy;
 static luat_rtos_task_handle spi_task_handle;
 
-
-#if defined(LUAT_USE_AIRLINK_EXEC_MOBILE)
-extern luat_airlink_dev_info_t g_airlink_self_dev_info;
-#endif
 static uint8_t basic_info[256];
 
 // static uint32_t is_waiting_queue = 0;
 
 static luat_rtos_queue_t evt_queue;
-static luat_rtos_queue_t rdy_evt_queue;  // 专门用于RDY事件(id=6)的队列
 
 extern luat_airlink_irq_ctx_t g_airlink_irq_ctx;
 extern luat_airlink_irq_ctx_t g_airlink_wakeup_irq_ctx;
@@ -54,26 +53,7 @@ extern luat_airlink_irq_ctx_t g_airlink_wakeup_irq_ctx;
 luat_airlink_irq_ctx_t g_airlink_irq_ctx;
 luat_airlink_irq_ctx_t g_airlink_wakeup_irq_ctx;
 
-// RDY引脚中断等待相关变量
-static volatile uint8_t rdy_ready_flag = 0;           // RDY就绪标志
-
-// RDY引脚下降沿中断处理函数,设置就绪标志并发送事件
-__AIRLINK_CODE_IN_RAM__ static int rdy_pin_irq_handler(void* param)
-{
-    // 设置RDY就绪标志
-    if (rdy_evt_queue == NULL) {
-        return 0;
-    }
-    rdy_ready_flag = 1;
-    // 发送通知事件,告知任务RDY已就绪
-    luat_event_t evt = {.id = 6};
-    luat_rtos_queue_send(rdy_evt_queue, &evt, sizeof(evt), 0);
-    
-    // LLOGD("RDY中断触发,设置就绪标志");
-    return 0;
-}
-
-__AIRLINK_CODE_IN_RAM__ static int slave_irq_cb(void *data, void *args)
+__USER_FUNC_IN_RAM__ static int slave_irq_cb(void *data, void *args)
 {
     uint32_t len = 0;
     luat_rtos_queue_get_cnt(evt_queue, &len);
@@ -90,7 +70,7 @@ __AIRLINK_CODE_IN_RAM__ static int slave_irq_cb(void *data, void *args)
     return 0;
 }
 
-__AIRLINK_CODE_IN_RAM__ static int wakeup_irq_cb(void *data, void *args)
+__USER_FUNC_IN_RAM__ static int wakeup_irq_cb(void *data, void *args)
 {
     // g_airlink_wakeup_irq_ctx.enable = 0;
     LLOGI("触发唤醒wifi wakeup_irq_cb");
@@ -98,7 +78,7 @@ __AIRLINK_CODE_IN_RAM__ static int wakeup_irq_cb(void *data, void *args)
 }
 
 
-__AIRLINK_CODE_IN_RAM__ static void on_newdata_notify(void)
+__USER_FUNC_IN_RAM__ static void on_newdata_notify(void)
 {
     luat_event_t evt = {.id = 3};
     luat_rtos_queue_send(evt_queue, &evt, sizeof(evt), 0);
@@ -106,8 +86,9 @@ __AIRLINK_CODE_IN_RAM__ static void on_newdata_notify(void)
 
 void luat_airlink_spi_master_pin_setup(void)
 {
-    if (spi_init_done)
+    if (spi_rdy)
         return;
+    spi_rdy = 1;
     if (g_airlink_spi_conf.cs_pin == 0)
     {
         // if (g_airlink_spi_conf.spi_id == 0) {
@@ -135,35 +116,23 @@ void luat_airlink_spi_master_pin_setup(void)
         .CPHA = 1,
         .CPOL = 1,
         .dataw = 8,
-        .bit_dict = 1, // MSB, 大部分平台也只支持MSB
+        .bit_dict = 0,
         .master = 1,
         .mode = 1, // mode设置为1,全双工
         .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);
 
     luat_spi_setup(&spi_conf);
     luat_gpio_cfg_t gpio_cfg = {0};
 
     // 从机准备好脚
-    // luat_gpio_set_default_cfg(&gpio_cfg);
-    // gpio_cfg.pin = AIRLINK_SPI_RDY_PIN;
-    // gpio_cfg.mode = LUAT_GPIO_INPUT;
-    // gpio_cfg.irq_type = LUAT_GPIO_FALLING_IRQ;
-    // gpio_cfg.pull = 0;
-    // luat_gpio_open(&gpio_cfg);
-
-    // 设置RDY引脚中断,
     luat_gpio_set_default_cfg(&gpio_cfg);
     gpio_cfg.pin = AIRLINK_SPI_RDY_PIN;
-    gpio_cfg.mode = LUAT_GPIO_IRQ;
-    gpio_cfg.irq_type = LUAT_GPIO_FALLING_IRQ;  // 下降沿中断
-    gpio_cfg.pull = LUAT_GPIO_PULLUP;
-    gpio_cfg.irq_cb = rdy_pin_irq_handler;
+    gpio_cfg.mode = LUAT_GPIO_INPUT;
+    gpio_cfg.irq_type = LUAT_GPIO_FALLING_IRQ;
+    gpio_cfg.pull = 0;
     luat_gpio_open(&gpio_cfg);
-    if (g_airlink_debug) {
-        LLOGD("RDY引脚中断模式已启用");
-    }
 
     // CS片选脚
     luat_gpio_set_default_cfg(&gpio_cfg);
@@ -183,10 +152,9 @@ void luat_airlink_spi_master_pin_setup(void)
         gpio_cfg.irq_cb = slave_irq_cb;
         luat_gpio_open(&gpio_cfg);
     }
-    spi_init_done = 1;
 }
 
-__AIRLINK_CODE_IN_RAM__ static void record_statistic(luat_event_t event)
+__USER_FUNC_IN_RAM__ static void record_statistic(luat_event_t event)
 {
     switch (event.id)
     {
@@ -212,26 +180,15 @@ static uint64_t tnow = 0;
 extern void airlink_sfota_exec();
 static uint8_t slave_is_irq_ready = 0;
 
-__AIRLINK_CODE_IN_RAM__ void airlink_transfer_and_exec(uint8_t *txbuff, uint8_t *rxbuff)
+__USER_FUNC_IN_RAM__ void airlink_transfer_and_exec(uint8_t *txbuff, uint8_t *rxbuff)
 {
     // 清除link
     memset(&s_link, 0, sizeof(airlink_link_data_t));
     airlink_link_data_t *link = NULL;
 
     g_airlink_statistic.tx_pkg.total++;
-    //luat_spi_lock(MASTER_SPI_ID);
-    // 拉低片选, 准备发送数据
-    luat_gpio_set(AIRLINK_SPI_CS_PIN, 0);
-    // 发送数据
-    #ifdef TYPE_EC718M
-    extern void SPI_FastTransfer(uint8_t SpiID, const uint8_t *TxData, uint8_t *RxData, uint32_t Len);
-    SPI_FastTransfer(MASTER_SPI_ID, txbuff, rxbuff, TEST_BUFF_SIZE);
-    #else
     luat_spi_transfer(MASTER_SPI_ID, (const char *)txbuff, TEST_BUFF_SIZE, (char *)rxbuff, TEST_BUFF_SIZE);
-    #endif
-    // 拉高片选, 结束发送
     luat_gpio_set(AIRLINK_SPI_CS_PIN, 1);
-    //luat_spi_unlock(MASTER_SPI_ID);
     // luat_airlink_print_buff("RX", rxbuff, 32);
     // 对接收到的数据进行解析
     link = luat_airlink_data_unpack(rxbuff, TEST_BUFF_SIZE);
@@ -252,62 +209,36 @@ __AIRLINK_CODE_IN_RAM__ void airlink_transfer_and_exec(uint8_t *txbuff, uint8_t
     }
 }
 
-__AIRLINK_CODE_IN_RAM__ void airlink_wait_for_slave_reply(size_t timeout_ms)
+__USER_FUNC_IN_RAM__ void airlink_wait_for_slave_ready(size_t timeout_ms)
 {
-    // 低速发送情况下:直接检查RDY flag
-    if (rdy_ready_flag) {
-        rdy_ready_flag = 0;  // 使用完后清除标志
-        // LLOGD("RDY已就绪,快速通过");
-        return;
-    }
-
-    // 高速发送情况下:RDY flag未就绪,使用事件等待机制等待RDY flag好了
-    uint32_t start_time = luat_mcu_tick64_ms();
-    uint32_t remaining_timeout = timeout_ms;
-    
-    while (remaining_timeout > 0) {
-        // 等待RDY就绪事件
-        luat_event_t event = {0};
-        int ret = luat_rtos_queue_recv(rdy_evt_queue, &event, sizeof(luat_event_t), remaining_timeout);
-        
-        if (ret == 0 && event.id == 6) {
-            // 收到RDY事件,检查flag(中断中已设置)
-            if (rdy_ready_flag) {
-                rdy_ready_flag = 0;  // 使用完后清除标志
-                uint32_t elapsed = luat_mcu_tick64_ms() - start_time;
-                if (elapsed > 1) {
-                    // 等待时间超过1ms, 计入统计
-                    g_airlink_statistic.wait_rdy.total++;
-                }
-                // LLOGD("RDY事件触发,flag已就绪");
-                return;
-            } else {
-                if (g_airlink_debug) {
-                    // 一般是之前慢速接收没有消耗队列信息, 导致RDY事件没有被处理,会在这里被消耗掉,然后等待真正有效的rdy时刻
-                    LLOGW("收到RDY事件但flag未设置,继续等待");
+    luat_gpio_set(AIRLINK_SPI_CS_PIN, 0); // 拉低片选, 等待从机就绪
+    int tmpval = 0;
+    for (size_t i = 0; i < timeout_ms; i++)
+    {
+        tmpval = luat_gpio_get(AIRLINK_SPI_RDY_PIN);
+        if (tmpval == 1)
+        {
+            g_airlink_statistic.wait_rdy.total++;
+            if (g_airlink_debug)
+            {
+                tnow = luat_mcu_tick64_ms();
+                if (tnow - warn_slave_no_ready > 1000)
+                {
+                    warn_slave_no_ready = tnow;
+                    LLOGD("从机未就绪,等1ms");
                 }
             }
+            luat_rtos_task_sleep(1);
+            continue;
         }
-        
-        // 更新剩余超时时间
-        uint32_t elapsed = luat_mcu_tick64_ms() - start_time;
-        if (elapsed >= timeout_ms) {
-            break;
-        }
-        remaining_timeout = timeout_ms - elapsed;
-    }
-    
-    // 超时处理
-    g_airlink_statistic.wait_rdy.total++;
-    if (remaining_timeout == 0) {
-        g_airlink_statistic.wait_rdy.err++;
-        LLOGW("等待RDY超时: timeout=%dms, flag=%d", (int)timeout_ms, rdy_ready_flag);
+        // LLOGD("从机已就绪!! %s %s", __DATE__, __TIME__);
+        break;
     }
 }
 
 static int queue_emtry_counter; // 上一个循环里, 是否待发送的数据, 如果有, 则需要立即发送
 
-__AIRLINK_CODE_IN_RAM__ void airlink_wait_and_prepare_data(uint8_t *txbuff)
+__USER_FUNC_IN_RAM__ void airlink_wait_and_prepare_data(uint8_t *txbuff)
 {
     luat_event_t event = {0};
     airlink_queue_item_t item = {0};
@@ -390,15 +321,12 @@ __AIRLINK_CODE_IN_RAM__ void airlink_wait_and_prepare_data(uint8_t *txbuff)
     else
     {
         // LLOGD("填充PING数据");
-        #if defined(LUAT_USE_AIRLINK_EXEC_MOBILE)
-        memcpy(basic_info + sizeof(luat_airlink_cmd_t), &g_airlink_self_dev_info, sizeof(g_airlink_self_dev_info));
-        #endif
         luat_airlink_data_pack(basic_info, sizeof(basic_info), txbuff);
         queue_emtry_counter ++;
     }
 }
 
-__AIRLINK_CODE_IN_RAM__ static void on_link_data_notify(airlink_link_data_t* link) {
+__USER_FUNC_IN_RAM__ static void on_link_data_notify(airlink_link_data_t* link) {
     memset(&link->flags, 0, sizeof(uint32_t));
     if (g_airlink_irq_ctx.enable) {
         link->flags.irq_ready = 1;
@@ -406,38 +334,18 @@ __AIRLINK_CODE_IN_RAM__ static void on_link_data_notify(airlink_link_data_t* lin
     }
 }
 
-#if defined(LUAT_USE_AIRLINK_EXEC_MOBILE)
-static void send_devinfo_update_evt(void) {
-    airlink_queue_item_t item = {0};
-    // 发送空消息, 会自动转为devinfo消息
-    luat_airlink_queue_send(LUAT_AIRLINK_QUEUE_CMD, &item); 
-}
-#endif
 
-__AIRLINK_CODE_IN_RAM__ static void spi_master_task(void *param)
+__USER_FUNC_IN_RAM__ static void spi_master_task(void *param)
 {
     // int i;
     // luat_event_t event = {0};
-    #if defined(LUAT_USE_AIRLINK_EXEC_MOBILE)
-    luat_airlink_cmd_t *cmd = (luat_airlink_cmd_t *)basic_info;
-    cmd->cmd = 0x10;
-    cmd->len = 128;
-
-    extern void luat_airlink_devinfo_init();
-    luat_airlink_devinfo_init(send_devinfo_update_evt);
-    #endif
-
     luat_rtos_task_sleep(5); // 等5ms
     luat_airlink_spi_master_pin_setup();
     g_airlink_newdata_notify_cb = on_newdata_notify;
     g_airlink_link_data_cb = on_link_data_notify;
     thread_rdy = 1;
-
-    uint64_t tnow = luat_mcu_tick64_ms();
-    extern uint64_t g_airlink_wifi_boot_time;
-    if (g_airlink_wifi_boot_time != 0 && g_airlink_wifi_boot_time + 130 > tnow) {
-        // Air8000开机时间还没到150ms, 等够150ms再继续
-        luat_rtos_task_sleep((uint32_t)(g_airlink_wifi_boot_time + 130 - tnow));
+    while (luat_gpio_get(AIRLINK_SPI_RDY_PIN) == 1) {
+        luat_rtos_task_sleep(10);
     }
     while (1)
     {
@@ -447,18 +355,10 @@ __AIRLINK_CODE_IN_RAM__ static void spi_master_task(void *param)
 
         memset(s_txbuff, 0, TEST_BUFF_SIZE);
         airlink_wait_and_prepare_data(s_txbuff);
-        
-        // 立即发送数据给从机
+        // slave_rdy = 0;
+        airlink_wait_for_slave_ready(1000); // 最多等1秒
+
         airlink_transfer_and_exec(s_txbuff, s_rxbuff);
-        
-        // 发送完成后,等待从机的响应/确认
-        if (0 == g_airlink_last_cmd_timestamp) {
-            airlink_wait_for_slave_reply(20); // 开机要快速检测
-        }
-        else {
-            airlink_wait_for_slave_reply(1000); // 最多等1秒
-        }
-        
 
         memset(s_rxbuff, 0, TEST_BUFF_SIZE);
         // start = 0;
@@ -476,16 +376,8 @@ void luat_airlink_start_master(void)
     s_txbuff = luat_heap_opt_malloc(AIRLINK_MEM_TYPE, TEST_BUFF_SIZE);
     s_rxbuff = luat_heap_opt_malloc(AIRLINK_MEM_TYPE, TEST_BUFF_SIZE);
 
-    // 创建通用事件队列 (id=2,3等)
     luat_rtos_queue_create(&evt_queue, 4 * 1024, sizeof(luat_event_t));
-    // 创建专门的RDY事件队列 (id=6)
-    luat_rtos_queue_create(&rdy_evt_queue, 1, sizeof(luat_event_t));
-    #ifdef TYPE_EC718M
-    #define AIRLINK_TASK_PRIORITY 150
-    #else
-    #define AIRLINK_TASK_PRIORITY 80
-    #endif
-    luat_rtos_task_create(&spi_task_handle, 8 * 1024, AIRLINK_TASK_PRIORITY, "spi", spi_master_task, NULL, 0);
+    luat_rtos_task_create(&spi_task_handle, 8 * 1024, 50, "spi", spi_master_task, NULL, 0);
 }
 
 int luat_airlink_irqmode(luat_airlink_irq_ctx_t *ctx) {

+ 15 - 11
components/airlink/src/task/luat_airlink_task.c

@@ -99,34 +99,38 @@ __AIRLINK_CODE_IN_RAM__ static int luat_airlink_task(void *param) {
                 LLOGW("空指令!");
                 continue;
             }
-            if (ptr->cmd == 0) {
-                goto clean;
+            if (ptr->cmd == 0x10) {
+                exec_cmd(ptr);
             }
 
-            exec_cmd(ptr);
-            
+            // 更新最后通讯时间
             if (g_airlink_last_cmd_timestamp == 0) {
                 g_airlink_last_cmd_timestamp = luat_mcu_tick64_ms();
-                extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
+                extern uint64_t g_airlink_wifi_boot_time;
+                uint64_t t_used = g_airlink_last_cmd_timestamp - g_airlink_wifi_boot_time;
+                uint32_t tmpv = 0;
                 if (g_airlink_ext_dev_info.tp == 0x01) {
-                    uint32_t tmpv = 0;
                     memcpy(&tmpv, g_airlink_ext_dev_info.wifi.version, 4);
-                    LLOGI("AIRLINK_READY %ld version %ld", (uint32_t)g_airlink_last_cmd_timestamp, tmpv);
+                    LLOGI("AIRLINK_READY %ld version %ld t %ld", (uint32_t)g_airlink_last_cmd_timestamp, tmpv, (uint32_t)t_used);
                 }
                 else if (g_airlink_ext_dev_info.tp == 0x02) {
-                    uint32_t tmpv = 0;
                     memcpy(&tmpv, g_airlink_ext_dev_info.cat1.version, 4);
-                    LLOGI("AIRLINK_READY %ld version %ld", (uint32_t)g_airlink_last_cmd_timestamp, tmpv);
+                    LLOGI("AIRLINK_READY %ld version %ld t %ld", (uint32_t)g_airlink_last_cmd_timestamp, tmpv, (uint32_t)t_used);
                 }
                 else {
-                    LLOGI("AIRLINK_READY %ld", (uint32_t)g_airlink_last_cmd_timestamp);
+                    LLOGI("AIRLINK_READY %ld t %ld", (uint32_t)g_airlink_last_cmd_timestamp, (uint32_t)t_used);
                 }
                 // TODO 发个系统消息
             }
             else {
                 g_airlink_last_cmd_timestamp = luat_mcu_tick64_ms();
             }
-            clean:
+
+            
+            if (ptr->cmd && ptr->cmd != 0x10) {
+                exec_cmd(ptr);
+            }
+
             // 处理完成, 释放内存
             luat_heap_opt_free(AIRLINK_MEM_TYPE, ptr);
         }

+ 10 - 20
components/bluetooth/drv/luat_drv_ble_port.c

@@ -24,16 +24,6 @@ luat_ble_cb_t g_drv_ble_cb;
 #undef LLOGD
 #define LLOGD(...)
 
-// 读取wifi固件版本, 控制API适配状态
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
-static uint32_t get_ble_version(void) {
-    uint32_t version = 0;
-    if (g_airlink_ext_dev_info.tp == 1) {
-        memcpy(&version, g_airlink_ext_dev_info.wifi.version, 4);
-    }
-    return version;
-}
-
 int luat_ble_init(void* args, luat_ble_cb_t luat_ble_cb) {
     LLOGD("执行luat_ble_init %p", luat_ble_cb);
     g_drv_ble_cb = luat_ble_cb;
@@ -534,8 +524,8 @@ int luat_ble_delete_scanning(void* args) {
 
 int luat_ble_connect(void* args, luat_ble_connect_req_t *conn) {
     LLOGD("执行luat_ble_connect");
-    if (get_ble_version() < 11) {
-        LLOGE("ble:connect not support, ble version is %d", get_ble_version());
+    if (luat_airlink_sversion() < 11) {
+        LLOGE("ble:connect not support, ble version is %d", luat_airlink_sversion());
         return -1;
     }
     uint64_t seq = luat_airlink_get_next_cmd_id();
@@ -562,8 +552,8 @@ int luat_ble_connect(void* args, luat_ble_connect_req_t *conn) {
 
 int luat_ble_disconnect(void* args) {
     LLOGD("执行luat_ble_disconnect");
-    if (get_ble_version() < 11) {
-        LLOGE("ble:disconnect not support, ble version is %d", get_ble_version());
+    if (luat_airlink_sversion() < 11) {
+        LLOGE("ble:disconnect not support, ble version is %d", luat_airlink_sversion());
         return -1;
     }
     uint64_t seq = luat_airlink_get_next_cmd_id();
@@ -585,8 +575,8 @@ int luat_ble_disconnect(void* args) {
 
 int luat_ble_read_value(luat_ble_uuid_t* uuid_service, luat_ble_uuid_t* uuid_characteristic, luat_ble_uuid_t* uuid_descriptor, uint8_t **data, uint16_t* len) {
     LLOGD("执行luat_ble_read_value");
-    if (get_ble_version() < 11) {
-        LLOGE("ble:read_value not support, ble version is %d", get_ble_version());
+    if (luat_airlink_sversion() < 11) {
+        LLOGE("ble:read_value not support, ble version is %d", luat_airlink_sversion());
         return -1;
     }
     uint16_t tmp = 0;
@@ -634,8 +624,8 @@ int luat_ble_read_value(luat_ble_uuid_t* uuid_service, luat_ble_uuid_t* uuid_cha
 
 int luat_ble_notify_enable(luat_ble_uuid_t* uuid_service, luat_ble_uuid_t* uuid_characteristic, uint8_t enable) {
     LLOGD("执行luat_ble_notify_enable %d", enable);
-    if (get_ble_version() < 11) {
-        LLOGE("ble:notify_enable not support, ble version is %d", get_ble_version());
+    if (luat_airlink_sversion() < 11) {
+        LLOGE("ble:notify_enable not support, ble version is %d", luat_airlink_sversion());
         return -1;
     }
     uint16_t tmp = 0;
@@ -676,8 +666,8 @@ int luat_ble_notify_enable(luat_ble_uuid_t* uuid_service, luat_ble_uuid_t* uuid_
 
 int luat_ble_indicate_enable(luat_ble_uuid_t* uuid_service, luat_ble_uuid_t* uuid_characteristic, uint8_t enable) {
     LLOGD("执行luat_ble_indicate_enable %d", enable);
-    if (get_ble_version() < 15) {
-        LLOGE("ble:indicate_enable not support, ble version is %d", get_ble_version());
+    if (luat_airlink_sversion() < 15) {
+        LLOGE("ble:indicate_enable not support, ble version is %d", luat_airlink_sversion());
         return -1;
     }
     uint16_t tmp = 0;

+ 0 - 2
components/bluetooth/drv/luat_drv_bt_port.c

@@ -13,8 +13,6 @@
 #define LUAT_LOG_TAG "drv.bt"
 #include "luat_log.h"
 
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
-
 int luat_bluetooth_init(void* args) {
     LLOGD("执行luat_bluetooth_init");
     uint64_t luat_airlink_next_cmd_id = luat_airlink_get_next_cmd_id();

+ 0 - 2
components/drv/src/luat_drv_wlan.c

@@ -16,8 +16,6 @@
 // #undef LLOGD
 // #define LLOGD(...) 
 
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
-
 int luat_drv_wlan_init(luat_wlan_config_t *conf) {
     return luat_airlink_drv_wlan_init(conf);
 }

+ 2 - 2
components/gmssl/src/gf128.c

@@ -46,7 +46,7 @@ int gf128_equ_hex(gf128_t a, const char *s)
 	gf128_to_bytes(a, bin2);
 	return memcmp(bin1, bin2, sizeof(bin1)) == 0;
 }
-
+#if 0
 void gf128_print_bits(gf128_t a)
 {
 	int i;
@@ -74,7 +74,7 @@ int gf128_print(FILE *fp, int fmt, int ind, const char *label, gf128_t a)
 	printf("\n");
 	return 1;
 }
-
+#endif
 static uint64_t reverse_bits(uint64_t a)
 {
 	uint64_t r = 0;

+ 1 - 1
components/gmssl/src/sm2_elgamal.c

@@ -105,7 +105,7 @@ int sm2_elgamal_solve_ecdlp(const SM2_PRE_COMPUTE table[1<<16], const SM2_POINT
 		}
 		sm2_jacobian_point_add(&P, &P, &Q);
 	}
-	printf("gaint steps failed\n");
+	//printf("gaint steps failed\n");
 
 ok:
 	i = j = 0;

+ 0 - 6
components/gtfont/luat_gtfont.c

@@ -27,9 +27,7 @@ unsigned char r_dat_bat(unsigned long address,unsigned long DataLen,unsigned cha
         (uint8_t)((address)>>8),
         (uint8_t)(address)
     };
-    luat_spi_lock(gt_spi_dev->bus_id);
     luat_spi_device_transfer(gt_spi_dev, send_buf, 4, (char *)pBuff, DataLen);
-    luat_spi_unlock(gt_spi_dev->bus_id);
     #if LUAT_GT_DEBUG
     LLOGD("r_dat_bat addr %08X len %d pBuff %X", address, DataLen,*pBuff);
     for(int i = 0; i < DataLen;i++){
@@ -51,9 +49,7 @@ unsigned char CheckID(unsigned char CMD, unsigned long address,unsigned long byt
         (uint8_t)((address)>>8),
         (uint8_t)(address)
     };
-    luat_spi_lock(gt_spi_dev->bus_id);
     luat_spi_device_transfer(gt_spi_dev, send_buf, 4, (char *)p_arr, byte_long);
-    luat_spi_unlock(gt_spi_dev->bus_id);
     // return p_arr[0];
     return 1;
 }
@@ -62,9 +58,7 @@ unsigned char gt_read_data(unsigned char* sendbuf , unsigned char sendlen , unsi
 {
     if (gt_spi_dev == NULL)
         return 0;
-    luat_spi_lock(gt_spi_dev->bus_id);
     luat_spi_device_transfer(gt_spi_dev, (const char *)sendbuf, sendlen,(char *)receivebuf, receivelen);
-    luat_spi_unlock(gt_spi_dev->bus_id);
     #if LUAT_GT_DEBUG
     LLOGD("gt_read_data sendlen:%d receivelen:%d",sendlen,receivelen);
     for(int i = 0; i < sendlen;i++){

+ 3 - 0
components/lvgl/binding/luat_lib_lvgl.c

@@ -1339,6 +1339,9 @@ LUAT_LV_EX_RLT
 {"font_load", ROREG_FUNC(luat_lv_font_load)},
 {"font_free", ROREG_FUNC(luat_lv_font_free)},
 
+{"font_load_ex", ROREG_FUNC(luat_lv_font_load_ex)},
+{"font_free_ex", ROREG_FUNC(luat_lv_font_free_ex)},
+
 // 结构体
 #if LV_USE_ANIMATION
 {"anim_t", ROREG_FUNC(luat_lv_struct_anim_t)},

+ 57 - 2
components/lvgl/binding/luat_lib_lvgl_font.c

@@ -9,7 +9,7 @@
 #include "luat_lvgl.h"
 #include "lvgl.h"
 #include "luat_mem.h"
-
+#include "luat_fs.h"
 #include "luat_lvgl_fonts.h"
 #include "luat_spi.h"
 
@@ -89,7 +89,7 @@ int luat_lv_font_get(lua_State *L) {
 }
 
 /*
-从文件系统加载字体
+从文件系统加载字体(lvgl官方bin和高通矢量字库芯片使用,lvgl官方bin需要完整加载占用ram极大)
 @api lvgl.font_load(path/spi_device,size,bpp,thickness,cache_size,sty_zh,sty_en)
 @string/userdata 字体路径/spi_device (spi_device为使用外置高通矢量字库芯片)
 @number size 可选,字号 16-192 默认16(使用高通矢量字库)
@@ -175,3 +175,58 @@ int luat_lv_font_free(lua_State *L) {
     }
     return 0;
 }
+
+
+/*
+从文件系统加载字体(lvgl_conv_tool制作的字体文件,占用ram小,无需完整加载)
+@api lvgl.font_load_ex(path)
+@string 字体路径
+@return userdata lvgl字体指针
+@usage
+-- 使用lvgl_conv_tool制作的字体文件
+-- github: https://github.com/Dozingfiretruck/lvgl_conv_tool
+-- gitee: https://gitee.com/Dozingfiretruck/lvgl_conv_tool
+
+local font = lvgl.font_load_ex("/font_32.bin")
+*/
+int luat_lv_font_load_ex(lua_State *L) {
+    lv_font_t *font = NULL;
+    if (lua_isstring(L, 1)) {
+        const char* font_path = luaL_checkstring(L, 1);
+        FILE* font_file = luat_fs_fopen(font_path, "rb");
+        if (font_file == NULL) {
+            LLOGE("open font file fail %s", font_path);
+            return 0;
+        }
+        extern lv_font_t* custom_get_font(FILE* font_file);
+        font = custom_get_font(font_file);
+        if (!font) {
+            LLOGE("从文件加载lvgl字体 %s 失败", font_path);
+        }
+    }
+    if (font) {
+        lua_pushlightuserdata(L, font);
+        return 1;
+    }
+    return 0;
+}
+
+/*
+释放字体,慎用!!!仅通过font_load_ex加载的字体允许卸载,通过font_get获取的字体不允许卸载
+@api lvgl.font_load_ex(font)
+@string 字体路径
+@return userdata 字体指针
+@usage
+local font = lvgl.font_load_ex("/font_32.bin")
+-- N N N N 操作
+-- 确定字体不被使用,不被引用,且内存紧张需要释放
+lvgl.font_free_ex(font)
+*/
+int luat_lv_font_free_ex(lua_State *L) {
+    lv_font_t* font = lua_touserdata(L, 1);
+    if (font) {
+        extern void custom_free_font(lv_font_t* custom_font);
+        custom_free_font(font);
+    }
+    return 0;
+}

+ 3 - 0
components/lvgl/binding/luat_lvgl_font.h

@@ -6,5 +6,8 @@ int luat_lv_font_get(lua_State *L);
 int luat_lv_font_load(lua_State *L);
 int luat_lv_font_free(lua_State *L);
 
+int luat_lv_font_load_ex(lua_State *L);
+int luat_lv_font_free_ex(lua_State *L);
+
 #define LUAT_LV_FONT_EX_RLT 
 

+ 162 - 0
components/lvgl/font/lv_font_ex.c

@@ -0,0 +1,162 @@
+
+/*******************************************************************************
+ * tool: lvgl_conv_tool V1.0.1
+ * author: Dozingfiretruck
+ * author_github_url: https://github.com/Dozingfiretruck
+ * author_gitee_url: https://gitee.com/Dozingfiretruck
+ * qq: 1041390013
+ ******************************************************************************/
+
+#ifdef __has_include
+    #if __has_include("lvgl.h")
+        #ifndef LV_LVGL_H_INCLUDE_SIMPLE
+            #define LV_LVGL_H_INCLUDE_SIMPLE
+        #endif
+    #endif
+#endif
+
+#ifdef LV_LVGL_H_INCLUDE_SIMPLE
+    #include "lvgl.h"
+#else
+    #include "lvgl/lvgl.h"
+#endif
+
+#include "luat_mem.h"
+#include "luat_fs.h"
+
+/*-----------------
+ *    STRUCT
+ *----------------*/
+
+typedef struct{
+    char head[5];
+    uint8_t bpp;
+    int8_t underline_position;
+    int8_t underline_thickness;
+    int16_t line_height;
+    int16_t base_line;
+    uint16_t codes_min;
+    uint16_t codes_max;
+}custom_font_header_t;
+
+typedef struct{
+    uint32_t adv_w;
+    uint16_t box_w;
+    uint16_t box_h;
+    int16_t ofs_x;
+    int16_t ofs_y;
+    uint32_t glyph_data_len;
+}custom_glyph_dsc_t;
+
+typedef struct{
+    custom_font_header_t custom_font_header;
+    FILE* font_file;
+    uint8_t glyph_data[2048];
+}custom_font_data_t;
+
+static void *custom_font_getdata(const lv_font_t * font, uint32_t offset, uint32_t size){
+    custom_font_data_t* custom_font_data = font->dsc;
+    FILE* font_file = custom_font_data->font_file;
+    uint8_t* glyph_data = custom_font_data->glyph_data;
+    memset(glyph_data, 0, sizeof(custom_font_data->glyph_data));
+    luat_fs_fseek(font_file, offset, SEEK_SET);
+    luat_fs_fread(glyph_data, 1, size, font_file);
+    return glyph_data;
+}
+
+/*-----------------
+ *  GET DSC
+ *----------------*/
+
+static bool custom_font_get_glyph_dsc_fmt_txt(const lv_font_t * font, lv_font_glyph_dsc_t * dsc_out, uint32_t unicode_letter, uint32_t unicode_letter_next) {
+    custom_font_data_t* custom_font_data = font->dsc;
+    custom_font_header_t* custom_font_header = &custom_font_data->custom_font_header;
+    bool is_tab = false;
+    if(unicode_letter == '\t') {
+        unicode_letter = ' ';
+        is_tab = true;
+    }
+    if( unicode_letter>custom_font_header->codes_max || unicode_letter<custom_font_header->codes_min ) {
+        return false;
+    }
+    uint32_t unicode_offset = sizeof(custom_font_header_t)+(unicode_letter-custom_font_header->codes_min)*sizeof(uint32_t);
+    uint32_t *p_pos = (uint32_t *)custom_font_getdata(font, unicode_offset, sizeof(uint32_t));
+    if(*p_pos) {
+        custom_glyph_dsc_t * gdsc = (custom_glyph_dsc_t*)custom_font_getdata(font, *p_pos, sizeof(custom_glyph_dsc_t));
+        dsc_out->box_h = gdsc->box_h;
+        dsc_out->box_w = gdsc->box_w;
+        dsc_out->ofs_x = gdsc->ofs_x;
+        dsc_out->ofs_y = gdsc->ofs_y;
+        dsc_out->bpp   = custom_font_header->bpp;
+
+        uint32_t adv_w = gdsc->adv_w;
+        if(is_tab) adv_w *= 2;
+        dsc_out->adv_w  = (adv_w + (1 << 3)) >> 4;
+        if(is_tab) dsc_out->box_w = dsc_out->box_w * 2;
+        return true;
+    }
+    return false;
+}
+
+/*-----------------
+ *  GET BITMAP
+ *----------------*/
+
+static const uint8_t* custom_font_get_bitmap_fmt_txt(const lv_font_t * font, uint32_t unicode_letter) {
+    custom_font_data_t* custom_font_data = font->dsc;
+    custom_font_header_t* custom_font_header = &custom_font_data->custom_font_header;
+    if(unicode_letter == '\t') unicode_letter = ' ';
+    if( unicode_letter>custom_font_header->codes_max || unicode_letter<custom_font_header->codes_min ) {
+        return NULL;
+    }
+    uint32_t unicode_offset = sizeof(custom_font_header_t )+(unicode_letter-custom_font_header->codes_min)*sizeof(uint32_t);
+    uint32_t *p_pos = (uint32_t *)custom_font_getdata(font, unicode_offset, sizeof(uint32_t));
+    uint32_t dsc_offset = p_pos[0];
+    if(dsc_offset) {
+        custom_glyph_dsc_t* gdsc = (custom_glyph_dsc_t*)custom_font_getdata(font, dsc_offset, sizeof(custom_glyph_dsc_t));
+        return custom_font_getdata(font, dsc_offset + sizeof(custom_glyph_dsc_t), gdsc->glyph_data_len);
+    }
+    return NULL;
+}
+
+/*-----------------
+ *  PUBLIC FONT
+ *----------------*/
+
+lv_font_t* custom_get_font(FILE* font_file){
+    lv_font_t* custom_font = luat_heap_malloc(sizeof(lv_font_t));
+    if (custom_font == NULL){
+        return NULL;
+    }
+    
+    memset(custom_font, 0, sizeof(lv_font_t));
+    custom_font->get_glyph_dsc = custom_font_get_glyph_dsc_fmt_txt;
+    custom_font->get_glyph_bitmap = custom_font_get_bitmap_fmt_txt;
+
+    custom_font_data_t* custom_font_data = luat_heap_malloc(sizeof(custom_font_data_t));
+    memset(custom_font_data, 0, sizeof(custom_font_data_t));
+    custom_font_data->font_file = font_file;
+    custom_font->dsc = custom_font_data;
+
+    uint32_t *p_pos = (uint32_t *)custom_font_getdata(custom_font, 0, sizeof(custom_font_header_t));
+    memcpy(&custom_font_data->custom_font_header, p_pos, sizeof(custom_font_header_t));
+    if (strncmp(custom_font_data->custom_font_header.head, "head", 4)){
+        luat_heap_free(custom_font);
+        return NULL;
+    }
+
+    custom_font->base_line = custom_font_data->custom_font_header.base_line;
+    custom_font->line_height = custom_font_data->custom_font_header.line_height;
+#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8
+    custom_font->underline_position = custom_font_data->custom_font_header.underline_position;
+    custom_font->underline_thickness = custom_font_data->custom_font_header.underline_thickness;
+#endif
+    return custom_font;
+}
+
+void custom_free_font(lv_font_t* custom_font){
+    if (custom_font){
+        luat_heap_free(custom_font->dsc);
+        luat_heap_free(custom_font);
+    }
+}

+ 1 - 1
components/lvgl/lv_conf.h

@@ -469,7 +469,7 @@ typedef void * lv_indev_drv_user_data_t;            /*Type of user data in the i
 /* Enables/disables support for compressed fonts. If it's disabled, compressed
  * glyphs cannot be processed by the library and won't be rendered.
  */
-#define LV_USE_FONT_COMPRESSED 0
+#define LV_USE_FONT_COMPRESSED 1
 
 /* Enable subpixel rendering */
 #define LV_USE_FONT_SUBPX 0

+ 2 - 1
components/mobile/luat_lib_mobile.c

@@ -681,7 +681,8 @@ static int l_mobile_sync_time(lua_State* L) {
 -- 7:仅SMS可用,且漫游状态
 -- 8:仅紧急呼叫. 注意, 国内不支持此状态,模块也不支持紧急呼叫
 
--- 不推荐使用本API判断联网状态, 建议使用socket.localIP()来判断
+-- 当不是1或者5的时候,无法通过蜂窝移动网络上网
+-- 不能使用本API判断联网状态, 可联网状态应该以连上目标服务器为准
  */
 static int l_mobile_status(lua_State* L) {
     int LUAT_MOBILE_REGISTER_STATUS_E = luat_mobile_get_register_status();

+ 0 - 1
components/mreport/src/luat_mreport.c

@@ -22,7 +22,6 @@
 // #define MREPORT_DOMAIN "112.125.89.8"
 // #define MREPORT_PORT (42919)
 
-extern luat_airlink_dev_info_t g_airlink_ext_dev_info;
 static struct udp_pcb* mreport_pcb;
 static luat_rtos_timer_t mreport_timer;
 const char *project_name = "unkonw";             // luatos项目名称

+ 1 - 0
components/network/netdrv/binding/luat_lib_netdrv.c

@@ -220,6 +220,7 @@ static int l_netdrv_ipv4(lua_State *L) {
     int ret = 0;
     luat_netdrv_t* netdrv = luat_netdrv_get(id);
     if (netdrv == NULL || netdrv->netif == NULL) {
+        LLOGW("对应的netdrv不存在或未就绪 %d %p", id, netdrv);
         return 0;
     }
     if (lua_isstring(L, 2) && lua_isstring(L, 3) && lua_isstring(L, 4)) {

+ 10 - 1
components/network/netdrv/include/luat_netdrv_napt.h

@@ -5,6 +5,15 @@
 
 // #define IP_NAPT_TIMEOUT_MS_TCP (30*60*1000)
 #define IP_NAPT_TIMEOUT_MS_TCP_DISCON (20*1000)
+#ifndef NAPT_TCP_MAP_ITEM_MAX
+#if defined(TYPE_EC718HM)
+#define NAPT_TCP_MAP_ITEM_MAX (8*1024)
+#elif defined(TYPE_EC718PM)
+#define NAPT_TCP_MAP_ITEM_MAX (4*1024)
+#else
+#define NAPT_TCP_MAP_ITEM_MAX (2*1024)
+#endif
+#endif
 
 typedef struct luat_netdrv_napt_icmp
 {
@@ -62,7 +71,7 @@ typedef struct luat_netdrv_napt_ctx{
     size_t clean_tm;
     size_t item_max;
     size_t item_last;
-    luat_netdrv_napt_tcpudp_t items[2048];
+    luat_netdrv_napt_tcpudp_t items[NAPT_TCP_MAP_ITEM_MAX];
     luat_rtos_mutex_t lock;
     uint32_t *port_used;
 }luat_netdrv_napt_ctx_t;

+ 3 - 3
components/network/netdrv/src/luat_netdrv_napt.c

@@ -38,7 +38,7 @@ luat_netdrv_napt_ctx_t *g_napt_udp_ctx;
 
 // 端口分配
 #define NAPT_PORT_RANGE_START     0x1BBC
-#define NAPT_PORT_RANGE_END       0x5AAA
+#define NAPT_PORT_RANGE_END       0x6AAA
 
 #define u32 uint32_t
 #define u16 uint16_t
@@ -222,7 +222,7 @@ static int ctx_init(luat_netdrv_napt_ctx_t** ctx_ptrptr) {
     ctx->port_used = luat_heap_malloc(port_len + 8);
     memset(ctx->port_used, 0, port_len + 8);
     ctx->clean_tm = 1;
-    ctx->item_max = 2048;
+    ctx->item_max = NAPT_TCP_MAP_ITEM_MAX;
 
     *ctx_ptrptr = ctx;
     return 0;
@@ -460,7 +460,7 @@ __NETDRV_CODE_IN_RAM__ int luat_netdrv_napt_tcp_lan2wan(napt_ctx_t* ctx, luat_ne
     }
     while (ret != 0) {
         if (napt_ctx->item_max == 0) {
-            napt_ctx->item_max = 2048;
+            napt_ctx->item_max = NAPT_TCP_MAP_ITEM_MAX;
         }
         if (napt_ctx->item_last >= napt_ctx->item_max) {
             LLOGE("TCP映射关系已经用完");

+ 6 - 3
components/sfud/luat_lib_sfud.c

@@ -80,14 +80,17 @@ local sfud_device = sfud.getDevice(1)
 */
 static int luat_sfud_get_device(lua_State *L){
     sfud_flash *flash = sfud_get_device(luaL_checkinteger(L, 1));
-    lua_pushlightuserdata(L, flash);
-    return 1;
+    if (flash){
+        lua_pushlightuserdata(L, flash);
+        return 1;
+    }
+    return 0;
 }
 
 /*
 获取flash设备信息表
 @api  sfud.getDeviceTable()
-@return userdata 成功返回一个数据结构,否则返回nil
+@return userdata 成功返回一个数据结构,sfud flash tables地址
 @usage
 local sfud_device = sfud.getDeviceTable()
 */

+ 4 - 4
components/tp/luat_tp.c

@@ -14,11 +14,11 @@ static luat_rtos_task_handle g_s_tp_task_handle = NULL;
 void luat_tp_task_entry(void* param){
     uint32_t message_id = 0;
     luat_tp_config_t *luat_tp_config = NULL;
+    luat_rtos_task_sleep(2);
+    while (!g_s_tp_task_handle){
+        luat_rtos_task_sleep(2);
+    }
     while (1){
-        while (!g_s_tp_task_handle){
-            luat_rtos_task_sleep(1);
-        }
-
         luat_rtos_message_recv(g_s_tp_task_handle, &message_id, &luat_tp_config, LUAT_WAIT_FOREVER);
 
         luat_tp_data_t* tp_data = luat_tp_config->tp_data;

+ 2 - 1
components/u8g2/u8x8_d_stdio.c

@@ -32,7 +32,7 @@
   ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  
 
 */
-
+#if 0
 #include "u8x8.h"
 
 #include <stdio.h>
@@ -99,3 +99,4 @@ void u8x8_SetupStdio(u8x8_t *u8x8)
   u8x8->display_cb = u8x8_d_stdio;
 }
 
+#endif

+ 1 - 1
components/wlan/luat_lib_wlan.c

@@ -481,7 +481,7 @@ static int l_wlan_ap_start(lua_State *L) {
 /**
 关闭AP功能
 @api wlan.stopAP()
-@return bool 成功创建返回true,否则返回false
+@return bool 成功返回true,否则返回false
 @usage
 wlan.stopAP()
 */

+ 112 - 8
lua/src/liolib.c

@@ -43,6 +43,18 @@ if fd then
 
   -- 执行完操作后,一定要关掉文件
   fd:close()
+
+  -- 2025.9.30 新增file:write支持zbuff参数
+  local zbuff = zbuff.create(1024)
+  zbuff:write("hello zbuff")
+  local f = io.open("/test_zbuff.bin", "wb+")
+  f:write(zbuff)
+  f:close()
+  -- 读出数据
+  local f = io.open("/test_zbuff.bin", "rb")
+  local data = f:read("*a")
+  f:close()
+  log.info("data", data)
 end
 */
 
@@ -270,7 +282,6 @@ static int aux_close (lua_State *L) {
   return (*cf)(L);  /* close it */
 }
 
-
 static int f_close (lua_State *L) {
   tofile(L);  /* make sure argument is an open stream */
   return aux_close(L);
@@ -293,6 +304,13 @@ static int f_gc (lua_State *L) {
 }
 
 
+
+/*
+关闭文件句柄
+@api file:close()
+@userdata 文件句柄, io.open返回的值
+@return nil 无返回值
+*/
 /*
 ** function to close regular files
 */
@@ -317,7 +335,44 @@ static void opencheck (lua_State *L, const char *fname, const char *mode) {
     luaL_error(L, "cannot open file '%s' (%d)", fname, errno);
 }
 
+/*
+打开文件,返回句柄
+@api io.open(path, mode)
+@string 文件路径
+@string 打开模式
+@see http://www.lua.org/manual/5.3/manual.html#pdf-io.open
+@return userdata 文件句柄,失败返回nil
+@usage
+-- 只读模式, 打开文件
+local fd = io.open("/xxx.txt", "rb")
+-- 读写默认,打开文件
+local fd = io.open("/xxx.txt", "wb")
+-- 写入文件,且截断为0字节
+local fd = io.open("/xxx.txt", "wb+")
+-- 追加模式
+local fd = io.open("/xxx.txt", "a")
+-- 若文件打开成功, fd不为nil,否则就是失败了
+if fd then
+  -- 读取指定字节数,如果数据不足,就只返回实际长度的数据
+  local data = fd:read(12)
 
+  -- 数据写入, 仅w或a模式可调用
+  -- 数据需要是字符串, lua的字符串是带长度的,可以包含任何二进制数据
+  fd:write("xxxx") 
+  -- 以下是写入0x12, 0x13
+  fd:write(string.char(0x12, 0x13))
+
+  -- 移动句柄,绝对坐标
+  fd:seek(1024, io.SEEK_SET)
+  -- 移动句柄,相对坐标
+  fd:seek(1024, io.SEEK_CUR)
+  -- 移动句柄,反向绝对坐标,从文件结尾往文件头部算
+  fd:seek(124, io.SEEK_END)
+
+  -- 执行完操作后,一定要关掉文件
+  fd:close()
+end
+*/
 static int io_open (lua_State *L) {
   const char *filename = luaL_checkstring(L, 1);
   const char *mode = luaL_optstring(L, 2, "r");
@@ -561,6 +616,7 @@ typedef struct {
 
 
 static int test_eof (lua_State *L, FILE *f) {
+  (void)L;
   return feof(f);
   // int c = getc(f);
   // ungetc(c, f);  /* no-op when c == EOF */
@@ -709,10 +765,12 @@ static int io_readline (lua_State *L) {
 
 /* }====================================================== */
 
-
+#include "luat_zbuff.h"
 static int g_write (lua_State *L, FILE *f, int arg) {
   int nargs = lua_gettop(L) - arg;
   int status = 1;
+  size_t l;
+  const char *s;
   for (; nargs--; arg++) {
     // if (lua_type(L, arg) == LUA_TNUMBER) {
     //   /* optimization: could be done exactly as for strings */
@@ -724,10 +782,14 @@ static int g_write (lua_State *L, FILE *f, int arg) {
     //   status = status && (len > 0);
     // }
     // else {
-      size_t l;
-      const char *s = luaL_checklstring(L, arg, &l);
+    if (lua_type(L, arg) == LUA_TUSERDATA) {
+      luat_zbuff_t*buff = (luat_zbuff_t *)luaL_checkudata(L, arg, LUAT_ZBUFF_TYPE);
+      status = status && (fwrite(buff->addr, sizeof(char), buff->used, f) == buff->used);
+    }
+    else {
+      s = luaL_checklstring(L, arg, &l);
       status = status && (fwrite(s, sizeof(char), l, f) == l);
-    // }
+    }
   }
   if (status) return 1;  /* file handle already on stack top */
   else return luaL_fileresult(L, status, NULL);
@@ -739,7 +801,29 @@ static int io_write (lua_State *L) {
 }
 #endif
 
-
+/*
+将数据写入文件
+@api file:write(data)
+@string/zbuff 数据
+@return boolean 成功返回true,否则返回nil和错误信息
+@usage
+local fd = io.open("/xxx.txt", "wb+")
+if fd then
+  -- 数据需要是字符串, lua的字符串是带长度的,可以包含任何二进制数据
+  local ret, err = fd:write("xxxx") 
+  if not ret then
+    log.error("io", "write error", err)
+  end
+  -- 2025.9.30 新增file:write支持zbuff参数
+  local zbuff = zbuff.create(1024)
+  zbuff:write("hello zbuff")
+  local ret, err = fd:write(zbuff)
+  if not ret then
+    log.error("io", "write error", err)
+  end
+  fd:close()
+end
+*/
 static int f_write (lua_State *L) {
   FILE *f = tofile(L);
   lua_pushvalue(L, 1);  /* push file at the stack top (to be returned) */
@@ -747,6 +831,22 @@ static int f_write (lua_State *L) {
 }
 
 
+/*
+移动文件指针
+@api file:seek(whence, offset)
+@string whence 定位方式, 可选值有 "set", "cur", "end", 默认 "cur"
+@int offset 偏移值, 默认0
+@return int 成功返回当前文件指针位置,否则返回nil和错误信息
+@usage
+local fd = io.open("/xxx.txt", "rb")
+if fd then
+  local pos = fd:seek("set", 0)
+  if not pos then
+    log.error("io", "seek error", err)
+  end
+  fd:close()
+end
+*/
 static int f_seek (lua_State *L) {
   static const int mode[] = {SEEK_SET, SEEK_CUR, SEEK_END};
   static const char *const modenames[] = {"set", "cur", "end", NULL};
@@ -881,12 +981,16 @@ static int io_readFile (lua_State *L) {
 
 /**
 将数据写入文件
-@api io.writeFile(path, data)
+@api io.writeFile(path, data, mode)
 @string 文件路径
 @string 数据
+@string 写入模式, 默认 "wb+"
 @return boolean 成功返回true, 否则返回false
 @usage
+-- 将数据写入到文件, 默认是"wb+"模式, 即完全覆写,原有文件数据全部删除, 文件不存在就新建
 io.writeFile("/bootime", "1")
+-- 以"ab+"模式打开文件, 追加数据到文件末尾
+io.writeFile("/bootime", "2", "ab+")
  */
 static int io_writeFile (lua_State *L) {
   const char *filename = luaL_checkstring(L, 1);
@@ -1256,7 +1360,7 @@ static int io_lsdir (lua_State *L) {
     return 2;
   }
 
-  return 0;
+  // return 0;
 }
 
 /*

+ 0 - 9
lua/src/loslib.c

@@ -216,15 +216,6 @@ static int os_getenv (lua_State *L) {
 }
 #endif
 
-/*
-返回程序使用的按秒计 CPU 时间的近似值
-@api os.clock()
-@return int 时间戳
-@usage
--- 不推荐使用本API
--- 如需要获取 时间戳, 请使用 os.time()
--- 如需获取系统运行时长, 请使用 mcu.ticks()
-*/
 static int os_clock (lua_State *L) {
   lua_pushinteger(L, ((lua_Integer)clock())/(lua_Integer)CLOCKS_PER_SEC);
   return 1;

+ 22 - 22
lua/src/lstrlib_exts.c

@@ -303,28 +303,28 @@ int l_str_toValue (lua_State *L) {
 }
 
 /*
-  将字符串进行url编码转换
-  @api string.urlEncode("123 abc")
-  @string 需要转换的字符串
-  @int	mode:url编码的转换标准,
-  			-1:自定义标准.为-1时,才会有后面的space和str_check
-  			 0:默认标准php
-  			 1:RFC3986标准,和默认的相比就是' '的转换方式不一样
-  			 这个参数不存在,按0:默认标准php处理
-  @int	space:' '空格的处理方式
-  			 0:' '转化为'+'
-  			 1:' '转换为"%20"
-  @string	str_check:不需要转换的字符,组成的字符串
-  @return string 返回转换后的字符串
-  @usage
-  -- 将字符串进行url编码转换
-  log.info(string.urlEncode("123 abc+/"))			-->> "123+abc%2B%2F"
-
-  log.info(string.urlEncode("123 abc+/",1))			-->> "123%20abc%2B%2F"
-
-  log.info(string.urlEncode("123 abc+/",-1,1,"/"))	-->> "123%20abc%2B/"
-  log.info(string.urlEncode("123 abc+/",-1,0,"/"))	-->> "123+abc%2B/"
-  log.info(string.urlEncode("123 abc+/",-1,0,"/ "))	-->> "123 abc%2B/"
+将字符串进行url编码转换
+@api string.urlEncode(data,mode,space,str_check)
+@string 需要转换的字符串,必须填
+@int url编码的转换标准,默认0, PHP标准, 1是RFC3986标准, -1是自定义标准
+@int 空格的处理方式 0:' '转化为'+' 1:' '转换为"%20". 仅mode为-1时有效
+@string	str_check:不需要转换的字符,组成的字符串. 仅mode为-1时有效
+@return string 返回转换后的字符串
+@usage
+-- mode可选值
+  -1 :自定义标准.为-1时,才会有后面的space和str_check
+  0 :默认标准php
+  1 :RFC3986标准,和默认的相比就是' '的转换方式不一样
+这个参数不存在,按0:默认标准php处理
+
+-- 将字符串进行url编码转换
+log.info(string.urlEncode("123 abc+/"))			-->> "123+abc%2B%2F"
+
+log.info(string.urlEncode("123 abc+/",1))			-->> "123%20abc%2B%2F"
+
+log.info(string.urlEncode("123 abc+/",-1,1,"/"))	-->> "123%20abc%2B/"
+log.info(string.urlEncode("123 abc+/",-1,0,"/"))	-->> "123+abc%2B/"
+log.info(string.urlEncode("123 abc+/",-1,0,"/ "))	-->> "123 abc%2B/"
 */
 int l_str_urlEncode (lua_State *L) {
   int argc = lua_gettop(L);

+ 1 - 1
luat/include/luat_base.h

@@ -7,7 +7,7 @@
 #ifndef LUAT_BASE_H
 #define LUAT_BASE_H
 /**LuatOS版本号*/
-#define LUAT_VERSION "25.03"
+#define LUAT_VERSION "25.11"
 #define LUAT_VERSION_BETA 0
 // 调试开关, 预留
 #define LUAT_DEBUG 0

+ 13 - 9
luat/modules/luat_lib_crypto.c

@@ -288,21 +288,25 @@ int l_crypto_cipher_decrypt(lua_State *L) {
 计算CRC16
 @api crypto.crc16(method, data, poly, initial, finally, inReversem outReverse)
 @string CRC16模式("IBM","MAXIM","USB","MODBUS","CCITT","CCITT-FALSE","X25","XMODEM","DNP","USER-DEFINED")
-@string 字符串
-@int poly值
-@int initial值
-@int finally值
-@int 输入反转,1反转,默认0不反转
+@string 字符串或者zbuff对象
+@int poly值,默认0x0000,范围0-0xFFFF
+@int initial值,默认0x0000,范围0-0xFFFF
+@int finally值,默认0x0000,范围0-0xFFFF
 @int 输入反转,1反转,默认0不反转
+@int 输出反转,1反转,默认0不反转
 @return int 对应的CRC16值
 @usage
--- 计算CRC16
-local crc = crypto.crc16("")
+-- 计算字符串的CRC16
+local crc = crypto.crc16("dfadfasfdsafdasf")
+-- 使用zbuff时,会计算used之后的全部数据,建议使用前seek(0)
+local zbuff = zbuff.create("dfadfasfdsafdasf")
+zbuff:seek(0)
+crc = crypto.crc16(zbuff)
  */
 static int l_crypto_crc16(lua_State *L)
 {
-    size_t inputlen;
-    const unsigned char *inputData;
+    size_t inputlen = 0;
+    const unsigned char *inputData = NULL;
     const char  *inputmethod = (const char*)luaL_checkstring(L, 1);
     if(lua_isuserdata(L, 2))
     {

+ 0 - 8
luat/modules/luat_lib_rtc.c

@@ -272,14 +272,6 @@ static int l_rtc_timer_stop(lua_State *L){
     return 1;
 }
 
-/*
-设置RTC基准年,不推荐
-@api rtc.setBaseYear(Base_year)
-@int 基准年Base_year,通常1900
-@usage
--- 本函数已经废弃, 不要使用
-rtc.setBaseYear(1900)
-*/
 static int l_rtc_set_base_year(lua_State *L){
     Base_year = luaL_checkinteger(L, 1);
     return 0;

+ 5 - 5
luat/modules/luat_lib_spi.c

@@ -17,8 +17,8 @@ spi.setup(0,nil,0,0,8,2000000,spi.MSB,1,1)
 local result = spi.send(0, "123")--发送123
 local recv = spi.recv(0, 4)--接收4字节数据
 spi.close(0)
--- 新API
-local spi_device = spi.deviceSetup(0,17,0,0,8,2000000,spi.MSB,1,1)
+-- 新API, 注意spi_device是一个对象,选一个全局名称, 避免被回收
+spi_device = spi.deviceSetup(0,17,0,0,8,2000000,spi.MSB,1,1)
 local result = spi_device:send("123")--发送123
 local recv = spi_device:recv(4)--接收4字节数据
 spi_device:close()
@@ -321,7 +321,7 @@ static int l_spi_close(lua_State *L) {
 @int SPI号(例如0)或软件SPI对象
 @string/zbuff 待发送的数据,如果为zbuff数据,则会从对象所处的指针处开始读
 @int 可选。待发送数据的长度,默认为data长度
-@int 可选。读取数据的长度,默认为1
+@int 可选。读取数据的长度,默认为1. 注意, 如果是全双工模式,收发长度必须相等
 @return string 读取成功返回字符串,否则返回nil
 @usage
 -- 初始化spi
@@ -485,7 +485,7 @@ static int l_spi_recv(lua_State *L) {
 
 /**
 发送SPI数据
-@api spi.send(id, data[, len])
+@api spi.send(id, data, len)
 @int SPI号,例如0
 @string/zbuff 待发送的数据,如果为zbuff数据,则会从对象所处的指针处开始读
 @int 可选。待发送数据的长度,默认为data长度
@@ -601,7 +601,7 @@ static int l_spi_device_close(lua_State *L) {
 @userdata spi_device
 @string/zbuff 待发送的数据,如果为zbuff数据,则会从对象所处的指针处开始读
 @int 可选。待发送数据的长度,默认为data长度
-@int 可选。读取数据的长度,默认为1
+@int 可选。读取数据的长度,默认为1. 注意, 如果是全双工模式,收发长度必须相等
 @return string 读取成功返回字符串,否则返回nil
 @usage
 -- 初始化spi

+ 2 - 0
luat/modules/luat_lib_timer.c

@@ -4,6 +4,8 @@
 @version 1.0
 @date    2020.03.30
 @tag LUAT_USE_TIMER
+@usage
+-- 本库通常不需要使用, 除非你很清楚会发生什么
 */
 #include "luat_base.h"
 #include "luat_log.h"

+ 4 - 5
module/Air780E/demolib/adxl34x.lua

@@ -135,11 +135,10 @@ log.info("adxl34x_data", "adxl34x_data.x"..(adxl34x_data.x),"adxl34x_data.y"..(a
 function adxl34x.get_data()
     local accel={x=nil,y=nil,z=nil}
     i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAX0)
-    _,accel.x = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
-    i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAY0)
-    _,accel.y = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
-    i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAZ0)
-    _,accel.z = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
+    local rcvData = i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 6)
+    if rcvData ~= nil and #rcvData == 6 then
+        accel.x, accel.y, accel.z = string.unpack("<hhh", rcvData)
+    end
     return accel
 end
 

+ 4 - 4
module/Air780E/demolib/spl06.lua

@@ -79,11 +79,11 @@ local function i2cReadTwoData(i2caddr,regaddr)
         msb = 0
         lsb = 0
         log.info("spl06 two data","the data is null")
-        return msb256+lsb
+        return msb*256+lsb
     else
     --log.info("spl06 two data","msb:"..msb.." lsb:"..lsb)
         if regaddr ==0x10 then
-            return msb16 + bit.rshift((lsb&0xF0),4)
+            return msb*16 + bit.rshift((lsb&0xF0),4)
         elseif regaddr ==0x11 then
             return (msb&0x0f)*256 + lsb
         else
@@ -106,7 +106,7 @@ local function chip_check()
         if revData:byte() ~= nil then
             SPL06_ADDRESS_ADR = SPL06_ADDRESS_ADR_LOW
         else
-            log.info("i2c", "Can't find adxl34x device")
+            log.info("i2c", "Can't find SPL06 device")
             return false
         end
     end
@@ -130,7 +130,7 @@ spl06初始化
 @usage
 spl06.init(0)
 ]]
-function spl06.init(i2cid)
+function spl06.init(i2c_id)
     i2cid = i2c_id
     sys.wait(20)--20 毫秒等待设备稳定
     if chip_check() then

+ 81 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirMICROSD_1010/http_download_file.lua

@@ -0,0 +1,81 @@
+--[[
+@module http_download_file
+@summary http下载文件模块
+@version 1.0.0
+@date    2025.08.25
+@author  王棚嶙
+@usage
+本文件演示的功能为通过http下载文件进入TF卡中:
+1. 网络就绪检测
+2. 创建HTTP下载任务并等待完成
+3. 记录下载结果
+4. 获取并记录文件大小
+本文件没有对外接口,直接在main.lua中require "http_download_file"即可
+]] 
+
+
+local function http_download_file_task()
+
+    -- 阶段1: 网络就绪检测
+
+    while not socket.adapter(socket.dft()) do
+        log.warn("HTTP下载", "等待网络连接", socket.dft())
+        -- 等待IP_READY消息,超时设为1秒
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("HTTP下载", "网络已就绪", socket.dft())
+
+    -- 在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)
+
+
+    -- 挂载文件系统
+    local mount_ok = fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)
+    if not mount_ok then
+        log.error("HTTP下载", "文件系统挂载失败")
+        fatfs.unmount("/sd")
+        spi.close(spi_id)
+        return
+    end
+
+    -- 阶段2: 执行下载任务
+    log.info("HTTP下载", "开始下载任务")
+
+    -- 核心下载操作开始 (支持http和https)
+    -- local code, headers, body = http.request("GET", "...", nil, nil, {dst = "/sd/1.mp3"}).wait()
+    -- 其中 "..."为url地址, 支持 http和https, 支持域名, 支持自定义端口。
+    local code, headers, body_size = http.request("GET",
+                                    "https://gitee.com/openLuat/LuatOS/raw/master/module/Air780EHM_Air780EHV_Air780EGH/demo/audio/1.mp3",
+                                    nil, nil, {dst = "/sd/1.mp3"}).wait()
+    -- 阶段3: 记录下载结果
+    log.info("HTTP下载", "下载完成", 
+        code==200 and "success" or "error", 
+        code, 
+        -- headers是下载的文件头信息
+        json.encode(headers or {}), 
+        -- body_size是下载的文件大小(字节数)
+        body_size) 
+        
+    if code == 200 then
+        -- 获取实际文件大小
+        local actual_size = io.fileSize("/sd/1.mp3")
+        log.info("HTTP下载", "文件大小验证", "预期:", body_size, "实际:", actual_size)
+        
+        if actual_size~= body_size then
+            log.error("HTTP下载", "文件大小不一致", "预期:", body_size, "实际:", actual_size)
+        end
+    end
+
+    -- 阶段4: 资源清理
+    fatfs.unmount("/sd")
+    spi.close(spi_id)
+    log.info("HTTP下载", "资源清理完成")
+end
+
+-- 创建下载任务
+sys.taskInit(http_download_file_task)

+ 90 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirMICROSD_1010/main.lua

@@ -0,0 +1,90 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 001.000.000
+@date    2025.08.25
+@author  王棚嶙
+@usage
+本 Demo 完整覆盖了 TF 卡操作的核心到高级流程,HTTP下载功能包括:
+1. 基础操作:
+   - CH390 供电控制
+   - 看门狗守护机制 
+2. 挂载及文件操作:
+   - 文件系统挂载/卸载
+   - TF卡空间信息查询
+   - 文件创建/读写/追加
+   - 目录创建/删除
+   - 文件重命名/删除
+   - 文件存在性检查与大小获取
+3. 下载功能:
+   - 网络检测与HTTP文件下载
+更多说明参考本目录下的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 = "tfcard"
+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)
+
+
+
+
+--[[在加载以下两个功能时,建议分别打开进行测试,因为文件操作和http下载功能是异步操作。
+放到一个项目中,如果加载的时间点是随机的,就会出现tfcard_app在spi.setup和fatfs挂载文件系统之后,
+还没有释放资源,然后http_download_file又去重复spi.setup和fatfs挂载文件系统了,不符合正常的业务逻辑,用户在参考编程的时候也要注意。]]
+
+--加载tf卡测试应用模块
+require "tfcard_app"
+--加载HTTP下载存入TF卡功能演示模块
+--require "http_download_file"
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 52 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirMICROSD_1010/pins_Air780EHM.json

@@ -0,0 +1,52 @@
+{
+  "model": "Air780EHM",
+  "pins": [
+    [7, "PWR_KEY", ""],
+    [16, "GPIO27", ""],
+    [17, "UART1_RXD", ""],
+    [18, "UART1_TXD", ""],
+    [19, "GPIO22", ""],
+    [20, "PWM1", ""],
+    [22, "PWM0", ""],
+    [23, "ONEWIRE", ""],
+    [25, "CAN_TXD", ""],
+    [26, "PWM4", ""],
+    [28, "UART2_RXD", ""],
+    [29, "UART2_TXD", ""],
+    [30, "GPIO29", ""],
+    [31, "GPIO30", ""],
+    [32, "GPIO31", ""],
+    [33, "GPIO32", ""],
+    [38, "DBG_RXD", ""],
+    [39, "DBG_TXD", ""],
+    [49, "LCD_RST", ""],
+    [50, "LCD_SDA", ""],
+    [51, "LCD_RS", ""],
+    [52, "LCD_CS", ""],
+    [53, "LCD_CLK", ""],
+    [54, "CAM_MCLK", ""],
+    [55, "CAM_RX0", ""],
+    [56, "CAM_RX1", ""],
+    [57, "UART3_TXD", ""],
+    [58, "UART3_RXD", ""],
+    [61, "VBUS", ""],
+    [66, "I2C1_SDA", ""],
+    [67, "I2C1_SCL", ""],
+    [78, "GPIO28", ""],
+    [79, "USIM_DET", ""],
+    [80, "CAM_BCLK", ""],
+    [81, "CAM_CS", ""],
+    [82, "USB_BOOT", ""],
+    [83, "SPI0_CS", ""],
+    [84, "SPI0_MISO", ""],
+    [85, "SPI0_MOSI", ""],
+    [86, "SPI0_CLK", ""],
+    [97, "GPIO16", ""],
+    [99, "GPIO23", ""],
+    [100, "GPIO17", ""],
+    [101, "WAKEUP0", ""],
+    [102, "GPIO20", ""],
+    [106, "CAN_RXD", ""],
+    [107, "GPIO21", ""]
+  ]
+}

+ 190 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirMICROSD_1010/readme.md

@@ -0,0 +1,190 @@
+## **功能模块介绍**
+
+本demo演示了在嵌入式环境中对TF卡(SD卡)的完整操作流程,覆盖了从文件系统挂载到高级文件操作的完整功能链。项目分为两个核心模块:
+
+1、main.lua:主程序入口 <br> 
+2、tfcard_app.lua:TF卡基础应用模块,实现文件系统管理、文件操作和目录管理功能。<br> 
+3、http_download_file.lua:HTTP下载模块,实现网络检测与文件下载到TF卡的功能
+
+## **演示功能概述**
+
+### 1、主程序入口模块(main.lua)
+
+- 初始化项目信息和版本号
+- 初始化看门狗,并定时喂狗
+- 启动一个循环定时器,每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
+- 加载tfcard_app模块(通过require "tfcard_app")
+- 加载http_download_file模块(通过require "http_download_file")
+- 最后运行sys.run()。
+
+
+
+### 3、TF卡核心演示模块(tfcard_app.lua)
+
+#### 文件系统管理
+
+- SPI初始化与挂载:
+  - 配置SPI接口参数(频率400kHz)
+  - 挂载FAT32文件系统到`/sd`路径
+  - 自动格式化检测与处理
+- 空间信息获取:
+  - 实时查询TF卡可用空间
+  - 输出详细存储信息(总空间/剩余空间)
+#### 文件操作
+- 创建目录:io.mkdir("/sd/io_test")
+- 创建/写入文件: io.open("/sd/io_test/boottime", "wb")
+- 检查文件存在: io.exists(file_path)
+- 获取文件大小:io.fileSize(file_path)
+- 读取文件内容: io.open(file_path, "rb"):read("*a")
+- 启动计数文件: 记录设备启动次数
+- 文件追加: io.open(append_file, "a+")
+- 按行读取: file:read("*l")
+- 文件关闭: file:close()
+- 文件重命名: os.rename(old_path, new_path)
+- 列举目录: io.lsdir(dir_path)
+- 删除文件: os.remove(file_path)
+- 删除目录: io.rmdir(dir_path)
+
+#### 结果处理
+
+- 资源清理(卸载/SPI关闭)
+
+### 4、HTTP下载功能 (http_download_file.lua)
+
+#### 文件系统管理
+
+- SPI初始化与挂载
+
+#### 网络就绪检测
+
+- 1秒循环等待IP就绪
+- 网络故障处理机制
+
+#### 安全下载
+
+- HTTP下载
+
+
+#### 结果处理
+
+- 下载状态码解析
+- 自动文件大小验证
+- 资源清理(卸载/spi关闭)
+
+
+## 演示硬件环境(二选一)
+
+### 1、Air780EHM核心板演示环境
+
+1、Air780EHM核心板一块(Air780EHM/780EGH/780EHV三种模块的核心板接线方式相同,这里以Air780EHM为例)
+
+2、TYPE-C USB数据线一根
+
+3、AirMICROSD_1010模块一个和SD卡一张
+
+4、Air780EHM/780EGH/780EHV核心板和数据线的硬件接线方式为
+
+- Air780EHM核心板通过TYPE-C USB口供电;(核心板USB旁边的开关拨到on一端)
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+5、Air780EHM核心板和AirMICROSD_1010模块接线方式
+
+|   Air780EHM     |    AirMICROSD_1010    |
+| --------------- | --------------------- |
+|  GND(任意)      |          GND          |
+|  VDD_EXT        |          3V3         |
+|  GPIO8/SPI0_CS  |        spi_cs       |
+|  SPI0_SLK       |        spi_clk,时钟       |
+|  SPI0_MOSI      |  spi_mosi,主机输出,从机输入|
+|  SPI0_MISO      |  spi_miso,主机输入,从机输出|
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具: https://docs.openluat.com/air780epm/common/Luatools/
+
+2、内核固件版本:
+Air780EHM:https://docs.openluat.com/air780epm/luatos/firmware/version/
+Air780EGH:https://docs.openluat.com/air780egh/luatos/firmware/version/
+Air780EHV:https://docs.openluat.com/air780ehv/luatos/firmware/version/
+
+
+## 演示核心步骤
+1、搭建好硬件环境
+
+2、通过Luatools将demo与固件烧录到核心板或开发板中
+
+3、烧录好后,板子开机将会在Luatools上看到如下打印:
+
+```lua
+(1)TF卡初始化与挂载
+[2025-08-24 19:51:24.152][000000001.389] SPI_HWInit 552:spi1 speed 2000000,1994805,154
+[2025-08-24 19:51:24.213][000000002.390] D/fatfs init sdcard at spi=1 cs=20
+[2025-08-24 19:51:24.286][000000002.390] SPI_SetNewConfig 996:spi1 speed 400000,400000
+[2025-08-24 19:51:24.329][000000002.408] SPI_SetNewConfig 996:spi1 speed 24000000,25600000
+[2025-08-24 19:51:24.383][000000002.408] D/SPI_TF 卡容量 122138624KB
+[2025-08-24 19:51:24.430][000000002.408] D/SPI_TF sdcard init OK OCR:0xc0ff8000!
+[2025-08-24 19:51:24.477][000000002.412] I/user.fatfs.mount 挂载成功 0
+[2025-08-24 19:51:24.535][000000002.617] I/user.fatfs getfree {"free_sectors":244262144,"total_kb":122132480,"free_kb":122131072,"total_sectors":244264960}
+[2025-08-24 19:51:24.583][000000002.618] I/user.fs lsmount [{"fs":"ec7xx","path":""},{"fs":"inline","path":"\/lua\/"},{"fs":"ram","path":"\/ram\/"},{"fs":"luadb","path":"\/luadb\/"},{"fs":"fatfs","path":"\/sd"}]
+
+
+(2)文件操作演示
+[2025-08-24 19:51:24.685][000000002.619] I/user.文件操作 ===== 开始文件操作 =====
+[2025-08-24 19:51:25.145][000000003.032] I/user.io.mkdir 目录创建成功 路径:/sd/io_test
+[2025-08-24 19:51:25.231][000000003.043] I/user.文件创建 文件写入成功 路径:/sd/io_test/boottime
+[2025-08-24 19:51:25.297][000000003.046] I/user.io.exists 文件存在 路径:/sd/io_test/boottime
+[2025-08-24 19:51:25.376][000000003.049] I/user.io.fileSize 文件大小:41字节 路径:/sd/io_test/boottime
+[2025-08-24 19:51:25.467][000000003.052] I/user.文件读取 路径:/sd/io_test/boottime 内容:这是io库API文档示例的测试内容
+[2025-08-24 19:51:25.547][000000003.056] I/user.启动计数 文件内容: 这是io库API文档示例的测试内容 十六进制: E8BF99E698AF696FE5BA93415049E69687E6A1A3E7A4BAE4BE8BE79A84E6B58BE8AF95E58685E5AEB9 82
+[2025-08-24 19:51:25.616][000000003.056] I/user.启动计数 当前值: 0
+[2025-08-24 19:51:25.693][000000003.057] I/user.启动计数 更新值: 1
+[2025-08-24 19:51:25.736][000000003.068] I/user.文件写入 路径:/sd/io_test/boottime 内容: 1
+[2025-08-24 19:51:25.795][000000003.081] I/user.文件创建 路径:/sd/io_test/test_a 初始内容:ABC
+[2025-08-24 19:51:25.852][000000003.088] I/user.文件追加 路径:/sd/io_test/test_a 追加内容:def
+[2025-08-24 19:51:25.909][000000003.091] I/user.文件验证 路径:/sd/io_test/test_a 内容:ABCdef 结果: 成功
+[2025-08-24 19:51:25.954][000000003.102] I/user.文件创建 路径:/sd/io_test/testline 写入3行文本
+[2025-08-24 19:51:26.001][000000003.106] I/user.按行读取 路径:/sd/io_test/testline 第1行: abc
+[2025-08-24 19:51:26.048][000000003.106] I/user.按行读取 路径:/sd/io_test/testline 第2行: 123
+[2025-08-24 19:51:26.093][000000003.107] I/user.按行读取 路径:/sd/io_test/testline 第3行: wendal
+[2025-08-24 19:51:26.140][000000003.112] I/user.os.rename 文件重命名成功 原路径:/sd/io_test/test_a 新路径:/sd/io_test/renamed_file.txt
+[2025-08-24 19:51:26.188][000000003.116] D/fatfs f_open /io_test/test_a 4
+[2025-08-24 19:51:26.238][000000003.116] D/vfs fopen /sd/io_test/test_a r not found
+[2025-08-24 19:51:26.312][000000003.117] I/user.验证结果 重命名验证成功 新文件存在 原文件不存在
+[2025-08-24 19:51:26.367][000000003.117] I/user.目录操作 ===== 开始目录列举 =====
+[2025-08-24 19:51:26.424][000000003.121] I/user.fs lsdir [{"name":"boottime","size":0,"type":0},{"name":"testline","size":0,"type":0},{"name":"renamed_file.txt","size":0,"type":0}]
+[2025-08-24 19:51:26.478][000000003.127] I/user.os.remove 文件删除成功 路径:/sd/io_test/renamed_file.txt
+[2025-08-24 19:51:26.539][000000003.129] D/fatfs f_open /io_test/renamed_file.txt 4
+[2025-08-24 19:51:26.593][000000003.130] D/vfs fopen /sd/io_test/renamed_file.txt r not found
+[2025-08-24 19:51:26.656][000000003.130] I/user.验证结果 renamed_file.txt文件删除验证成功
+[2025-08-24 19:51:26.734][000000003.137] I/user.os.remove testline文件删除成功 路径:/sd/io_test/testline
+[2025-08-24 19:51:26.856][000000003.139] D/fatfs f_open /io_test/testline 4
+[2025-08-24 19:51:26.922][000000003.140] D/vfs fopen /sd/io_test/testline r not found
+[2025-08-24 19:51:27.113][000000003.140] I/user.验证结果 testline文件删除验证成功
+[2025-08-24 19:51:27.197][000000003.147] I/user.os.remove 文件删除成功 路径:/sd/io_test/boottime
+[2025-08-24 19:51:27.251][000000003.149] D/fatfs f_open /io_test/boottime 4
+[2025-08-24 19:51:27.302][000000003.150] D/vfs fopen /sd/io_test/boottime r not found
+[2025-08-24 19:51:27.365][000000003.150] I/user.验证结果 boottime文件删除验证成功
+[2025-08-24 19:51:27.407][000000003.158] I/user.io.rmdir 目录删除成功 路径:/sd/io_test
+[2025-08-24 19:51:27.461][000000003.159] D/fatfs f_open /io_test 4
+[2025-08-24 19:51:27.536][000000003.159] D/vfs fopen /sd/io_test r not found
+[2025-08-24 19:51:27.610][000000003.159] I/user.验证结果 目录删除验证成功
+[2025-08-24 19:51:27.668][000000003.160] I/user.文件操作 ===== 文件操作完成 =====
+[2025-08-24 19:51:27.712][000000003.160] I/user.系统清理 开始执行关闭操作...
+[2025-08-24 19:51:27.772][000000003.160] I/user.文件系统 卸载成功
+[2025-08-24 19:51:27.867][000000003.160] I/user.SPI接口 已关闭
+
+
+(3)网络连接与HTTP下载
+[2025-08-24 20:31:49.405][000000006.268] I/user.HTTP下载 开始下载任务
+[2025-08-24 20:31:49.438][000000006.275] dns_run 674:gitee.com state 0 id 1 ipv6 0 use dns server2, try 0
+[2025-08-24 20:31:49.471][000000006.277] D/mobile TIME_SYNC 0
+[2025-08-24 20:31:49.503][000000006.297] dns_run 691:dns all done ,now stop
+[2025-08-24 20:31:54.800][000000012.080] I/user.HTTP下载 下载完成 success 200 
+[2025-08-24 20:31:54.872][000000012.080] {"Age":"0","Cache-Control":"public, max-age=60","Via":"1.1 varnish","Transfer-Encoding":"chunked","Date":"Sun, 24 Aug 2025 12:31:49 GMT","Access-Control-Allow-Credentials":"true","Vary":"Accept-Encoding","X-Served-By":"cache-ffe9","X-Gitee-Server":"http-pilot 1.9.21","Connection":"keep-alive","Server":"ADAS\/1.0.214","Access-Control-Allow-Headers":"Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With,X-CustomHeader,Content-Range,Range,Set-Language","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline'; sandbox","X-Request-Id":"1f7e4b55-53c8-440a-9806-8894aa823f50","Accept-Ranges":"bytes","Etag":"W\/\"6ea36a6c51a48eaba0ffbc01d409424e7627bc56\"","Content-Type":"text\/plain; charset=utf-8","Access-Control-Allow-Methods":"GET, POST, PUT, PATCH, DELETE, OPTIONS","X-Frame-Options":"DENY","X-Cache":"MISS","Set-Cookie":"BEC=1f1759df3ccd099821dcf0da6feb0357;Path=\/;Max-Age=126000"}
+[2025-08-24 20:31:54.910][000000012.080]  411922
+[2025-08-24 20:31:54.936][000000012.082] I/user.HTTP下载 文件大小验证 预期: 411922 实际: 411922
+[2025-08-24 20:31:54.979][000000012.083] I/user.HTTP下载 资源清理完成
+
+```

+ 333 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirMICROSD_1010/tfcard_app.lua

@@ -0,0 +1,333 @@
+--[[
+@module  tfcard_app
+@summary TF卡文件操作测试模块
+@version 1.0.0
+@date    2025.08.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初始化 ##########
+    -- 在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)
+
+
+    -- ########## 开始进行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
+    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
+        end
+    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
+    end
+
+    -- 3. 检查文件是否存在
+    if io.exists(file_path) then
+        log.info("io.exists", "文件存在", "路径:" .. file_path)
+    else
+        log.error("io.exists", "文件不存在", "路径:" .. file_path)
+        goto resource_cleanup
+    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
+    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
+    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("启动计数", "文件不存在或无法打开")
+
+    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)
+    else
+        log.error("文件写入", "无法打开文件", "路径:" .. file_path)
+        goto resource_cleanup
+    end
+
+    -- 7. 文件追加测试
+    append_file = dir_path .. "/test_a"
+    -- 清理旧文件
+    os.remove(append_file)
+
+    -- 创建并写入初始内容
+    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
+    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
+
+    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
+    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
+    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
+    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("验证结果", "重命名验证成功", "新文件存在", "原文件不存在")
+        else
+            log.error("验证结果", "重命名验证失败")
+        end
+    else
+        log.error("os.rename", "重命名失败", "错误:" .. tostring(err), "原路径:" .. old_path)
+        goto resource_cleanup
+    end
+
+    -- 10. 列举目录内容
+    log.info("目录操作", "===== 开始目录列举 =====")
+
+    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
+    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
+    end
+
+    -- 测试删除testline文件
+    if os.remove(line_file) then
+        log.info("os.remove", "testline文件删除成功", "路径:" .. line_file)
+
+        -- 验证删除结果
+        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
+    end
+
+    if os.remove(file_path) then
+        log.info("os.remove", "文件删除成功", "路径:" .. file_path)
+
+        -- 验证删除结果
+        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
+    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
+    end
+
+    log.info("文件操作", "===== 文件操作完成 =====")
+
+    -- ########## 功能: 收尾功能演示##########
+    -- 卸载文件系统和关闭SPI
+    ::resource_cleanup::
+
+    log.info("结束", "开始执行关闭操作...")  
+    -- 如已挂载需先卸载文件系统,未挂载直接关闭SPI
+    if mount_ok then
+        if fatfs.unmount("/sd") then
+            log.info("文件系统", "卸载成功")
+        else
+            log.error("文件系统", "卸载失败")
+        end
+    end
+
+    -- 2. 关闭SPI接口
+    spi.close(spi_id)
+    log.info("SPI接口", "已关闭")
+
+end
+
+sys.taskInit(tfcard_main_task)

+ 16 - 17
module/Air780EHM_Air780EHV_Air780EGH/demo/tf_card/readme.md

@@ -87,7 +87,7 @@
 
 2、TYPE-C USB数据线一根
 
-3、AirMICROSD_1000模块一个和SD卡一张
+3、AirMICROSD_1010模块一个和SD卡一张
 
 4、Air780EHM/780EGH/780EHV核心板和数据线的硬件接线方式为
 
@@ -95,16 +95,16 @@
 
 - TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
 
-5、Air780EHM核心板和AirMICROSD_1000模块接线方式
+5、Air780EHM核心板和AirMICROSD_1010模块接线方式
 
-|   Air780EHM     |    AirMICROSD_1000    |
+|   Air780EHM     |    AirMICROSD_1010    |
 | --------------- | --------------------- |
 |  GND(任意)      |          GND          |
-|  VDD_EXT        |          VCC          |
-|  GPIO8/SPI0_CS  |        CS,片选        |
-|  SPI0_SLK       |        CLK,时钟       |
-|  SPI0_MOSI      |  MOSI,主机输出,从机输入|
-|  SPI0_MISO      |  MISO,主机输入,从机输出|
+|  VDD_EXT        |          3V3         |
+|  GPIO8/SPI0_CS  |        spi_cs       |
+|  SPI0_SLK       |        spi_clk,时钟       |
+|  SPI0_MOSI      |  spi_mosi,主机输出,从机输入|
+|  SPI0_MISO      |  spi_miso,主机输入,从机输出|
 
 ### 2、Air780EHM开发板演示环境
 
@@ -112,7 +112,7 @@
 
 2、TYPE-C USB数据线一根
 
-3、AirMICROSD_1000模块一个和SD卡一张
+3、AirMICROSD_1010模块一个和SD卡一张
 
 4、Air780EHM/780EGH/780EHV开发板和数据线的硬件接线方式为
 
@@ -120,16 +120,15 @@
 
 - TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
 
-5、Air780EHM开发板和AirMICROSD_1000模块接线方式
-
-|   Air780EHM     |    AirMICROSD_1000    |
+5、Air780EHM开发板和AirMICROSD_1010模块接线方式
+|   Air780EHM     |    AirMICROSD_1010    |
 | --------------- | --------------------- |
 |  GND(任意)      |          GND          |
-|  VDD_EXT        |          VCC          |
-|  GPIO16/SPI0_CS  |        CS,片选        |
-|  SPI0_SLK       |        CLK,时钟       |
-|  SPI0_MOSI      |  MOSI,主机输出,从机输入|
-|  SPI0_MISO      |  MISO,主机输入,从机输出|
+|  VDD_EXT        |          3V3         |
+|  GPIO16/SPI0_CS  |        spi_cs       |
+|  SPI0_SLK       |        spi_clk,时钟       |
+|  SPI0_MOSI      |  spi_mosi,主机输出,从机输入|
+|  SPI0_MISO      |  spi_miso,主机输入,从机输出|
 
 ## 演示软件环境
 

+ 4 - 5
module/Air780EHM_Air780EHV_Air780EGH/demolib/adxl34x.lua

@@ -135,11 +135,10 @@ log.info("adxl34x_data", "adxl34x_data.x"..(adxl34x_data.x),"adxl34x_data.y"..(a
 function adxl34x.get_data()
     local accel={x=nil,y=nil,z=nil}
     i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAX0)
-    _,accel.x = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
-    i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAY0)
-    _,accel.y = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
-    i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAZ0)
-    _,accel.z = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
+    local rcvData = i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 6)
+    if rcvData ~= nil and #rcvData == 6 then
+        accel.x, accel.y, accel.z = string.unpack("<hhh", rcvData)
+    end
     return accel
 end
 

+ 4 - 4
module/Air780EHM_Air780EHV_Air780EGH/demolib/spl06.lua

@@ -79,11 +79,11 @@ local function i2cReadTwoData(i2caddr,regaddr)
         msb = 0
         lsb = 0
         log.info("spl06 two data","the data is null")
-        return msb256+lsb
+        return msb*256+lsb
     else
     --log.info("spl06 two data","msb:"..msb.." lsb:"..lsb)
         if regaddr ==0x10 then
-            return msb16 + bit.rshift((lsb&0xF0),4)
+            return msb*16 + bit.rshift((lsb&0xF0),4)
         elseif regaddr ==0x11 then
             return (msb&0x0f)*256 + lsb
         else
@@ -106,7 +106,7 @@ local function chip_check()
         if revData:byte() ~= nil then
             SPL06_ADDRESS_ADR = SPL06_ADDRESS_ADR_LOW
         else
-            log.info("i2c", "Can't find adxl34x device")
+            log.info("i2c", "Can't find SPL06 device")
             return false
         end
     end
@@ -130,7 +130,7 @@ spl06初始化
 @usage
 spl06.init(0)
 ]]
-function spl06.init(i2cid)
+function spl06.init(i2c_id)
     i2cid = i2c_id
     sys.wait(20)--20 毫秒等待设备稳定
     if chip_check() then

+ 89 - 89
module/Air780EPM/demo/780EPM视频教程示例代码/25_RS485实战演练/Radar_485.lua

@@ -69,6 +69,95 @@ end
 --=============================================================
 --采集雷达数据
 local function get_radar_date()
+    --=============================================================
+    --注册串口事件回调
+    uart.on(uartid, "receive", function(id, len)
+        local s = ""
+        repeat --repeadt类似C语言的do…while循环,repeat重复执行循环,直到until指定条件为真
+            s = uart.read(id, len)
+            --log.info("uart", "receive", id, #s, s)
+            log.info("串口2", "receive", id, #s, s:toHex())
+            --log.info("第三个数string.byte(s,3)=", string.byte(s,3))
+            if #s > 0  then -- #s 是取字符串的长度,string.byte(s,3)==8判断师回复的那条命令
+                -- 如果传输二进制/十六进制数据, 部分字符不可见, 不代表没收到
+                -- 关于收发hex值,请查阅 https://doc.openluat.com/article/583
+                log.info("第三个数string.byte(s,3)=", string.byte(s,3))
+                local crc16=crc16(s,#s-2)
+                local crc16_high=crc16 >> 8
+                local crc16_low=crc16 & 0xFF
+                log.info("CRC校验高位",crc16_high,"CRC校验低位",crc16_low)
+                --打印其hex字符串形式
+                --数据转换为数值string.byte,用于crc校验
+                local receive_crc_high = string.byte(s,#s-1)
+                local receive_crc_low = string.byte(s,#s)
+                local receive_Switch_state = string.byte(s,#s-2)
+                log.info("接收到的CRC校验高位", "接收到的CRC校验低位", receive_crc_high, receive_crc_low)
+                log.info("uart", "receive", id, #s, s:toHex())
+                --=============================================================
+                --识别读取数据并将数据赋值给空高料高变量
+                if crc16_high==receive_crc_high and crc16_low==receive_crc_low and string.byte(s,3)==20 then--物位计的判断
+                --if crc16_high==receive_crc_high and crc16_low==receive_crc_low and string.byte(s,3)==16 then--判断10数据
+                    --注意,string.byte(s,3)==16,收到字符串的第3个数据,判断时要转换为十进制数,第三个数为0x10,十进制数为16--低功耗雷达
+                    --注意,string.byte(s,3)==16,收到字符串的第3个数据,判断时要转换为十进制数,第三个数为0x14,十进制数为20--四线制雷达
+                    log.info("crc检验通过数据合法")
+                    --先将字符串转为hex格式
+                    local hexStr, len = string.toHex(s) -- 返回值"3132",2,后面的2是长度
+                    --log.info("收到的16进制字符串=",hexStr,len)
+                    --print(hexStr,len) -- 将输出 3132
+                    --截取空高数据,并加上0x个前缀
+                    --local kong_high="0x"..string.sub(hexStr,7,14)--将0x拼接到字符串上,string.sub(hexStr,7,14)是一个数字算一个(低功耗雷达)
+                    local kong_high="0x"..string.sub(hexStr,11,18)--物位计
+                    --截取料高数据,并加上0x个前缀
+                    --local liao_high="0x"..string.sub(hexStr,15,22)--将0x拼接到字符串上(低功耗雷达)
+                    local liao_high="0x"..string.sub(hexStr,19,26)--物位计
+                    --信噪比
+                    --local SNR_Date="0x"..string.sub(hexStr,23,30)--将0x拼接到字符串上(低功耗雷达)
+                    local SNR_Date="0x"..string.sub(hexStr,27,34)--物位计
+                    --雷达温度
+                    --local temp_Date="0x"..string.sub(hexStr,31,38)--将0x拼接到字符串上(低功耗雷达)
+                    local temp_Date="0x"..string.sub(hexStr,35,42)--物位计
+                    log.info("空高16进制字符串=",kong_high)
+                    log.info("料高16进制字符串=",liao_high)
+                    log.info("信噪比16进制字符串=",SNR_Date)
+                    log.info("雷达温度比16进制字符串=",temp_Date)
+                    --将hex格式转化为浮点型数据
+                    local kong_temp = string.pack("<L",kong_high)
+                    kong_Value_485 = string.unpack("f",kong_temp)
+                    log.info("空高kong_Value_485=",kong_Value_485)
+                    local liao_temp = string.pack("<L",liao_high)
+                    liao_Value_485= string.unpack("f",liao_temp)
+                    log.info("料高liao_Value_485=",liao_Value_485)
+                    local SNR_temp = string.pack("<L",SNR_Date)
+                    SNR_485 = string.unpack("f",SNR_temp)
+                    log.info("信噪比SNR_485=",SNR_485)
+                    local xinhao_temp = string.pack("<L",temp_Date)
+                    Radar_temp_485 = string.unpack("f",xinhao_temp)
+                    log.info("雷达温度Radar_temp_485=",Radar_temp_485)                               
+                    --uart.write(1, s)--正常采集不透传给串口1,也就是传给蓝牙
+                    log.info("发布雷达完成消息", "receive", id, #s, s)
+                    sys.publish("雷达完成")
+                --=============================================================
+                --如果不是采集命令,则透传给串口1
+                elseif crc16_high==receive_crc_high and crc16_low==receive_crc_low then
+                    log.info("串口2透传给串口1数据", "receive", id, #s, s)
+                    -- log.info("uart", "receive", id, #s, s:toHex())
+                    uart.write(1, s)--暂时不透传给串口1
+                elseif crc16_high~=receive_crc_high or crc16_low~=receive_crc_low then
+                    log.info("无数据或crc检验不通过")
+                    kong_Value_485=-100--空高
+                    liao_Value_485=-100--料高
+                    uart.write(1, s)--暂时不透传给串口1
+                    --两种情况,一种是采集失败,一种是采集回波曲线
+                    end
+            end
+            if #s == len then
+                --log.info("程序经过这里1")
+                break
+            end
+        until s == ""
+        --log.info("程序经过这里2")
+    end)
+    --=============================================================
     while true do
         --sys.waitUntil("读雷达")
         log.info("读485空高数值")
@@ -77,95 +166,6 @@ local function get_radar_date()
         -- log.info("串口发送完成",Read_Radar_Date)
         uart.write(uartid,Read_4_Date)
         log.info("485雷达采集发送完成",Read_4_Date)
-        --=============================================================
-        --注册串口事件回调
-        uart.on(uartid, "receive", function(id, len)
-            local s = ""
-            repeat --repeadt类似C语言的do…while循环,repeat重复执行循环,直到until指定条件为真
-                s = uart.read(id, len)
-                --log.info("uart", "receive", id, #s, s)
-                log.info("串口2", "receive", id, #s, s:toHex())
-                --log.info("第三个数string.byte(s,3)=", string.byte(s,3))
-                if #s > 0  then -- #s 是取字符串的长度,string.byte(s,3)==8判断师回复的那条命令
-                    -- 如果传输二进制/十六进制数据, 部分字符不可见, 不代表没收到
-                    -- 关于收发hex值,请查阅 https://doc.openluat.com/article/583
-                    log.info("第三个数string.byte(s,3)=", string.byte(s,3))
-                    local crc16=crc16(s,#s-2)
-                    local crc16_high=crc16 >> 8
-                    local crc16_low=crc16 & 0xFF
-                    log.info("CRC校验高位",crc16_high,"CRC校验低位",crc16_low)
-                    --打印其hex字符串形式
-                    --数据转换为数值string.byte,用于crc校验
-                    local receive_crc_high = string.byte(s,#s-1)
-                    local receive_crc_low = string.byte(s,#s)
-                    local receive_Switch_state = string.byte(s,#s-2)
-                    log.info("接收到的CRC校验高位", "接收到的CRC校验低位", receive_crc_high, receive_crc_low)
-                    log.info("uart", "receive", id, #s, s:toHex())
-                    --=============================================================
-                    --识别读取数据并将数据赋值给空高料高变量
-                    if crc16_high==receive_crc_high and crc16_low==receive_crc_low and string.byte(s,3)==20 then--物位计的判断
-                    --if crc16_high==receive_crc_high and crc16_low==receive_crc_low and string.byte(s,3)==16 then--判断10数据
-                        --注意,string.byte(s,3)==16,收到字符串的第3个数据,判断时要转换为十进制数,第三个数为0x10,十进制数为16--低功耗雷达
-                        --注意,string.byte(s,3)==16,收到字符串的第3个数据,判断时要转换为十进制数,第三个数为0x14,十进制数为20--四线制雷达
-                        log.info("crc检验通过数据合法")
-                        --先将字符串转为hex格式
-                        local hexStr, len = string.toHex(s) -- 返回值"3132",2,后面的2是长度
-                        --log.info("收到的16进制字符串=",hexStr,len)
-                        --print(hexStr,len) -- 将输出 3132
-                        --截取空高数据,并加上0x个前缀
-                        --local kong_high="0x"..string.sub(hexStr,7,14)--将0x拼接到字符串上,string.sub(hexStr,7,14)是一个数字算一个(低功耗雷达)
-                        local kong_high="0x"..string.sub(hexStr,11,18)--物位计
-                        --截取料高数据,并加上0x个前缀
-                        --local liao_high="0x"..string.sub(hexStr,15,22)--将0x拼接到字符串上(低功耗雷达)
-                        local liao_high="0x"..string.sub(hexStr,19,26)--物位计
-                        --信噪比
-                        --local SNR_Date="0x"..string.sub(hexStr,23,30)--将0x拼接到字符串上(低功耗雷达)
-                        local SNR_Date="0x"..string.sub(hexStr,27,34)--物位计
-                        --雷达温度
-                        --local temp_Date="0x"..string.sub(hexStr,31,38)--将0x拼接到字符串上(低功耗雷达)
-                        local temp_Date="0x"..string.sub(hexStr,35,42)--物位计
-                        log.info("空高16进制字符串=",kong_high)
-                        log.info("料高16进制字符串=",liao_high)
-                        log.info("信噪比16进制字符串=",SNR_Date)
-                        log.info("雷达温度比16进制字符串=",temp_Date)
-                        --将hex格式转化为浮点型数据
-                        local kong_temp = string.pack("<L",kong_high)
-                        kong_Value_485 = string.unpack("f",kong_temp)
-                        log.info("空高kong_Value_485=",kong_Value_485)
-                        local liao_temp = string.pack("<L",liao_high)
-                        liao_Value_485= string.unpack("f",liao_temp)
-                        log.info("料高liao_Value_485=",liao_Value_485)
-                        local SNR_temp = string.pack("<L",SNR_Date)
-                        SNR_485 = string.unpack("f",SNR_temp)
-                        log.info("信噪比SNR_485=",SNR_485)
-                        local xinhao_temp = string.pack("<L",temp_Date)
-                        Radar_temp_485 = string.unpack("f",xinhao_temp)
-                        log.info("雷达温度Radar_temp_485=",Radar_temp_485)                               
-                        --uart.write(1, s)--正常采集不透传给串口1,也就是传给蓝牙
-                        log.info("发布雷达完成消息", "receive", id, #s, s)
-                        sys.publish("雷达完成")
-                    --=============================================================
-                    --如果不是采集命令,则透传给串口1
-                    elseif crc16_high==receive_crc_high and crc16_low==receive_crc_low then
-                        log.info("串口2透传给串口1数据", "receive", id, #s, s)
-                        -- log.info("uart", "receive", id, #s, s:toHex())
-                        uart.write(1, s)--暂时不透传给串口1
-                    elseif crc16_high~=receive_crc_high or crc16_low~=receive_crc_low then
-                        log.info("无数据或crc检验不通过")
-                        kong_Value_485=-100--空高
-                        liao_Value_485=-100--料高
-                        uart.write(1, s)--暂时不透传给串口1
-                        --两种情况,一种是采集失败,一种是采集回波曲线
-                        end
-                end
-                if #s == len then
-                    --log.info("程序经过这里1")
-                    break
-                end
-            until s == ""
-            --log.info("程序经过这里2")
-        end)
-        --=============================================================
     end
 end
 --=============================================================

+ 4 - 5
module/Air780EPM/demolib/adxl34x.lua

@@ -135,11 +135,10 @@ log.info("adxl34x_data", "adxl34x_data.x"..(adxl34x_data.x),"adxl34x_data.y"..(a
 function adxl34x.get_data()
     local accel={x=nil,y=nil,z=nil}
     i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAX0)
-    _,accel.x = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
-    i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAY0)
-    _,accel.y = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
-    i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAZ0)
-    _,accel.z = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
+    local rcvData = i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 6)
+    if rcvData ~= nil and #rcvData == 6 then
+        accel.x, accel.y, accel.z = string.unpack("<hhh", rcvData)
+    end
     return accel
 end
 

+ 4 - 4
module/Air780EPM/demolib/spl06.lua

@@ -79,11 +79,11 @@ local function i2cReadTwoData(i2caddr,regaddr)
         msb = 0
         lsb = 0
         log.info("spl06 two data","the data is null")
-        return msb256+lsb
+        return msb*256+lsb
     else
     --log.info("spl06 two data","msb:"..msb.." lsb:"..lsb)
         if regaddr ==0x10 then
-            return msb16 + bit.rshift((lsb&0xF0),4)
+            return msb*16 + bit.rshift((lsb&0xF0),4)
         elseif regaddr ==0x11 then
             return (msb&0x0f)*256 + lsb
         else
@@ -106,7 +106,7 @@ local function chip_check()
         if revData:byte() ~= nil then
             SPL06_ADDRESS_ADR = SPL06_ADDRESS_ADR_LOW
         else
-            log.info("i2c", "Can't find adxl34x device")
+            log.info("i2c", "Can't find SPL06 device")
             return false
         end
     end
@@ -130,7 +130,7 @@ spl06初始化
 @usage
 spl06.init(0)
 ]]
-function spl06.init(i2cid)
+function spl06.init(i2c_id)
     i2cid = i2c_id
     sys.wait(20)--20 毫秒等待设备稳定
     if chip_check() then

+ 78 - 0
module/Air8000/demo/accessory_board/AirMICROSD_1010/http_download_file.lua

@@ -0,0 +1,78 @@
+--[[
+@module http_download_file
+@summary http下载文件模块
+@version 1.0.0
+@date    2025.08.25
+@author  王棚嶙
+@usage
+本文件演示的功能为通过http下载文件进入TF卡中:
+1. 网络就绪检测
+2. 创建HTTP下载任务并等待完成
+3. 记录下载结果
+4. 获取并记录文件大小
+本文件没有对外接口,直接在main.lua中require "http_download_file"即可
+]] 
+
+
+local function http_download_file_task()
+
+    -- 阶段1: 网络就绪检测
+
+    while not socket.adapter(socket.dft()) do
+        log.warn("HTTP下载", "等待网络连接", socket.dft())
+        -- 等待IP_READY消息,超时设为1秒
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("HTTP下载", "网络已就绪", socket.dft())
+
+    -- 进行SPI初始化,Air8000核心板TF卡的CS脚为:SPI1,GPIO12
+    local spi_id, pin_cs = 1, 12 
+    spi.setup(spi_id, nil, 0, 0, 400 * 1000)
+    gpio.setup(pin_cs, 1)
+    -- 挂载文件系统
+    local mount_ok = fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)
+    if not mount_ok then
+        log.error("HTTP下载", "文件系统挂载失败")
+        fatfs.unmount("/sd")
+        spi.close(spi_id)
+        return
+    end
+
+    -- 阶段2: 执行下载任务
+    log.info("HTTP下载", "开始下载任务")
+
+    -- 核心下载操作开始 (支持http和https)
+    --local code, headers, body = http.request("GET", "...", nil, nil, {dst = "/sd/1.mp3"}).wait()
+    -- 其中 "..."为url地址, 支持 http和https, 支持域名, 支持自定义端口。
+    local code, headers, body_size = http.request("GET",
+                                    "https://gitee.com/openLuat/LuatOS/raw/master/module/Air780EHM_Air780EHV_Air780EGH/demo/audio/1.mp3",
+                                    nil, nil, {dst = "/sd/1.mp3"}).wait()
+    -- 阶段3: 记录下载结果
+    log.info("HTTP下载", "下载完成", 
+        code==200 and "success" or "error", 
+        code, 
+        -- headers是下载的文件头信息
+        json.encode(headers or {}), 
+        -- body_size是下载的文件大小(字节数)
+        body_size) 
+        
+    if code == 200 then
+        -- 获取实际文件大小
+        local actual_size = io.fileSize("/sd/1.mp3")
+        log.info("HTTP下载", "文件大小验证", "预期:", body_size, "实际:", actual_size)
+        
+        if actual_size~= body_size then
+            log.error("HTTP下载", "文件大小不一致", "预期:", body_size, "实际:", actual_size)
+        end
+    end
+
+    -- 阶段4: 资源清理
+    fatfs.unmount("/sd")
+    spi.close(spi_id)
+    log.info("HTTP下载", "资源清理完成")
+end
+
+-- 创建下载任务
+sys.taskInit(http_download_file_task)

+ 96 - 0
module/Air8000/demo/accessory_board/AirMICROSD_1010/http_upload_file.lua

@@ -0,0 +1,96 @@
+--[[
+@module http_upload_file
+@summary TF卡大文件httpplus上传模块
+@version 1.0.0
+@date 2025.08.25
+@author 王棚嶙
+@usage
+本文件演示通过httpplus库将TF卡中的大文件上传到HTTP服务器:
+1. 网络就绪检测
+2. TF卡文件系统挂载
+3. 大文件上传功能
+4. 上传结果记录
+本文件没有对外接口,直接在main.lua中require "http_upload_file"即可
+]]
+-- 加载httpplus扩展库,不可省略
+local httpplus = require "httpplus"
+
+local function http_upload_task()
+    -- 阶段1: 网络就绪检测
+    while not socket.adapter(socket.dft()) do
+        log.warn("HTTP上传", "等待网络连接", socket.dft())
+        -- 待IP_READY消息,超时设为1秒
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("HTTP上传", "网络已就绪", socket.dft())
+
+    -- 阶段2: TF卡文件系统初始化
+    local spi_id, pin_cs = 1, 12
+    spi.setup(spi_id, nil, 0, 0, 400 * 1000)
+    gpio.setup(pin_cs, 1)
+    
+    local mount_ok = fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)
+    if not mount_ok then
+        log.error("HTTP上传", "文件系统挂载失败")
+        fatfs.unmount("/sd")
+        spi.close(spi_id)
+        return
+    end
+
+    -- 阶段3: 检查要上传的文件是否存在
+    -- 替换为实际的文件路径
+    local upload_file_path = "/sd/30M_test.txt" 
+    if not io.exists(upload_file_path) then
+        log.error("HTTP上传", "要上传的文件不存在", upload_file_path)
+        fatfs.unmount("/sd")
+        spi.close(spi_id)
+        return
+    end
+
+    -- 获取文件大小
+    local file_size = io.fileSize(upload_file_path)
+    log.info("HTTP上传", "准备上传文件", upload_file_path, "大小:", file_size, "字节")
+
+    -- 阶段4: 执行文件上传
+    log.info("HTTP上传", "开始上传任务")
+    
+    -- 使用httpplus库上传文件,参考httpplus_app_post_file的实现
+    -- hhtplus.request接口支持单文件上传、多文件上传、单文本上传、多文本上传、单/多文本+单/多文件上传
+    -- http://airtest.openluat.com:2900/uploadFileToStatic 仅支持单文件上传,并且上传的文件name必须使用"uploadFile"
+    -- 所以此处仅演示了单文件上传功能,并且"uploadFile"不能改成其他名字,否则会出现上传失败的应答
+    local code, response = httpplus.request({
+        url = "http://airtest.openluat.com:2900/uploadFileToStatic",
+        files = {
+            -- 服务器要求文件名必须为"uploadFile"
+            ["uploadFile"] = upload_file_path, 
+        },
+    })
+
+    -- 阶段5: 记录上传结果
+    log.info("HTTP上传", "上传完成", 
+        code == 200 and "success" or "error", 
+        code)
+    
+    if code == 200 then
+        log.info("HTTP上传", "服务器响应头", json.encode(response.headers or {}))
+        local body = response.body and response.body:query()
+        log.info("HTTP上传", "服务器响应体长度", body and body:len() or 0)
+        
+        -- 可以进一步解析服务器响应
+        if body then
+            log.info("HTTP上传", "服务器响应内容", body:len() > 512 and "内容过长,不显示" or body)
+        end
+    else
+        log.error("HTTP上传", "上传失败", code)
+    end
+
+    -- 阶段6: 资源清理
+    fatfs.unmount("/sd")
+    spi.close(spi_id)
+    log.info("HTTP上传", "资源清理完成")
+end
+
+-- 创建上传任务
+sys.taskInit(http_upload_task)

+ 92 - 0
module/Air8000/demo/accessory_board/AirMICROSD_1010/main.lua

@@ -0,0 +1,92 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 001.000.000
+@date    2025.08.25
+@author  王棚嶙
+@usage
+本 Demo 完整覆盖了 TF 卡操作的核心到高级流程,HTTP下载功能包括:
+1. 基础操作:
+   - CH390 供电控制
+   - 看门狗守护机制 
+2. 挂载及文件操作:
+   - 文件系统挂载/卸载
+   - TF卡空间信息查询
+   - 文件创建/读写/追加
+   - 目录创建/删除
+   - 文件重命名/删除
+   - 文件存在性检查与大小获取
+3. 下载功能:
+   - 网络检测与HTTP文件下载
+更多说明参考本目录下的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 = "tfcard"
+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)
+
+
+
+
+--[[在加载以下三个功能时,建议分别打开进行测试,因为文件操作,http下载功能和http大文件上传功能是异步操作。
+放到一个项目中,如果加载的时间点是随机的,就会出现tfcard_app在spi.setup和fatfs挂载文件系统之后,
+还没有释放资源,然后http_download_file或http_upload_file又去重复spi.setup和fatfs挂载文件系统了,
+不符合正常的业务逻辑,用户在参考编程的时候也要注意。]]
+
+--加载tf卡测试应用模块
+require "tfcard_app"
+--加载HTTP下载存入TF卡功能演示模块
+--require "http_download_file"
+--加载HTTP上传文件到服务器的功能演示模块
+--require "http_upload_file"
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 54 - 0
module/Air8000/demo/accessory_board/AirMICROSD_1010/pins_Air8000.json

@@ -0,0 +1,54 @@
+{
+  "model": "Air8000",
+  "pins": [
+    [1, "USB_BOOT", ""],
+    [2, "VBUS", ""],
+    [14, "PWR_KEY", ""],
+    [16, "UART1_TXD", ""],
+    [17, "UART1_RXD", ""],
+    [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", "配置为spi1_sclk"],
+    [39, "SPI1_MISO", "配置为spi1_miso"],
+    [40, "SPI1_MOSI", "配置为spi1_mosi"],
+    [41, "SPI1_CS", "配置为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", ""]
+  ]
+}

+ 238 - 0
module/Air8000/demo/accessory_board/AirMICROSD_1010/readme.md

@@ -0,0 +1,238 @@
+## **功能模块介绍**
+
+本demo演示了在嵌入式环境中对TF卡(SD卡)的完整操作流程,覆盖了从文件系统挂载到高级文件操作的完整功能链。项目分为两个核心模块:
+
+1、main.lua:主程序入口 <br> 
+2、tfcard_app.lua:TF卡基础应用模块,实现文件系统管理、文件操作和目录管理功能<br> 
+3、http_download_file.lua:HTTP下载模块,实现网络检测与文件下载到TF卡的功能<br>
+4、http_upload_file.lua:HTTP下载模块,实现网络检测与tf卡内大文件上传服务器的功能
+
+## **演示功能概述**
+
+### 1、主程序入口模块(main.lua)
+
+- 初始化项目信息和版本号
+- 初始化看门狗,并定时喂狗
+- 启动一个循环定时器,每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况方便分析内存使用是否有异常
+- 加载tfcard_app模块(通过require "tfcard_app")
+- 加载http_download_file模块(通过require "http_download_file")
+- 最后运行sys.run()。
+
+### 3、TF卡核心演示模块(tfcard_app.lua)
+
+#### 文件系统管理
+
+- SPI初始化与挂载:
+  - 配置SPI接口参数(频率400kHz)
+  - 挂载FAT32文件系统到`/sd`路径
+  - 自动格式化检测与处理
+- 空间信息获取:
+  - 实时查询TF卡可用空间
+  - 输出详细存储信息(总空间/剩余空间)
+#### 文件操作
+- 创建目录:io.mkdir("/sd/io_test")
+- 创建/写入文件: io.open("/sd/io_test/boottime", "wb")
+- 检查文件存在: io.exists(file_path)
+- 获取文件大小:io.fileSize(file_path)
+- 读取文件内容: io.open(file_path, "rb"):read("*a")
+- 启动计数文件: 记录设备启动次数
+- 文件追加: io.open(append_file, "a+")
+- 按行读取: file:read("*l")
+- 文件关闭: file:close()
+- 文件重命名: os.rename(old_path, new_path)
+- 列举目录: io.lsdir(dir_path)
+- 删除文件: os.remove(file_path)
+- 删除目录: io.rmdir(dir_path)
+
+#### 结果处理
+
+- 资源清理(卸载/SPI关闭)
+
+### 4、HTTP下载功能 (http_download_file.lua)
+
+#### 文件系统管理
+
+- SPI初始化与挂载
+
+#### 网络就绪检测
+
+- 1秒循环等待IP就绪
+- 网络故障处理机制
+
+#### 安全下载
+
+- HTTP下载
+
+#### 结果处理
+
+- 下载状态码解析
+- 自动文件大小验证
+- 资源清理(卸载/spi关闭)
+
+### 5、HTTP上传功能 (http_download_file.lua)
+
+#### 加载扩展库
+
+- require("httpplus")
+
+#### 网络就绪检测
+
+- 1秒循环等待IP就绪
+
+#### 文件系统管理
+
+- SPI初始化与挂载
+
+- 确认文件存在
+
+
+#### 安全上传
+
+- HTTP上传
+
+#### 结果处理
+
+- 解析服务器响应
+- 资源清理(卸载/spi关闭)
+
+## **演示硬件环境**
+
+1、Air8000核心板一块(Air8000系列模块的核心板接线方式相同,这里以Air8000为例)
+
+2、TYPE-C USB数据线一根
+
+3、AirMICROSD_1010模块一个和SD卡一张
+
+4、Air8000系列核心板和数据线的硬件接线方式为
+
+- Air8000核心板通过TYPE-C USB口供电;(核心板USB旁边的开关拨到供电一端)
+
+- Air8000核心板背面的拨码开关拨到USB ON
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+5、Air8000核心板和AirMICROSD_1010模块接线方式
+
+|   Air8000核心板    |    AirMICROSD_1010    |
+| --------------- | --------------------- |
+|  GND(任意)      |          GND          |
+|  VDD_EXT        |          3V3         |
+|  GPIO12/SPI1_CS  |        spi_cs         |
+|  SPI1_SLK       |        spi_clk,时钟       |
+|  SPI1_MOSI      |  spi_mosi,主机输出,从机输入|
+|  SPI1_MISO      |  spi_miso,主机输入,从机输出|
+
+## **演示软件环境**
+
+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)TF卡初始化与挂载
+[2025-08-24 19:51:24.152][000000001.389] SPI_HWInit 552:spi1 speed 2000000,1994805,154
+[2025-08-24 19:51:24.213][000000002.390] D/fatfs init sdcard at spi=1 cs=20
+[2025-08-24 19:51:24.286][000000002.390] SPI_SetNewConfig 996:spi1 speed 400000,400000
+[2025-08-24 19:51:24.329][000000002.408] SPI_SetNewConfig 996:spi1 speed 24000000,25600000
+[2025-08-24 19:51:24.383][000000002.408] D/SPI_TF 卡容量 122138624KB
+[2025-08-24 19:51:24.430][000000002.408] D/SPI_TF sdcard init OK OCR:0xc0ff8000!
+[2025-08-24 19:51:24.477][000000002.412] I/user.fatfs.mount 挂载成功 0
+[2025-08-24 19:51:24.535][000000002.617] I/user.fatfs getfree {"free_sectors":244262144,"total_kb":122132480,"free_kb":122131072,"total_sectors":244264960}
+[2025-08-24 19:51:24.583][000000002.618] I/user.fs lsmount [{"fs":"ec7xx","path":""},{"fs":"inline","path":"\/lua\/"},{"fs":"ram","path":"\/ram\/"},{"fs":"luadb","path":"\/luadb\/"},{"fs":"fatfs","path":"\/sd"}]
+
+
+(2)文件操作演示
+[2025-08-24 19:51:24.685][000000002.619] I/user.文件操作 ===== 开始文件操作 =====
+[2025-08-24 19:51:25.145][000000003.032] I/user.io.mkdir 目录创建成功 路径:/sd/io_test
+[2025-08-24 19:51:25.231][000000003.043] I/user.文件创建 文件写入成功 路径:/sd/io_test/boottime
+[2025-08-24 19:51:25.297][000000003.046] I/user.io.exists 文件存在 路径:/sd/io_test/boottime
+[2025-08-24 19:51:25.376][000000003.049] I/user.io.fileSize 文件大小:41字节 路径:/sd/io_test/boottime
+[2025-08-24 19:51:25.467][000000003.052] I/user.文件读取 路径:/sd/io_test/boottime 内容:这是io库API文档示例的测试内容
+[2025-08-24 19:51:25.547][000000003.056] I/user.启动计数 文件内容: 这是io库API文档示例的测试内容 十六进制: E8BF99E698AF696FE5BA93415049E69687E6A1A3E7A4BAE4BE8BE79A84E6B58BE8AF95E58685E5AEB9 82
+[2025-08-24 19:51:25.616][000000003.056] I/user.启动计数 当前值: 0
+[2025-08-24 19:51:25.693][000000003.057] I/user.启动计数 更新值: 1
+[2025-08-24 19:51:25.736][000000003.068] I/user.文件写入 路径:/sd/io_test/boottime 内容: 1
+[2025-08-24 19:51:25.795][000000003.081] I/user.文件创建 路径:/sd/io_test/test_a 初始内容:ABC
+[2025-08-24 19:51:25.852][000000003.088] I/user.文件追加 路径:/sd/io_test/test_a 追加内容:def
+[2025-08-24 19:51:25.909][000000003.091] I/user.文件验证 路径:/sd/io_test/test_a 内容:ABCdef 结果: 成功
+[2025-08-24 19:51:25.954][000000003.102] I/user.文件创建 路径:/sd/io_test/testline 写入3行文本
+[2025-08-24 19:51:26.001][000000003.106] I/user.按行读取 路径:/sd/io_test/testline 第1行: abc
+[2025-08-24 19:51:26.048][000000003.106] I/user.按行读取 路径:/sd/io_test/testline 第2行: 123
+[2025-08-24 19:51:26.093][000000003.107] I/user.按行读取 路径:/sd/io_test/testline 第3行: wendal
+[2025-08-24 19:51:26.140][000000003.112] I/user.os.rename 文件重命名成功 原路径:/sd/io_test/test_a 新路径:/sd/io_test/renamed_file.txt
+[2025-08-24 19:51:26.188][000000003.116] D/fatfs f_open /io_test/test_a 4
+[2025-08-24 19:51:26.238][000000003.116] D/vfs fopen /sd/io_test/test_a r not found
+[2025-08-24 19:51:26.312][000000003.117] I/user.验证结果 重命名验证成功 新文件存在 原文件不存在
+[2025-08-24 19:51:26.367][000000003.117] I/user.目录操作 ===== 开始目录列举 =====
+[2025-08-24 19:51:26.424][000000003.121] I/user.fs lsdir [{"name":"boottime","size":0,"type":0},{"name":"testline","size":0,"type":0},{"name":"renamed_file.txt","size":0,"type":0}]
+[2025-08-24 19:51:26.478][000000003.127] I/user.os.remove 文件删除成功 路径:/sd/io_test/renamed_file.txt
+[2025-08-24 19:51:26.539][000000003.129] D/fatfs f_open /io_test/renamed_file.txt 4
+[2025-08-24 19:51:26.593][000000003.130] D/vfs fopen /sd/io_test/renamed_file.txt r not found
+[2025-08-24 19:51:26.656][000000003.130] I/user.验证结果 renamed_file.txt文件删除验证成功
+[2025-08-24 19:51:26.734][000000003.137] I/user.os.remove testline文件删除成功 路径:/sd/io_test/testline
+[2025-08-24 19:51:26.856][000000003.139] D/fatfs f_open /io_test/testline 4
+[2025-08-24 19:51:26.922][000000003.140] D/vfs fopen /sd/io_test/testline r not found
+[2025-08-24 19:51:27.113][000000003.140] I/user.验证结果 testline文件删除验证成功
+[2025-08-24 19:51:27.197][000000003.147] I/user.os.remove 文件删除成功 路径:/sd/io_test/boottime
+[2025-08-24 19:51:27.251][000000003.149] D/fatfs f_open /io_test/boottime 4
+[2025-08-24 19:51:27.302][000000003.150] D/vfs fopen /sd/io_test/boottime r not found
+[2025-08-24 19:51:27.365][000000003.150] I/user.验证结果 boottime文件删除验证成功
+[2025-08-24 19:51:27.407][000000003.158] I/user.io.rmdir 目录删除成功 路径:/sd/io_test
+[2025-08-24 19:51:27.461][000000003.159] D/fatfs f_open /io_test 4
+[2025-08-24 19:51:27.536][000000003.159] D/vfs fopen /sd/io_test r not found
+[2025-08-24 19:51:27.610][000000003.159] I/user.验证结果 目录删除验证成功
+[2025-08-24 19:51:27.668][000000003.160] I/user.文件操作 ===== 文件操作完成 =====
+[2025-08-24 19:51:27.712][000000003.160] I/user.系统清理 开始执行关闭操作...
+[2025-08-24 19:51:27.772][000000003.160] I/user.文件系统 卸载成功
+[2025-08-24 19:51:27.867][000000003.160] I/user.SPI接口 已关闭
+
+
+(3)网络连接与HTTP下载
+[2025-08-24 20:31:49.405][000000006.268] I/user.HTTP下载 开始下载任务
+[2025-08-24 20:31:49.438][000000006.275] dns_run 674:gitee.com state 0 id 1 ipv6 0 use dns server2, try 0
+[2025-08-24 20:31:49.471][000000006.277] D/mobile TIME_SYNC 0
+[2025-08-24 20:31:49.503][000000006.297] dns_run 691:dns all done ,now stop
+[2025-08-24 20:31:54.800][000000012.080] I/user.HTTP下载 下载完成 success 200 
+[2025-08-24 20:31:54.872][000000012.080] {"Age":"0","Cache-Control":"public, max-age=60","Via":"1.1 varnish","Transfer-Encoding":"chunked","Date":"Sun, 24 Aug 2025 12:31:49 GMT","Access-Control-Allow-Credentials":"true","Vary":"Accept-Encoding","X-Served-By":"cache-ffe9","X-Gitee-Server":"http-pilot 1.9.21","Connection":"keep-alive","Server":"ADAS\/1.0.214","Access-Control-Allow-Headers":"Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With,X-CustomHeader,Content-Range,Range,Set-Language","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline'; sandbox","X-Request-Id":"1f7e4b55-53c8-440a-9806-8894aa823f50","Accept-Ranges":"bytes","Etag":"W\/\"6ea36a6c51a48eaba0ffbc01d409424e7627bc56\"","Content-Type":"text\/plain; charset=utf-8","Access-Control-Allow-Methods":"GET, POST, PUT, PATCH, DELETE, OPTIONS","X-Frame-Options":"DENY","X-Cache":"MISS","Set-Cookie":"BEC=1f1759df3ccd099821dcf0da6feb0357;Path=\/;Max-Age=126000"}
+[2025-08-24 20:31:54.910][000000012.080]  411922
+[2025-08-24 20:31:54.936][000000012.082] I/user.HTTP下载 文件大小验证 预期: 411922 实际: 411922
+[2025-08-24 20:31:54.979][000000012.083] I/user.HTTP下载 资源清理完成
+
+(4)网络连接与HTTP上传
+[2025-09-24 18:07:54.587][000000000.360] I/user.main tfcard 001.000.000
+[2025-09-24 18:07:54.601][000000000.396] W/user.HTTP上传 等待网络连接 1 3
+[2025-09-24 18:07:56.758][000000004.693] D/mobile cid1, state0
+[2025-09-24 18:07:56.763][000000004.694] D/mobile bearer act 0, result 0
+[2025-09-24 18:07:56.774][000000004.695] D/mobile NETIF_LINK_ON -> IP_READY
+[2025-09-24 18:07:56.779][000000004.696] I/user.HTTP上传 网络已就绪 1 3
+[2025-09-24 18:07:56.791][000000004.696] SPI_HWInit 552:spi1 speed 2000000,1994805,154
+[2025-09-24 18:07:56.796][000000004.697] D/fatfs init sdcard at spi=1 cs=20
+[2025-09-24 18:07:56.809][000000004.716] D/SPI_TF 卡容量 122138624KB
+[2025-09-24 18:07:56.828][000000004.716] D/SPI_TF sdcard init OK OCR:0xc0ff8000!
+[2025-09-24 18:07:56.833][000000004.725] I/user.HTTP上传 准备上传文件 /sd/30M_test.txt 大小: 31467520 字节
+[2025-09-24 18:07:56.852][000000004.725] I/user.HTTP上传 开始上传任务
+[2025-09-24 18:07:56.860][000000004.730] D/socket connect to airtest.openluat.com,2900
+[2025-09-24 18:07:56.865][000000004.731] dns_run 676:airtest.openluat.com state 0 id 1 ipv6 0 use dns server2, try 0
+[2025-09-24 18:07:56.869][000000004.799] dns_run 693:dns all done ,now stop
+[2025-09-24 18:07:56.885][000000004.881] soc_cms_proc 2189:cenc report 1,51,1,15
+[2025-09-24 18:07:57.063][000000005.068] D/mobile NETIF_LINK_ON -> IP_READY
+[2025-09-24 18:07:58.364][000000006.364] D/mobile ims reg state 0
+[2025-09-24 18:07:58.375][000000006.365] D/mobile LUAT_MOBILE_EVENT_CC status 0
+[2025-09-24 18:07:58.385][000000006.365] D/mobile LUAT_MOBILE_CC_READY
+[2025-09-24 18:09:32.042][000000100.039] I/user.httpplus 等待服务器完成响应
+[2025-09-24 18:09:32.135][000000100.135] I/user.httpplus 等待服务器完成响应
+[2025-09-24 18:09:32.982][000000100.973] I/user.httpplus 服务器已完成响应,开始解析响应
+[2025-09-24 18:09:32.997][000000100.998] I/user.HTTP上传 上传完成 success 200
+[2025-09-24 18:09:33.004][000000100.998] I/user.HTTP上传 服务器响应头 {"Content-Type":"text\/plain;charset=UTF-8","Connection":"close","Content-Length":"20","Vary":"Access-Control-Request-Headers","Date":"Wed, 24 Sep 2025 10"}
+[2025-09-24 18:09:33.011][000000100.999] I/user.HTTP上传 服务器响应体长度 20
+[2025-09-24 18:09:33.021][000000101.000] I/user.HTTP上传 服务器响应内容 uploadFileToStaticOK
+[2025-09-24 18:09:33.027][000000101.000] I/user.HTTP上传 资源清理完成
+```

+ 331 - 0
module/Air8000/demo/accessory_board/AirMICROSD_1010/tfcard_app.lua

@@ -0,0 +1,331 @@
+--[[
+@module  tfcard_app
+@summary TF卡文件操作测试模块
+@version 1.0.0
+@date    2025.08.25
+@author  王棚嶙
+@usage
+本文件为TF卡的文件操作测试流程:
+1. 创建目录
+2. 创建并写入文件
+3. 检查文件是否存在
+4. 获取文件大小
+5. 读取文件内容
+6. 启动计数文件操作
+7. 文件追加测试
+8. 按行读取测试
+9. 读取后关闭文件
+10. 文件重命名
+11. 列举目录内容
+12. 删除文件
+13. 删除目录
+本文件没有对外接口,直接在main.lua中require "tfcard_app"就可以加载运行
+]] 
+
+function tfcard_main_task() -- 开始进行主测试流程。
+
+    -- ##########  SPI初始化 ##########
+    -- Air8000整机核心板上TF卡的的pin_cs为gpio20,spi_id为1.请根据实际硬件修改
+    spi_id, pin_cs = 1, 12
+    spi.setup(spi_id, nil, 0, 0, 400 * 1000)
+    --设置片选引脚同一spi总线上的所有从设备在初始化时必须要先拉高CS脚,防止从设备之间互相干扰。
+    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
+    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
+        end
+    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
+    end
+
+    -- 3. 检查文件是否存在
+    if io.exists(file_path) then
+        log.info("io.exists", "文件存在", "路径:" .. file_path)
+    else
+        log.error("io.exists", "文件不存在", "路径:" .. file_path)
+        goto resource_cleanup
+    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
+    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
+    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("启动计数", "文件不存在或无法打开")
+
+    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)
+    else
+        log.error("文件写入", "无法打开文件", "路径:" .. file_path)
+        goto resource_cleanup
+    end
+
+    -- 7. 文件追加测试
+    append_file = dir_path .. "/test_a"
+    -- 清理旧文件
+    os.remove(append_file)
+
+    -- 创建并写入初始内容
+    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
+    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
+
+    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
+    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
+    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
+    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("验证结果", "重命名验证成功", "新文件存在", "原文件不存在")
+        else
+            log.error("验证结果", "重命名验证失败")
+        end
+    else
+        log.error("os.rename", "重命名失败", "错误:" .. tostring(err), "原路径:" .. old_path)
+        goto resource_cleanup
+    end
+
+    -- 10. 列举目录内容
+    log.info("目录操作", "===== 开始目录列举 =====")
+
+    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
+    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
+    end
+
+    -- 测试删除testline文件
+    if os.remove(line_file) then
+        log.info("os.remove", "testline文件删除成功", "路径:" .. line_file)
+
+        -- 验证删除结果
+        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
+    end
+
+    if os.remove(file_path) then
+        log.info("os.remove", "文件删除成功", "路径:" .. file_path)
+
+        -- 验证删除结果
+        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
+    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
+    end
+
+    log.info("文件操作", "===== 文件操作完成 =====")
+
+    -- ########## 功能: 收尾功能演示##########
+    -- 卸载文件系统和关闭SPI
+    ::resource_cleanup::
+
+    log.info("结束", "开始执行关闭操作...")  
+    -- 如已挂载需先卸载文件系统,未挂载直接关闭SPI
+    if mount_ok then
+        if fatfs.unmount("/sd") then
+            log.info("文件系统", "卸载成功")
+        else
+            log.error("文件系统", "卸载失败")
+        end
+    end
+
+    -- 2. 关闭SPI接口
+    spi.close(spi_id)
+    log.info("SPI接口", "已关闭")
+
+end
+
+sys.taskInit(tfcard_main_task)
+
+

+ 14 - 14
module/Air8000/demo/netdrv/ch390/lan_TCP/main.lua

@@ -92,25 +92,25 @@ function TCP_TASK()
     socket.debug(netCB, true)                -- 打开调试日志
     socket.config(netCB, nil, protocol, ssl)      -- 此配置为TCP连接,无SSL加密
 
+    -- 收取数据会触发回调, 这里的"receive" 是固定值不要修改。
+    uart.on(uartid, "receive", function(id, len)
+        while true do
+            local len = uart.rx(id, uart_rx_buff)   -- 接收串口收到的数据,并赋值到uart_rx_buff
+            if len <= 0 then    -- 接收到的字节长度为0 则退出
+                break
+            end
+            -- 如果已经在线了,则发送socket.EVENT消息来打断任务里的阻塞等待状态,让任务循环继续
+            if connect_state then
+                sys_send(taskName, socket.EVENT, 0)
+            end
+        end
+    end)
+
     -- 串口和TCP服务器的交互逻辑
     while true do
         -- 连接服务器,返回是否连接成功
         result = libnet.connect(taskName, 15000, netCB, ip, port)
 
-        -- 收取数据会触发回调, 这里的"receive" 是固定值不要修改。
-        uart.on(uartid, "receive", function(id, len)
-            while true do
-                local len = uart.rx(id, uart_rx_buff)   -- 接收串口收到的数据,并赋值到uart_rx_buff
-                if len <= 0 then    -- 接收到的字节长度为0 则退出
-                    break
-                end
-                -- 如果已经在线了,则发送socket.EVENT消息来打断任务里的阻塞等待状态,让任务循环继续
-                if connect_state then
-                    sys_send(taskName, socket.EVENT, 0)
-                end
-            end
-        end)
-
         -- 如果连接成功,则改变连接状态参数,并且随便发一条数据到服务器,看服务器能不能收到
         if result then
             connect_state = true

+ 14 - 14
module/Air8000/demo/netdrv/ch390/wan_TCP/main.lua

@@ -73,25 +73,25 @@ function TCP_TASK()
     socket.debug(netCB, true)                -- 打开调试日志
     socket.config(netCB, nil, protocol, ssl)      -- 此配置为TCP连接,无SSL加密
 
+    -- 收取数据会触发回调, 这里的"receive" 是固定值不要修改。
+    uart.on(uartid, "receive", function(id, len)
+        while true do
+            local len = uart.rx(id, uart_rx_buff)   -- 接收串口收到的数据,并赋值到uart_rx_buff
+            if len <= 0 then    -- 接收到的字节长度为0 则退出
+                break
+            end
+            -- 如果已经在线了,则发送socket.EVENT消息来打断任务里的阻塞等待状态,让任务循环继续
+            if connect_state then
+                sys_send(taskName, socket.EVENT, 0)
+            end
+        end
+    end)
+
     -- 串口和TCP服务器的交互逻辑
     while true do
         -- 连接服务器,返回是否连接成功
         result = libnet.connect(taskName, 15000, netCB, ip, port)
 
-        -- 收取数据会触发回调, 这里的"receive" 是固定值不要修改。
-        uart.on(uartid, "receive", function(id, len)
-            while true do
-                local len = uart.rx(id, uart_rx_buff)   -- 接收串口收到的数据,并赋值到uart_rx_buff
-                if len <= 0 then    -- 接收到的字节长度为0 则退出
-                    break
-                end
-                -- 如果已经在线了,则发送socket.EVENT消息来打断任务里的阻塞等待状态,让任务循环继续
-                if connect_state then
-                    sys_send(taskName, socket.EVENT, 0)
-                end
-            end
-        end)
-
         -- 如果连接成功,则改变连接状态参数,并且随便发一条数据到服务器,看服务器能不能收到
         if result then
             connect_state = true

+ 0 - 200
module/Air8000/demo/record/main.lua

@@ -1,200 +0,0 @@
--- LuaTools需要PROJECT和VERSION这两个信息
-PROJECT = "record"
-VERSION = "1.0.0"
-
--- sys库是标配
-_G.sys = require("sys")
-_G.sysplus = require("sysplus")
-
-
-gpio.setup(24, 1, gpio.PULLUP)          -- i2c工作的电压域
-
-local taskName = "task_audio"
-
-local MSG_MD = "moreData"   -- 播放缓存有空余
-local MSG_PD = "playDone"   -- 播放完成所有数据
-
--- amr数据存放buffer,尽可能地给大一些
-amr_buff = zbuff.create(20 * 1024)
---创建一个amr的encoder
-encoder = nil
-pcm_buff0 = zbuff.create(16000)
-pcm_buff1 = zbuff.create(16000)
-audio.on(0, function(id, event, point)
-    --使用play来播放文件时只有播放完成回调
-    if event == audio.RECORD_DATA then -- 录音数据
-		if point == 0 then
-			log.info("buff", point, pcm_buff0:used())
-			codec.encode(encoder, pcm_buff0, amr_buff)
-		else
-			log.info("buff", point, pcm_buff1:used())
-			codec.encode(encoder, pcm_buff1, amr_buff)
-		end
-        
-    elseif event == audio.RECORD_DONE then -- 录音完成
-        sys.publish("AUDIO_RECORD_DONE")
-    else
-        local succ,stop,file_cnt = audio.getError(0)
-        if not succ then
-            if stop then
-                log.info("用户停止播放")
-            else
-                log.info("第", file_cnt, "个文件解码失败")
-            end
-        end
-        -- log.info("播放完成一个音频")
-        sysplus.sendMsg(taskName, MSG_PD)
-    end
-end)
-
----- MultipartForm上传文件
--- url string 请求URL地址
--- filename string 上传服务器的文件名
--- filePath string 待上传文件的路径
-local function postMultipartFormData(url, filename, filePath)
-    local boundary = "----WebKitFormBoundary"..os.time()
-    local req_headers = {
-        ["Content-Type"] = "multipart/form-data; boundary=" .. boundary,
-    }
-    local body = {}
-    table.insert(body, "--"..boundary.."\r\nContent-Disposition: form-data; name=\"file\"; filename=\"".. filename .."\"\r\n\r\n")
-    table.insert(body, io.readFile(filePath))
-    table.insert(body, "\r\n")
-    table.insert(body, "--"..boundary.."--\r\n")
-    body = table.concat(body)
-    log.info("headers: ", "\r\n" .. json.encode(req_headers), type(body))
-    log.info("body: " .. body:len() .. "\r\n" .. body)
-    local code, headers, body = http.request("POST",url,
-            req_headers,
-            body
-    ).wait()   
-    log.info("http.post", code, headers, body)
-end
-
-function audio_setup()
-		local i2c_id = 0            -- i2c_id 0
-
-        local pa_pin = 162           -- 喇叭pa功放脚
-        local power_pin = 164         -- es8311电源脚
-
-
-
-		local i2s_id = 0            -- i2s_id 0
-		local i2s_mode = 0          -- i2s模式 0 主机 1 从机
-		local i2s_sample_rate = 16000   -- 采样率
-		local i2s_bits_per_sample = 16  -- 数据位数
-		local i2s_channel_format = i2s.MONO_R   -- 声道, 0 左声道, 1 右声道, 2 立体声
-		local i2s_communication_format = i2s.MODE_LSB   -- 格式, 可选MODE_I2S, MODE_LSB, MODE_MSB
-		local i2s_channel_bits = 16     -- 声道的BCLK数量
-
-		local multimedia_id = 0         -- 音频通道 0
-		local pa_on_level = 1           -- PA打开电平 1 高电平 0 低电平
-		local power_delay = 3           -- 在DAC启动前插入的冗余时间,单位100ms
-		local pa_delay = 100            -- 在DAC启动后,延迟多长时间打开PA,单位1ms
-		local power_on_level = 1        -- 电源控制IO的电平,默认拉高
-		local power_time_delay = 100    -- 音频播放完毕时,PA与DAC关闭的时间间隔,单位1ms
-
-		local voice_vol = 70        -- 喇叭音量
-		local mic_vol = 80          -- 麦克风音量
-        gpio.setup(power_pin, 1, gpio.PULLUP)         
-        gpio.setup(pa_pin, 1, gpio.PULLUP)
-		pm.power(pm.LDO_CTL, false)  --开发板上ES8311由LDO_CTL控制上下电
-		sys.wait(100)
-		pm.power(pm.LDO_CTL, true)  --开发板上ES8311由LDO_CTL控制上下电
-		gpio.setup(8, 1)
-	
-		i2c.setup(i2c_id,i2c.FAST)      --设置i2c
-		i2s.setup(i2s_id, i2s_mode, i2s_sample_rate, i2s_bits_per_sample, i2s_channel_format, i2s_communication_format,i2s_channel_bits)    --设置i2s
-	
-		audio.config(multimedia_id, pa_pin, pa_on_level, power_delay, pa_delay, power_pin, power_on_level, power_time_delay)
-		audio.setBus(multimedia_id, audio.BUS_I2S,{chip = "es8311",i2cid = i2c_id , i2sid = i2s_id, voltage = audio.VOLTAGE_1800})	--通道0的硬件输出通道设置为I2S
-	
-		audio.vol(multimedia_id, voice_vol)
-		audio.micVol(multimedia_id, mic_vol)
-		sys.publish("AUDIO_READY")
-end
-
--- 配置好audio外设
-sys.taskInit(audio_setup)
-
-local function audio_task()
-    sys.waitUntil("AUDIO_READY")
-    sys.wait(5000)
-    local result
-
-    --下面为录音demo,根据适配情况选择性开启
-    local recordPath = "/record.amr"
-    
-    -- -- 直接录音到文件
-    -- err = audio.record(0, audio.AMR, 5, 7, recordPath)
-    -- sys.waitUntil("AUDIO_RECORD_DONE")
-    -- log.info("record","录音结束")
-    -- result = audio.play(0, {recordPath})
-    -- if result then
-    --     --等待音频通道的回调消息,或者切换歌曲的消息
-    --     while true do
-    --         msg = sysplus.waitMsg(taskName, nil)
-    --         if type(msg) == 'table' then
-    --             if msg[1] == MSG_PD then
-    --                 log.info("播放结束")
-    --                 break
-    --             end
-    --         else
-    --             log.error(type(msg), msg)
-    --         end
-    --     end
-    -- else
-    --     log.debug("解码失败!")
-    --     sys.wait(1000)
-    -- end
-
-    -- -- 录音到内存自行编码
-    
-    encoder = codec.create(codec.AMR, false, 7)
-    log.info("encoder",encoder)
-    log.info("开始录音")
-    err = audio.record(0, audio.AMR, 5, 7, nil,nil, pcm_buff0, pcm_buff1)
-    sys.waitUntil("AUDIO_RECORD_DONE")
-    log.info("record","录音结束")
-    os.remove(recordPath)
-    io.writeFile(recordPath, "#!AMR\n")
-	io.writeFile(recordPath, amr_buff:query(), "a+b")
-
-	result = audio.play(0, {recordPath})
-    if result then
-        --等待音频通道的回调消息,或者切换歌曲的消息
-        while true do
-            msg = sysplus.waitMsg(taskName, nil)
-            if type(msg) == 'table' then
-                if msg[1] == MSG_PD then
-                    log.info("播放结束")
-                    break
-                end
-            else
-                log.error(type(msg), msg)
-            end
-        end
-    else
-        log.debug("解码失败!")
-        sys.wait(1000)
-    end
-
-	-- 下面的演示是将音频文件发送到服务器上,如有需要,可以将下面代码注释打开,这里的url是合宙的文件上传测试服务器,上传的文件到http://tools.openluat.com/tools/device-upload-test查看
-	--[[ 
-		local timeTable = os.date("*t", os.time())
-		local nowTime = string.format("%4d%02d%02d_%02d%02d%02d", timeTable.year, timeTable.month, timeTable.day, timeTable.hour, timeTable.min, timeTable.sec)
-		local filename = mobile.imei() .. "_" .. nowTime .. ".amr"
-		postMultipartFormData("http://tools.openluat.com/api/site/device_upload_file", filename, recordPath)
- 	]]
-    --该方法为从串口1,把录音数据传给串口1
-    --  uart.setup(1, 115200)								-- 开启串口1
-    --  uart.write(1, io.readFile(recordPath))				-- 向串口发送录音文件
-
-end
-
-sysplus.taskInitEx(audio_task, taskName)
-
--- 用户代码已结束---------------------------------------------
--- 结尾总是这一句
-sys.run()
--- sys.run()之后后面不要加任何语句!!!!!

+ 98 - 0
module/Air8000/demo/tf_card/http_upload_file.lua

@@ -0,0 +1,98 @@
+--[[
+@module http_upload_file
+@summary TF卡大文件httpplus上传模块
+@version 1.0.0
+@date 2025.08.25
+@author 王棚嶙
+@usage
+本文件演示通过httpplus库将TF卡中的大文件上传到HTTP服务器:
+1. 网络就绪检测
+2. TF卡文件系统挂载
+3. 大文件上传功能
+4. 上传结果记录
+本文件没有对外接口,直接在main.lua中require "http_upload_file"即可
+]]
+-- 加载httpplus扩展库,不可省略
+local httpplus = require "httpplus"
+
+local function http_upload_task()
+    -- 阶段1: 网络就绪检测
+    while not socket.adapter(socket.dft()) do
+        log.warn("HTTP上传", "等待网络连接", socket.dft())
+        -- 待IP_READY消息,超时设为1秒
+        sys.waitUntil("IP_READY", 1000)
+    end
+
+    -- 检测到了IP_READY消息
+    log.info("HTTP上传", "网络已就绪", socket.dft())
+
+    -- 阶段2: TF卡文件系统初始化
+    local spi_id, pin_cs = 1, 20
+    spi.setup(spi_id, nil, 0, 0, 400 * 1000)
+    -- 同一spi总线上的所有从设备在初始化时必须要先拉高CS脚,防止从设备之间互相干扰。
+    -- 在air8000开发板上,TF卡和ch390共用SPI1总线。
+    gpio.setup(pin_cs, 1)
+    
+    local mount_ok = fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)
+    if not mount_ok then
+        log.error("HTTP上传", "文件系统挂载失败")
+        fatfs.unmount("/sd")
+        spi.close(spi_id)
+        return
+    end
+
+    -- 阶段3: 检查要上传的文件是否存在
+    -- 替换为实际的文件路径
+    local upload_file_path = "/sd/30M_test.txt" 
+    if not io.exists(upload_file_path) then
+        log.error("HTTP上传", "要上传的文件不存在", upload_file_path)
+        fatfs.unmount("/sd")
+        spi.close(spi_id)
+        return
+    end
+
+    -- 获取文件大小
+    local file_size = io.fileSize(upload_file_path)
+    log.info("HTTP上传", "准备上传文件", upload_file_path, "大小:", file_size, "字节")
+
+    -- 阶段4: 执行文件上传
+    log.info("HTTP上传", "开始上传任务")
+    
+    -- 使用httpplus库上传文件,参考httpplus_app_post_file的实现
+    -- hhtplus.request接口支持单文件上传、多文件上传、单文本上传、多文本上传、单/多文本+单/多文件上传
+    -- http://airtest.openluat.com:2900/uploadFileToStatic 仅支持单文件上传,并且上传的文件name必须使用"uploadFile"
+    -- 所以此处仅演示了单文件上传功能,并且"uploadFile"不能改成其他名字,否则会出现上传失败的应答
+    local code, response = httpplus.request({
+        url = "http://airtest.openluat.com:2900/uploadFileToStatic",
+        files = {
+            -- 服务器要求文件名必须为"uploadFile"
+            ["uploadFile"] = upload_file_path, 
+        },
+    })
+
+    -- 阶段5: 记录上传结果
+    log.info("HTTP上传", "上传完成", 
+        code == 200 and "success" or "error", 
+        code)
+    
+    if code == 200 then
+        log.info("HTTP上传", "服务器响应头", json.encode(response.headers or {}))
+        local body = response.body and response.body:query()
+        log.info("HTTP上传", "服务器响应体长度", body and body:len() or 0)
+        
+        -- 可以进一步解析服务器响应
+        if body then
+            log.info("HTTP上传", "服务器响应内容", body:len() > 512 and "内容过长,不显示" or body)
+        end
+    else
+        log.error("HTTP上传", "上传失败", code)
+    end
+
+    -- 阶段6: 资源清理
+    fatfs.unmount("/sd")
+    spi.close(spi_id)
+    log.info("HTTP上传", "资源清理完成")
+end
+
+-- 创建上传任务
+sys.taskInit(http_upload_task)

+ 6 - 4
module/Air8000/demo/tf_card/main.lua

@@ -76,15 +76,17 @@ end
 --加载ch390控制模块,仅限于AIR8000整机开发板系列
 require "ch390_manager"
 
---[[在加载以下两个功能时,建议分别打开进行测试,因为文件操作和http下载功能是异步操作。
+--[[在加载以下三个功能时,建议分别打开进行测试,因为文件操作,http下载功能和http大文件上传功能是异步操作。
 放到一个项目中,如果加载的时间点是随机的,就会出现tfcard_app在spi.setup和fatfs挂载文件系统之后,
-还没有释放资源,然后http_download_file又去重复spi.setup和fatfs挂载文件系统了,不符合正常的业务逻辑,用户在参考编程的时候也要注意。]]
+还没有释放资源,然后http_download_file或http_upload_file又去重复spi.setup和fatfs挂载文件系统了,
+不符合正常的业务逻辑,用户在参考编程的时候也要注意。]]
 
 --加载tf卡测试应用模块
-require "tfcard_app"
+-- require "tfcard_app"
 --加载HTTP下载存入TF卡功能演示模块
 --require "http_download_file"
-
+--加载HTTP上传文件到服务器的功能演示模块
+require "http_upload_file"
 
 -- 用户代码已结束---------------------------------------------
 -- 结尾总是这一句

+ 58 - 2
module/Air8000/demo/tf_card/readme.md

@@ -3,8 +3,9 @@
 本demo演示了在嵌入式环境中对TF卡(SD卡)的完整操作流程,覆盖了从文件系统挂载到高级文件操作的完整功能链。项目分为两个核心模块:
 
 1、main.lua:主程序入口 <br> 
-2、tfcard_app.lua:TF卡基础应用模块,实现文件系统管理、文件操作和目录管理功能。<br> 
-3、http_download_file.lua:HTTP下载模块,实现网络检测与文件下载到TF卡的功能
+2、tfcard_app.lua:TF卡基础应用模块,实现文件系统管理、文件操作和目录管理功能<br> 
+3、http_download_file.lua:HTTP下载模块,实现网络检测与文件下载到TF卡的功能<br>
+4、http_upload_file.lua:HTTP下载模块,实现网络检测与tf卡内大文件上传服务器的功能
 
 ## **演示功能概述**
 
@@ -77,6 +78,32 @@
 - 自动文件大小验证
 - 资源清理(卸载/spi关闭)
 
+### 5、HTTP上传功能 (http_download_file.lua)
+
+#### 加载扩展库
+
+- require("httpplus")
+
+#### 网络就绪检测
+
+- 1秒循环等待IP就绪
+
+#### 文件系统管理
+
+- SPI初始化与挂载
+
+- 确认文件存在
+
+
+#### 安全上传
+
+- HTTP上传
+
+#### 结果处理
+
+- 解析服务器响应
+- 资源清理(卸载/spi关闭)
+
 ## **演示硬件环境**
 
 1、Air8000整机开发板一块
@@ -176,4 +203,33 @@
 [2025-08-24 20:31:54.936][000000012.082] I/user.HTTP下载 文件大小验证 预期: 411922 实际: 411922
 [2025-08-24 20:31:54.979][000000012.083] I/user.HTTP下载 资源清理完成
 
+(4)网络连接与HTTP上传
+[2025-09-24 18:07:54.587][000000000.360] I/user.main tfcard 001.000.000
+[2025-09-24 18:07:54.601][000000000.396] W/user.HTTP上传 等待网络连接 1 3
+[2025-09-24 18:07:56.758][000000004.693] D/mobile cid1, state0
+[2025-09-24 18:07:56.763][000000004.694] D/mobile bearer act 0, result 0
+[2025-09-24 18:07:56.774][000000004.695] D/mobile NETIF_LINK_ON -> IP_READY
+[2025-09-24 18:07:56.779][000000004.696] I/user.HTTP上传 网络已就绪 1 3
+[2025-09-24 18:07:56.791][000000004.696] SPI_HWInit 552:spi1 speed 2000000,1994805,154
+[2025-09-24 18:07:56.796][000000004.697] D/fatfs init sdcard at spi=1 cs=20
+[2025-09-24 18:07:56.809][000000004.716] D/SPI_TF 卡容量 122138624KB
+[2025-09-24 18:07:56.828][000000004.716] D/SPI_TF sdcard init OK OCR:0xc0ff8000!
+[2025-09-24 18:07:56.833][000000004.725] I/user.HTTP上传 准备上传文件 /sd/30M_test.txt 大小: 31467520 字节
+[2025-09-24 18:07:56.852][000000004.725] I/user.HTTP上传 开始上传任务
+[2025-09-24 18:07:56.860][000000004.730] D/socket connect to airtest.openluat.com,2900
+[2025-09-24 18:07:56.865][000000004.731] dns_run 676:airtest.openluat.com state 0 id 1 ipv6 0 use dns server2, try 0
+[2025-09-24 18:07:56.869][000000004.799] dns_run 693:dns all done ,now stop
+[2025-09-24 18:07:56.885][000000004.881] soc_cms_proc 2189:cenc report 1,51,1,15
+[2025-09-24 18:07:57.063][000000005.068] D/mobile NETIF_LINK_ON -> IP_READY
+[2025-09-24 18:07:58.364][000000006.364] D/mobile ims reg state 0
+[2025-09-24 18:07:58.375][000000006.365] D/mobile LUAT_MOBILE_EVENT_CC status 0
+[2025-09-24 18:07:58.385][000000006.365] D/mobile LUAT_MOBILE_CC_READY
+[2025-09-24 18:09:32.042][000000100.039] I/user.httpplus 等待服务器完成响应
+[2025-09-24 18:09:32.135][000000100.135] I/user.httpplus 等待服务器完成响应
+[2025-09-24 18:09:32.982][000000100.973] I/user.httpplus 服务器已完成响应,开始解析响应
+[2025-09-24 18:09:32.997][000000100.998] I/user.HTTP上传 上传完成 success 200
+[2025-09-24 18:09:33.004][000000100.998] I/user.HTTP上传 服务器响应头 {"Content-Type":"text\/plain;charset=UTF-8","Connection":"close","Content-Length":"20","Vary":"Access-Control-Request-Headers","Date":"Wed, 24 Sep 2025 10"}
+[2025-09-24 18:09:33.011][000000100.999] I/user.HTTP上传 服务器响应体长度 20
+[2025-09-24 18:09:33.021][000000101.000] I/user.HTTP上传 服务器响应内容 uploadFileToStaticOK
+[2025-09-24 18:09:33.027][000000101.000] I/user.HTTP上传 资源清理完成
 ```

+ 27 - 0
module/Air8000/demo/wifi_location/check_wifi.lua

@@ -0,0 +1,27 @@
+--[[
+@module  check_wifi
+@summary 远程升级wifi固件模块
+@version 1.1
+@date    2025.09.23
+@author  拓毅恒
+@usage
+检查WiFi版本并自动升级
+功能:检查当前Air8000模组的WiFi固件是否为最新版本,若不是则自动启动升级(需插入可联网的SIM卡)。
+注意:升级完毕后最好取消调用,防止后期版本升级过高导致程序使用不稳定。
+
+本文件没有对外接口,直接在main.lua中require "check_wifi"就可以加载运行。
+]]
+
+local exfotawifi = require("exfotawifi")
+
+local function fota_wifi_task()
+    local result = exfotawifi.request()
+    if result then
+        log.info("exfotawifi", "升级任务执行成功")
+    else
+        log.info("exfotawifi", "升级任务执行失败")
+    end
+end
+
+-- 在设备启动时检查SIM卡状态
+sys.taskInit(fota_wifi_task)

+ 1 - 4
module/Air8000/demo/wifi_location/main.lua

@@ -25,7 +25,7 @@ local requestParam = {} -- 用于保存定位请求参数
 
 -- 如果无法定位,可以开启此功能升级WiFi固件版本后再次尝试
 -- 升级完毕后最好取消调用,防止后期版本升级过高导致程序使用不稳定
--- local exfotawifi = require("exfotawifi") 
+-- require "check_wifi" 
 
 local airlbs = require "airlbs"
 
@@ -89,9 +89,6 @@ sys.taskInit(function()
     end
 end)
 
--- 在设备启动时检查网络状态
-sys.taskInit(wait_ip_ready)
-
 sysplus.taskInitEx(wlan_location_task, taskName)
 
 -- 用户代码已结束---------------------------------------------

+ 0 - 5
module/Air8000/demo/wlan/AP/main.lua

@@ -39,11 +39,6 @@ sys.subscribe("WLAN_AP_INC", function(evt, data)
     log.info("收到AP事件", evt, data and data:toHex())
 end)
 
-
-
--- 在设备启动时检查网络状态
-sys.taskInit(wait_ip_ready)
-
 sys.taskInit(function()
     log.info("开始AP 测试...")
     wlan.init()

+ 0 - 3
module/Air8000/demo/wlan/Power_Save/main.lua

@@ -123,9 +123,6 @@ end)
 --     end
 -- end)
 
--- 在设备启动时检查网络状态
-sys.taskInit(wait_ip_ready)
-
 sys.taskInit(function()
     log.info("新的Air8000脚本...")
 

+ 0 - 3
module/Air8000/demo/wlan/STA/main.lua

@@ -83,9 +83,6 @@ end
 --     end
 -- end)
 
--- 在设备启动时检查网络状态
-sys.taskInit(wait_ip_ready)
-
 sys.taskInit(function()
     log.info("新的Air8000脚本...")
     wlan.init()

+ 0 - 3
module/Air8000/demo/wlan/wifi_configuration_network_by_ap/main.lua

@@ -91,9 +91,6 @@ function main_task()
 
 end
 
--- 在设备启动时检查网络状态
-sys.taskInit(wait_ip_ready)
-
 sys.subscribe("WLAN_SCAN_DONE", scan_done_handle)
 sys.subscribe("IP_READY", ip_ready_handle)
 sys.taskInit(main_task)

+ 6 - 9
module/Air8000/demo/wlan/wifi_scan/main.lua

@@ -31,18 +31,15 @@ function scan_done_handle()
 end
 
 --  每隔6秒打印一次airlink统计数据, 调试用
-sys.taskInit(function()
-    while 1 do
-        sys.wait(6000)
-        airlink.statistics()
-    end
-end)
+-- sys.taskInit(function()
+--     while 1 do
+--         sys.wait(6000)
+--         airlink.statistics()
+--     end
+-- end)
 
 sys.subscribe("WLAN_SCAN_DONE", scan_done_handle)
 
--- 在设备启动时检查网络状态
-sys.taskInit(wait_ip_ready)
-
 sys.taskInit(function()
     -- 稍微缓一下
     sys.wait(500)

+ 4 - 5
module/Air8000/demolib/adxl34x.lua

@@ -135,11 +135,10 @@ log.info("adxl34x_data", "adxl34x_data.x"..(adxl34x_data.x),"adxl34x_data.y"..(a
 function adxl34x.get_data()
     local accel={x=nil,y=nil,z=nil}
     i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAX0)
-    _,accel.x = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
-    i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAY0)
-    _,accel.y = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
-    i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAZ0)
-    _,accel.z = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
+    local rcvData = i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 6)
+    if rcvData ~= nil and #rcvData == 6 then
+        accel.x, accel.y, accel.z = string.unpack("<hhh", rcvData)
+    end
     return accel
 end
 

+ 4 - 4
module/Air8000/demolib/spl06.lua

@@ -79,11 +79,11 @@ local function i2cReadTwoData(i2caddr,regaddr)
         msb = 0
         lsb = 0
         log.info("spl06 two data","the data is null")
-        return msb256+lsb
+        return msb*256+lsb
     else
     --log.info("spl06 two data","msb:"..msb.." lsb:"..lsb)
         if regaddr ==0x10 then
-            return msb16 + bit.rshift((lsb&0xF0),4)
+            return msb*16 + bit.rshift((lsb&0xF0),4)
         elseif regaddr ==0x11 then
             return (msb&0x0f)*256 + lsb
         else
@@ -106,7 +106,7 @@ local function chip_check()
         if revData:byte() ~= nil then
             SPL06_ADDRESS_ADR = SPL06_ADDRESS_ADR_LOW
         else
-            log.info("i2c", "Can't find adxl34x device")
+            log.info("i2c", "Can't find SPL06 device")
             return false
         end
     end
@@ -130,7 +130,7 @@ spl06初始化
 @usage
 spl06.init(0)
 ]]
-function spl06.init(i2cid)
+function spl06.init(i2c_id)
     i2cid = i2c_id
     sys.wait(20)--20 毫秒等待设备稳定
     if chip_check() then

+ 31 - 0
module/Air8000/project/整机开发板出厂工程/readme.md

@@ -0,0 +1,31 @@
+## 概述
+
+本工程的目的是为了方便展示Air8000的部分功能,客户可以根据功能进行自己需要的功能进行相关的测试,以及根据硬件设计手册进行相关的[硬件设计](https://docs.openluat.com/air8000/product/shouce/)
+功能上,开发板演示了如下的功能:
+1. GPS 定位 
+2. WIFI AP 
+3. WIFI STA
+4. 摄像头
+5. 电话
+6. 蓝牙
+7. 短信
+8. TTS
+9. 录音
+10. 播放音乐
+11. TF 卡读写
+12. 运动传感
+13. 电源管理
+14. 以太网LAN
+15. 以太网WAN
+16. 多网融合
+17. 485 (待开发)
+18. CAN(待开发)
+19. ONEWIRE(待开发)
+20. PWM(待开发)
+21. UART(待开发)
+22. 232(待开发)
+23. 对讲 
+
+## 说明
+
+需要根据自己需要的功能选择core,core 列表和功能[点击此处](https://docs.openluat.com/air8000/luatos/firmware/)

+ 4 - 5
module/Air8101/demolib/adxl34x.lua

@@ -135,11 +135,10 @@ log.info("adxl34x_data", "adxl34x_data.x"..(adxl34x_data.x),"adxl34x_data.y"..(a
 function adxl34x.get_data()
     local accel={x=nil,y=nil,z=nil}
     i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAX0)
-    _,accel.x = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
-    i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAY0)
-    _,accel.y = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
-    i2c.send(i2cid, ADXL34X_ADDRESS_ADR,ADXL34X_DATAZ0)
-    _,accel.z = pack.unpack(i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 2),">h")
+    local rcvData = i2c.recv(i2cid, ADXL34X_ADDRESS_ADR, 6)
+    if rcvData ~= nil and #rcvData == 6 then
+        accel.x, accel.y, accel.z = string.unpack("<hhh", rcvData)
+    end
     return accel
 end
 

+ 4 - 4
module/Air8101/demolib/spl06.lua

@@ -79,11 +79,11 @@ local function i2cReadTwoData(i2caddr,regaddr)
         msb = 0
         lsb = 0
         log.info("spl06 two data","the data is null")
-        return msb256+lsb
+        return msb*256+lsb
     else
     --log.info("spl06 two data","msb:"..msb.." lsb:"..lsb)
         if regaddr ==0x10 then
-            return msb16 + bit.rshift((lsb&0xF0),4)
+            return msb*16 + bit.rshift((lsb&0xF0),4)
         elseif regaddr ==0x11 then
             return (msb&0x0f)*256 + lsb
         else
@@ -106,7 +106,7 @@ local function chip_check()
         if revData:byte() ~= nil then
             SPL06_ADDRESS_ADR = SPL06_ADDRESS_ADR_LOW
         else
-            log.info("i2c", "Can't find adxl34x device")
+            log.info("i2c", "Can't find SPL06 device")
             return false
         end
     end
@@ -130,7 +130,7 @@ spl06初始化
 @usage
 spl06.init(0)
 ]]
-function spl06.init(i2cid)
+function spl06.init(i2c_id)
     i2cid = i2c_id
     sys.wait(20)--20 毫秒等待设备稳定
     if chip_check() then

+ 1740 - 0
script/libs/exeasyui.lua

@@ -0,0 +1,1740 @@
+--[[
+exEasyUI - 简化的UI组件库
+版本号:1.6.1
+作者: zengshuai 
+日期:2025-10-09
+=====================================
+
+结构说明:
+1. 常量定义 - UI颜色常量和调试配置
+2. 硬件依赖 - 使用exlcd/extp初始化LCD和TP,并使用gtfont初始化字体(可选)
+3. 核心部分 - 组件管理、事件分发、渲染系统
+4. 组件部分 - 目前有6个组件
+    - Button:按钮组件
+    - CheckBox:复选框组件
+    - Label:标签组件
+    - Picture:图片组件
+    - MessageBox:消息框组件
+    - Window:窗口组件
+    - ProgressBar:进度条组件
+
+基于原exSimpleUI重构,将所有代码合并为单个文件,便于使用和维护。
+支持触摸事件分发、组件渲染、主题切换等核心功能。
+]]
+
+local screen_data = require "screen_data_table"     -- 唯一引入屏幕配置参数的地方
+local exlcd = require "exlcd"             -- 显示驱动模块
+local extp = require "extp"                    -- 触摸驱动模块
+
+gtfont_dev = gtfont_dev or nil                  -- 全局SPI设备句柄,避免被GC
+
+-- ================================
+-- 1. 常量定义
+-- ================================
+
+-- UI颜色常量
+local COLOR_WHITE = 0xFFFF
+local COLOR_BLACK = 0x0000
+local COLOR_GRAY  = 0x8410
+local COLOR_BLUE  = 0x001F
+local COLOR_RED   = 0xF800
+local COLOR_GREEN = 0x07E0
+local COLOR_YELLOW = 0xFFE0
+local COLOR_CYAN = 0x07FF
+local COLOR_MAGENTA = 0xF81F
+local COLOR_ORANGE = 0xFC00
+local COLOR_PINK = 0xF81F
+
+-- Windows 11 风格颜色(v1.6.0新增)
+-- Light模式
+local COLOR_WIN11_LIGHT_DIALOG_BG = 0xF79E      -- RGB(243, 243, 243) - 对话框背景
+local COLOR_WIN11_LIGHT_BUTTON_BG = 0xFFDF      -- RGB(251, 251, 252) - 按钮背景
+local COLOR_WIN11_LIGHT_BUTTON_BORDER = 0xE73C  -- RGB(229, 229, 229) - 按钮边框
+-- Dark模式
+local COLOR_WIN11_DARK_DIALOG_BG = 0x2104      -- RGB(32, 32, 32) - 对话框背景
+local COLOR_WIN11_DARK_BUTTON_BG = 0x3186      -- RGB(51, 51, 51) - 按钮背景
+local COLOR_WIN11_DARK_BUTTON_BORDER = 0x4A69  -- RGB(76, 76, 76) - 按钮边框
+
+-- ================================
+-- 2. 硬件依赖部分 (hw)
+-- ================================
+
+local hw = {}
+local FontAdapter = { _backend = "default", _size = 12, _gray = false, _name = nil }
+
+-- 硬件初始化入口
+function hw.init(opts)
+	-- 初始化显示屏
+    -- 使用screen_data配置表中的参数初始化LCD,在配置表中修改即可
+    local lcd_init_success exlcd.init(screen_data.lcdargs)
+	
+
+    -- 检查LCD初始化是否成功
+    if lcd_init_success then
+        log.error("ui_main", "LCD初始化失败")
+        return  -- 初始化失败,退出任务
+    end
+
+	
+
+	-- 通用显示设置
+    lcd.setupBuff(nil, false)     -- 设置帧缓冲区
+    lcd.autoFlush(false)          -- 禁止自动刷新
+
+    -- -- 设置字体为模组自带的opposansm12中文字体
+    -- lcd.setFont(lcd.font_opposansm12_chinese)
+
+    -- 初始化触摸IC
+    -- 使用配置表中的参数初始化触摸
+    extp.init(screen_data.touch)
+    extp.setPublishEnabled("all", true) -- 发布所有消息
+
+    -- 自定义配置
+    extp.setSlideThreshold(40)        -- 设置滑动阈值为40像素
+    extp.setLongPressThreshold(600)   -- 设置长按阈值为600毫秒
+
+    -- 字体后端装配(保持原逻辑,可选)
+    local fcfg = opts.font or {}
+    if fcfg.type == "gtfont" then
+        local spi_id = (fcfg.spi and fcfg.spi.id) or 0
+        local spi_cs = (fcfg.spi and fcfg.spi.cs) or 8
+        local spi_clk = (fcfg.spi and fcfg.spi.clock) or (20 * 1000 * 1000)
+        gtfont_dev = spi.deviceSetup(spi_id or 1, spi_cs or 12, 0, 0, 8, spi_clk or (20*1000*1000), spi.MSB, 1, 0)
+        log.error("exEasyUI.gtfont", "spi.deviceSetup", type(gtfont_dev))
+        if type(gtfont_dev) ~= "userdata" then
+            log.error("exEasyUI.gtfont", "spi.deviceSetup error", type(gtfont_dev))
+            gtfont_dev = nil
+        end
+        local gtfont_ok = gtfont.init(gtfont_dev)
+        if gtfont_ok then
+            FontAdapter._backend = "gtfont"
+            FontAdapter._size = tonumber(fcfg.size or 16)
+            FontAdapter._gray = not not fcfg.gray
+            log.info("exEasyUI", "gtfont enabled", spi_id, spi_cs, FontAdapter._size)
+        else
+            FontAdapter._backend = "default"
+            FontAdapter._size = 12
+            FontAdapter._gray = false
+            log.warn("exEasyUI", "gtfont init failed, fallback to default font")
+        end
+    else
+        FontAdapter._backend = "default"
+        FontAdapter._size = 12
+        FontAdapter._gray = false
+        FontAdapter._name = (fcfg and fcfg.name) or nil
+        if lcd and lcd.setFont and lcd.font_opposansm12_chinese then
+            lcd.setFont(lcd.font_opposansm12_chinese)
+        end
+    end
+    return true
+end
+
+-- ================================
+-- 3. 核心部分 (core + event)
+-- ================================
+
+local core = {}
+local event = {}
+
+-- 组件注册表
+local registry = {}
+local last_action = nil
+local current_theme = "dark"
+
+-- 调试开关
+core.debug_touch = true
+
+-- 调试配置函数
+function core.debug(v)
+    if v == nil then return { touch = not not core.debug_touch } end
+    if type(v) == "boolean" then
+        core.debug_touch = v
+        return
+    end
+    if type(v) == "table" then
+        if v.touch ~= nil then core.debug_touch = not not v.touch end
+        return
+    end
+end
+
+-- 添加组件到渲染队列
+function core.add(component)
+    registry[#registry + 1] = component
+end
+
+-- 从注册表移除组件
+function core.remove(component)
+    for i = #registry, 1, -1 do
+        if registry[i] == component then
+            table.remove(registry, i)
+            return true
+        end
+    end
+    return false
+end
+
+-- 清屏
+function core.clear(color)
+    lcd.clear(color or COLOR_BLACK)
+end
+
+-- 渲染所有可见组件
+function core.render()
+    for i = 1, #registry do
+        local c = registry[i]
+        if c and c.visible ~= false and c.draw then c:draw() end
+    end
+    lcd.flush()
+end
+
+-- 获取当前时间戳
+local function now_ms()
+    if mcu and mcu.ticks then return mcu.ticks() end
+    return (os.clock() or 0) * 1000
+end
+
+-- 命中测试
+local function hit_test(x, y, r)
+    return x >= r.x and y >= r.y and x <= (r.x + r.w) and y <= (r.y + r.h)
+end
+
+-- 触摸事件分发(extp事件)
+function core.handleTouchEvent(evt, x, y)
+    local start_ms
+    if core.debug_touch then
+        start_ms = now_ms()
+    end
+    for i = #registry, 1, -1 do
+        local c = registry[i]
+        if c and c.enabled ~= false and c.handleEvent and 
+           (c._capture == true or hit_test(x, y, { x = c.x, y = c.y, w = c.w, h = c.h })) then
+            if c:handleEvent(evt, x, y) then
+                if core.debug_touch and start_ms then
+                    local dt = now_ms() - start_ms
+                    log.info("exEasyUI", "consumed_by", tostring(c.__name or "component"), string.format("%.2fms", dt))
+                end
+                return true
+            end
+        end
+    end
+    if core.debug_touch and start_ms then
+        local dt = now_ms() - start_ms
+        if evt ~= "MOVE_X" and evt ~= "MOVE_Y" then
+            log.info("exEasyUI", "not_consumed_cost", string.format("%.2fms", dt))
+        end
+    end
+    return false
+end
+
+-- 系统初始化
+function core.init(opts)
+    opts = opts or {}
+    
+    -- 主题设置:根据传入参数设置当前主题(light/dark)
+    if opts.theme == "light" or opts.theme == "dark" then
+        current_theme = opts.theme
+    end
+
+    -- 触摸事件订阅与转发(extp)
+    -- extp_down_x, extp_down_y 记录按下时的原始坐标
+    -- extp_curr_x, extp_curr_y 记录当前触摸点坐标(用于MOVE/滑动等)
+    local extp_down_x, extp_down_y
+    local extp_curr_x, extp_curr_y
+
+    -- extp_dispatch: 触摸事件分发函数,负责将底层触摸事件转换为UI事件
+    -- evt: 事件类型(如TOUCH_DOWN、MOVE_X、SINGLE_TAP等)
+    -- a, b: 事件参数(如坐标或偏移量)
+    local function extp_dispatch(evt, a, b)
+        if evt == "TOUCH_DOWN" then
+            -- 记录触摸按下时的原始坐标,a和b通常为触摸点的x、y坐标,若无法转换为数字则默认为0
+            extp_down_x, extp_down_y = tonumber(a) or 0, tonumber(b) or 0
+            extp_curr_x, extp_curr_y = extp_down_x, extp_down_y
+            if core.debug_touch then log.info("exEasyUI", "extp", "TOUCH_DOWN", extp_curr_x, extp_curr_y) end
+            -- 分发TOUCH_DOWN事件
+            core.handleTouchEvent("TOUCH_DOWN", extp_curr_x, extp_curr_y)
+            last_action = "TOUCH_DOWN"
+            return
+        end
+
+        -- 若未按下则忽略后续事件
+        if not extp_down_x or not extp_down_y then return end
+
+        if evt == "MOVE_X" then
+            -- 处理横向滑动,a为x方向偏移
+            local dx = tonumber(a) or 0
+            extp_curr_x = extp_down_x + dx
+            if core.debug_touch then log.info("exEasyUI", "extp", "MOVE_X", extp_curr_x, extp_curr_y) end
+            core.handleTouchEvent("MOVE_X", extp_curr_x, extp_curr_y)
+            last_action = "MOVE_X"
+            return
+        elseif evt == "MOVE_Y" then
+            -- 处理纵向滑动,b为y方向偏移
+            local dy = tonumber(b) or 0
+            extp_curr_y = extp_down_y + dy
+            if core.debug_touch then log.info("exEasyUI", "extp", "MOVE_Y", extp_curr_x, extp_curr_y) end
+            core.handleTouchEvent("MOVE_Y", extp_curr_x, extp_curr_y)
+            last_action = "MOVE_Y"
+            return
+        elseif evt == "SWIPE_LEFT" or evt == "SWIPE_RIGHT" then
+            -- 处理左右滑动手势,a为x方向偏移
+            local dx = tonumber(a) or 0
+            extp_curr_x = extp_down_x + dx
+            if core.debug_touch then log.info("exEasyUI", "extp", evt, extp_curr_x, extp_curr_y) end
+            core.handleTouchEvent(evt, extp_curr_x, extp_curr_y)
+            last_action = evt
+        elseif evt == "SWIPE_UP" or evt == "SWIPE_DOWN" then
+            -- 处理上下滑动手势,b为y方向偏移
+            local dy = tonumber(b) or 0
+            extp_curr_y = extp_down_y + dy
+            if core.debug_touch then log.info("exEasyUI", "extp", evt, extp_curr_x, extp_curr_y) end
+            core.handleTouchEvent(evt, extp_curr_x, extp_curr_y)
+            last_action = evt
+        elseif evt == "SINGLE_TAP" or evt == "LONG_PRESS" then
+            -- 处理单击/长按事件,a/b为最终坐标(若无则用当前坐标)
+            local ux = tonumber(a) or extp_curr_x or 0
+            local uy = tonumber(b) or extp_curr_y or 0
+            if core.debug_touch then log.info("exEasyUI", "extp", evt, ux, uy) end
+            core.handleTouchEvent(evt, ux, uy)
+            last_action = evt
+        end
+
+        -- 触摸序列结束后,清空坐标状态
+        if last_action == "SINGLE_TAP" or last_action == "LONG_PRESS" or
+           last_action == "SWIPE_LEFT" or last_action == "SWIPE_RIGHT" or
+           last_action == "SWIPE_UP" or last_action == "SWIPE_DOWN" then
+            extp_down_x, extp_down_y = nil, nil
+            extp_curr_x, extp_curr_y = nil, nil
+        end
+    end
+
+    -- 订阅底层触摸事件(baseTouchEvent),由extp_dispatch处理
+    sys.subscribe("baseTouchEvent", extp_dispatch)
+end
+
+-- 获取当前主题
+function core.getTheme()
+    return current_theme
+end
+
+-- 事件系统:订阅事件
+function event.on(name, cb)
+    return sys.subscribe(name, cb)
+end
+
+-- 事件系统:发送事件
+function event.emit(name, ...)
+    return sys.publish(name, ...)
+end
+
+-- ================================
+-- 4. 组件部分
+-- ================================
+
+-- 通用绘图函数
+local function fill_rect(x1, y1, x2, y2, color)
+    lcd.fill(x1, y1, x2, y2 + 1, color) -- 右下边界为不含区间, y2需要+1
+end
+
+local function stroke_rect(x1, y1, x2, y2, color)
+    lcd.drawLine(x1, y1, x2, y1, color)
+    lcd.drawLine(x2, y1, x2, y2, color)
+    lcd.drawLine(x2, y2, x1, y2, color)
+    lcd.drawLine(x1, y2, x1, y1, color)
+end
+
+-- v1.6.1新增:绘制图片占位符(方框+X叉)
+local function draw_image_placeholder(x, y, w, h, bg_color, border_color)
+    bg_color = bg_color or 0x8410  -- 默认灰色
+    border_color = border_color or COLOR_WHITE
+    
+    -- 填充背景
+    fill_rect(x, y, x + w - 1, y + h - 1, bg_color)
+    
+    -- 绘制边框
+    stroke_rect(x, y, x + w - 1, y + h - 1, border_color)
+    
+    -- 绘制X叉(对角线)
+    lcd.drawLine(x, y, x + w - 1, y + h - 1, border_color)
+    lcd.drawLine(x + w - 1, y, x, y + h - 1, border_color)
+    
+    -- 如果尺寸足够大,绘制内缩的X叉使其更明显
+    if w >= 20 and h >= 20 then
+        local margin = math.min(w, h) // 8  -- 内缩边距
+        lcd.drawLine(x + margin, y + margin, x + w - 1 - margin, y + h - 1 - margin, border_color)
+        lcd.drawLine(x + w - 1 - margin, y + margin, x + margin, y + h - 1 - margin, border_color)
+    end
+end
+
+-- FontAdapter 实现
+local function font_line_height(style)
+    if FontAdapter._backend == "gtfont" then
+        local sz = (style and style.size) or FontAdapter._size or 16
+        return sz
+    end
+    -- default backend:优先使用样式中的 size,没有则回退 12
+    if style and style.size then
+        return tonumber(style.size) or 12
+    end
+    return 12
+end
+
+local function font_set(style)
+    style = style or {}
+    if FontAdapter._backend == "gtfont" then
+        FontAdapter._size = tonumber(style.size or FontAdapter._size or 16)
+        FontAdapter._gray = (style.gray ~= nil) and not not style.gray or FontAdapter._gray
+        if lcd and lcd.setFont and lcd.drawGtfontUtf8 then
+            lcd.setFont(lcd.drawGtfontUtf8)
+        end
+        return
+    end
+    -- default backend
+    FontAdapter._name = style.name or FontAdapter._name
+    if lcd and lcd.setFont then
+        -- 优先按 name,其次按 size 猜测常见字体名,最后回退
+        if FontAdapter._name and lcd["font_" .. FontAdapter._name] then
+            lcd.setFont(lcd["font_" .. FontAdapter._name])
+        elseif style and style.size then
+            local size_num = tonumber(style.size)
+            if size_num then
+                local guess = "font_opposansm" .. tostring(size_num) .. "_chinese"
+                if lcd[guess] then
+                    lcd.setFont(lcd[guess])
+                elseif lcd.font_opposansm12_chinese then
+                    lcd.setFont(lcd.font_opposansm12_chinese)
+                end
+            elseif lcd.font_opposansm12_chinese then
+                lcd.setFont(lcd.font_opposansm12_chinese)
+            end
+        elseif lcd.font_opposansm12_chinese then
+            lcd.setFont(lcd.font_opposansm12_chinese)
+        end
+    end
+end
+
+local function font_draw(text, x, y, color, style)
+    color = color or COLOR_WHITE
+    style = style or {}
+    if FontAdapter._backend == "gtfont" then
+        local sz = tonumber(style.size or FontAdapter._size or 16)
+        if FontAdapter._gray and lcd.drawGtfontUtf8Gray then
+            -- 固件灰度级目前不可调,传固定值4
+            lcd.drawGtfontUtf8Gray(text, sz, 4, x, y, color)
+        elseif lcd.drawGtfontUtf8 then
+            lcd.drawGtfontUtf8(text, sz, x, y, color)
+        else
+            -- 回退:不应触达
+            if lcd.drawStr then
+                lcd.drawStr(x, y + 12, text, color)
+            end
+        end
+        return
+    end
+    -- default backend:y 为顶部坐标,内部转换为基线
+    if lcd and lcd.setFont then
+        if FontAdapter._name and lcd["font_" .. FontAdapter._name] then
+            lcd.setFont(lcd["font_" .. FontAdapter._name])
+        else
+            -- 尝试根据 size 选择合适字体
+            local used = false
+            if style and style.size then
+                local guess = "font_opposansm" .. tostring(style.size) .. "_chinese"
+                if lcd[guess] then
+                    lcd.setFont(lcd[guess])
+                    used = true
+                end
+            end
+            if not used and lcd.font_opposansm12_chinese then
+                lcd.setFont(lcd.font_opposansm12_chinese)
+            end
+        end
+    end
+    local lh = font_line_height(style)
+    lcd.drawStr(x, y + lh, text, color)
+end
+
+-- 文本宽度测量
+local function font_measure(text, style)
+    if not text or text == "" then return 0 end
+    style = style or {}
+    if FontAdapter._backend == "gtfont" then
+        local sz = tonumber(style.size or FontAdapter._size or 16)
+        local w = 0
+        local i = 1
+        while i <= #text do
+            local b = string.byte(text, i)
+            if b == 32 then -- space
+                w = w + math.ceil(sz / 2)
+                i = i + 1
+            elseif b < 128 then
+                w = w + math.ceil(sz / 2)
+                i = i + 1
+            else
+                w = w + sz
+                -- 简化处理UTF-8宽字节
+                if i + 2 <= #text then i = i + 3 else i = i + 1 end
+            end
+        end
+        return w
+    end
+    -- default backend,尽量使用原生接口
+    if lcd and lcd.setFont then
+        -- 尝试在测量前设置到期望字体,以匹配绘制
+        if FontAdapter._name and lcd["font_" .. FontAdapter._name] then
+            lcd.setFont(lcd["font_" .. FontAdapter._name])
+        elseif style and style.size then
+            local guess = "font_opposansm" .. tostring(style.size) .. "_chinese"
+            if lcd[guess] then
+                lcd.setFont(lcd[guess])
+            elseif lcd.font_opposansm12_chinese then
+                lcd.setFont(lcd.font_opposansm12_chinese)
+            end
+        elseif lcd.font_opposansm12_chinese then
+            lcd.setFont(lcd.font_opposansm12_chinese)
+        end
+    end
+    if lcd and lcd.getStrWidth then return lcd.getStrWidth(text) end
+    if lcd and lcd.strWidth then return lcd.strWidth(text) end
+    if lcd and lcd.get_string_width then return lcd.get_string_width(text) end
+    -- 估算:英文约为 size/2,中文约为 size
+    local width = 0
+    local i = 1
+    while i <= #text do
+        local byte = string.byte(text, i)
+        if byte < 128 then
+            width = width + math.ceil((tonumber(style.size) or 12) / 2)
+            i = i + 1
+        else
+            width = width + (tonumber(style.size) or 12)
+            i = i + 3
+        end
+    end
+    return width
+end
+
+-- UTF-8字符获取(返回字符和字节长度)
+local function get_utf8_char(text, i)
+    if not text or i > #text then return "", 0 end
+    local byte = string.byte(text, i)
+    if byte < 128 then
+        -- ASCII字符(1字节)
+        return string.sub(text, i, i), 1
+    elseif byte >= 224 and byte < 240 then
+        -- 3字节UTF-8字符(中文等)
+        if i + 2 <= #text then
+            return string.sub(text, i, i + 2), 3
+        else
+            return string.sub(text, i, i), 1
+        end
+    elseif byte >= 192 and byte < 224 then
+        -- 2字节UTF-8字符
+        if i + 1 <= #text then
+            return string.sub(text, i, i + 1), 2
+        else
+            return string.sub(text, i, i), 1
+        end
+    elseif byte >= 240 then
+        -- 4字节UTF-8字符
+        if i + 3 <= #text then
+            return string.sub(text, i, i + 3), 4
+        else
+            return string.sub(text, i, i), 1
+        end
+    else
+        -- 其他情况
+        return string.sub(text, i, i), 1
+    end
+end
+
+-- 文本换行处理(返回行数组)
+-- 支持英文按单词换行,中文按字符换行
+local function wrap_text_lines(text, maxWidth, style)
+    if not text or text == "" then return {""} end
+    if not maxWidth or maxWidth <= 0 then return {text} end
+    
+    local lines = {}
+    local currentLine = ""
+    local currentWidth = 0
+    local wordBuffer = ""  -- 当前英文单词缓冲
+    local wordWidth = 0    -- 当前单词宽度
+    local i = 1
+    
+    while i <= #text do
+        local char, charLen = get_utf8_char(text, i)
+        local charWidth = font_measure(char, style)
+        local byte = string.byte(text, i)
+        
+        -- 判断是否为英文字母或数字
+        local isAlphaNum = (byte >= 48 and byte <= 57) or   -- 0-9
+                          (byte >= 65 and byte <= 90) or    -- A-Z
+                          (byte >= 97 and byte <= 122)      -- a-z
+        
+        if isAlphaNum then
+            -- 英文字符或数字,加入单词缓冲
+            wordBuffer = wordBuffer .. char
+            wordWidth = wordWidth + charWidth
+            i = i + charLen
+        else
+            -- 非英文字符(空格、标点、中文等)
+            -- 先处理缓冲的单词
+            if wordBuffer ~= "" then
+                if currentWidth + wordWidth > maxWidth then
+                    -- 单词放不下
+                    if currentLine ~= "" then
+                        -- 当前行有内容,换行后放单词
+                        table.insert(lines, currentLine)
+                        currentLine = wordBuffer
+                        currentWidth = wordWidth
+                    else
+                        -- 单词本身超长,强制显示
+                        currentLine = wordBuffer
+                        currentWidth = wordWidth
+                    end
+                else
+                    -- 单词可以放下
+                    currentLine = currentLine .. wordBuffer
+                    currentWidth = currentWidth + wordWidth
+                end
+                wordBuffer = ""
+                wordWidth = 0
+            end
+            
+            -- 处理当前字符(空格、标点、中文等)
+            if char == " " then
+                -- 空格:尝试加入当前行
+                if currentWidth + charWidth <= maxWidth then
+                    currentLine = currentLine .. char
+                    currentWidth = currentWidth + charWidth
+                else
+                    -- 空格放不下,换行(空格不放到下一行开头)
+                    if currentLine ~= "" then
+                        table.insert(lines, currentLine)
+                    end
+                    currentLine = ""
+                    currentWidth = 0
+                end
+            else
+                -- 标点或中文
+                if currentWidth + charWidth > maxWidth then
+                    -- 字符放不下,换行
+                    if currentLine ~= "" then
+                        table.insert(lines, currentLine)
+                    end
+                    currentLine = char
+                    currentWidth = charWidth
+                else
+                    -- 字符可以放下
+                    currentLine = currentLine .. char
+                    currentWidth = currentWidth + charWidth
+                end
+            end
+            i = i + charLen
+        end
+    end
+    
+    -- 处理剩余的单词
+    if wordBuffer ~= "" then
+        if currentWidth + wordWidth > maxWidth and currentLine ~= "" then
+            -- 单词放不下,换行
+            table.insert(lines, currentLine)
+            currentLine = wordBuffer
+        else
+            currentLine = currentLine .. wordBuffer
+        end
+    end
+    
+    -- 添加最后一行
+    if currentLine ~= "" then
+        table.insert(lines, currentLine)
+    end
+    
+    -- 至少返回一个空行
+    if #lines == 0 then
+        lines = {""}
+    end
+    
+    return lines
+end
+
+-- 兼容旧接口已移除,统一使用下方两个新 API
+
+-- 新增:直接绘制文本 API(支持 style.size/name/gray)
+local function draw_text_direct(x, y, text, opts)
+    opts = opts or {}
+    local color = opts.color or COLOR_WHITE
+    local style = opts.style or {}
+    font_draw(text or "", x, y, color, style)
+end
+
+-- 新增:在矩形内自适应(仅居中与边界约束,不缩放、不换行)
+local function draw_text_in_rect_centered(x, y, w, h, text, opts)
+    opts = opts or {}
+    local color = opts.color or COLOR_WHITE
+    local style = opts.style or {}
+    local padding = opts.padding or 0
+
+    local tw = font_measure(text or "", style)
+    local lh = font_line_height(style)
+
+    local inner_x = x + padding
+    local inner_y = y + padding
+    local inner_w = w - padding * 2
+    local inner_h = h - padding * 2
+
+    local tx = inner_x + (inner_w - tw) // 2
+    local ty = inner_y + (inner_h - lh) // 2
+
+    -- 夹紧,避免越界
+    tx = math.max(inner_x, tx)
+    tx = math.min(inner_x + inner_w - tw, tx)
+
+    font_draw(text or "", tx, ty, color, style)
+end
+
+-- 旧的宽度测量包装已移除,请直接使用 font_measure(text, style)
+
+-- Button组件 - 基础按钮,支持文本/图片、按下/抬起与点击回调,支持toggle模式
+local Button = {}
+Button.__index = Button
+
+function Button:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.w = opts.width or opts.w or 100
+    o.h = opts.height or opts.h or 36
+    
+    -- 文本模式参数
+    o.text = opts.text or "Button"
+    o.textSize = opts.textSize or opts.size
+    local dark = (current_theme == "dark")
+    o.bgColor = opts.bgColor or (dark and COLOR_WIN11_DARK_BUTTON_BG or COLOR_WIN11_LIGHT_BUTTON_BG)
+    o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.borderColor = opts.borderColor or (dark and COLOR_WIN11_DARK_BUTTON_BORDER or COLOR_WIN11_LIGHT_BUTTON_BORDER)
+    
+    -- 图片模式参数(v1.6.0新增,合并自ToolButton)
+    o.src = opts.src
+    o.src_pressed = opts.src_pressed
+    o.src_toggled = opts.src_toggled
+    
+    -- Toggle模式参数(v1.6.0新增)
+    o.toggle = opts.toggle or false
+    o.toggled = opts.toggled or false
+    o.onToggle = opts.onToggle
+    
+    -- 状态
+    o.pressed = false
+    o.onClick = opts.onClick
+    o.visible = opts.visible ~= false
+    o.enabled = opts.enabled ~= false
+    o._imageCache = {}  -- v1.6.1:缓存图片加载状态,避免重复检查和重复打印警告
+    return o
+end
+
+function Button:draw()
+    if not self.visible then return end
+    
+    -- 图片模式:优先显示图片
+    if self.src then
+        local path
+        if self.toggle and self.toggled then
+            path = self.src_toggled or self.src
+        elseif self.pressed then
+            path = self.src_pressed or self.src
+        else
+            path = self.src
+        end
+        
+        -- v1.6.1修复:检查文件是否存在,并改进占位符显示(使用缓存避免重复检查)
+        if type(path) == "string" and path ~= "" and path:lower():sub(-4) == ".jpg" then
+            -- 检查缓存
+            if self._imageCache[path] == nil then
+                -- 未缓存,首次检查文件是否存在
+                if io and io.exists and io.exists(path) then
+                    self._imageCache[path] = true  -- 缓存:文件存在
+                else
+                    self._imageCache[path] = false  -- 缓存:文件不存在
+                    log.warn("Button", "图片文件不存在:", path)
+                end
+            end
+            
+            -- 根据缓存状态处理
+            if self._imageCache[path] == true then
+                lcd.showImage(self.x, self.y, path)
+            else
+                -- 文件不存在(已缓存),直接显示占位符,不再重复警告
+                draw_image_placeholder(self.x, self.y, self.w, self.h, COLOR_GRAY, COLOR_WHITE)
+            end
+        else
+            -- path无效或不是jpg,显示占位符
+            draw_image_placeholder(self.x, self.y, self.w, self.h, COLOR_GRAY, COLOR_WHITE)
+        end
+        return
+    end
+    
+    -- 文本模式:绘制文本按钮
+    local bg = self.pressed and COLOR_GRAY or self.bgColor
+    fill_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, bg)
+    stroke_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, self.borderColor)
+    draw_text_in_rect_centered(self.x, self.y, self.w, self.h, self.text, {
+        color = self.textColor,
+        style = { size = self.textSize },
+        padding = 2
+    })
+end
+
+function Button:setText(newText)
+    self.text = tostring(newText or "")
+end
+
+function Button:handleEvent(evt, x, y)
+    if not self.enabled then return false end
+    local inside = hit_test(x, y, { x = self.x, y = self.y, w = self.w, h = self.h })
+    
+    if evt == "TOUCH_DOWN" and inside then
+        self.pressed = true
+        self._capture = true
+        return true
+    elseif evt == "MOVE_X" or evt == "MOVE_Y" then
+        if self._capture then
+            self.pressed = inside
+            return true
+        end
+    elseif evt == "SINGLE_TAP" then
+        local was_pressed = self.pressed
+        self.pressed = false
+        self._capture = false
+        if was_pressed and inside then
+            -- Toggle模式处理
+            if self.toggle then
+                self.toggled = not self.toggled
+                if self.onToggle then self.onToggle(self.toggled, self) end
+            end
+            -- 触发点击回调
+            if self.onClick then self.onClick(self) end
+            return true
+        end
+        return true
+    elseif evt == "LONG_PRESS" or evt == "SWIPE_LEFT" or evt == "SWIPE_RIGHT" or evt == "SWIPE_UP" or evt == "SWIPE_DOWN" then
+        self.pressed = false
+        self._capture = false
+        return true
+    end
+    return false
+end
+
+-- CheckBox组件 - 复选框,支持选中状态切换
+local CheckBox = {}
+CheckBox.__index = CheckBox
+
+function CheckBox:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.boxSize = opts.boxSize or 16
+    o.text = opts.text
+    local dark = (current_theme == "dark")
+    o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.borderColor = opts.borderColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.bgColor = opts.bgColor or (dark and COLOR_BLACK or COLOR_WHITE)
+    o.tickColor = opts.tickColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.checked = opts.checked or false
+    o.enabled = opts.enabled ~= false
+    o.visible = opts.visible ~= false
+    o.onChange = opts.onChange
+    local text_w = (o.text and (#o.text * 6 + 6) or 0)
+    o.w = o.boxSize + text_w
+    o.h = math.max(o.boxSize, 16)
+    return o
+end
+
+function CheckBox:draw()
+    local x2 = self.x + self.boxSize - 1
+    local y2 = self.y + self.boxSize - 1
+    stroke_rect(self.x, self.y, x2, y2, self.borderColor)
+    fill_rect(self.x + 2, self.y + 2, x2 - 2, y2 - 2, self.bgColor)
+    if self.checked then
+        local pad = 2
+        fill_rect(self.x + pad, self.y + pad, x2 - pad, y2 - pad, self.tickColor)
+    end
+    if self.text then
+        local lh = font_line_height(nil)
+        local ty = self.y + (self.h - lh) // 2
+        draw_text_direct(self.x + self.boxSize + 10, ty, self.text, { color = self.textColor })
+    end
+end
+
+function CheckBox:setChecked(v)
+    local nv = not not v
+    if nv ~= self.checked then
+        self.checked = nv
+        if self.onChange then self.onChange(self.checked) end
+    end
+end
+
+function CheckBox:toggle()
+    self:setChecked(not self.checked)
+end
+
+function CheckBox:handleEvent(evt, x, y)
+    if not self.enabled then return false end
+    if evt == "SINGLE_TAP" then
+        if x >= self.x and y >= self.y and x <= (self.x + self.w) and y <= (self.y + self.h) then
+            self:toggle()
+            return true
+        end
+    end
+    return false
+end
+
+-- Label组件 - 文本标签,仅显示不响应事件
+local Label = {}
+Label.__index = Label
+
+function Label:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.text = tostring(opts.text or "")
+    local dark = (current_theme == "dark")
+    o.color = opts.color or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.font = opts.font
+    o.size = opts.size or opts.textSize
+    o.wordWrap = not not opts.wordWrap  -- 是否启用换行
+    o._autoW = (opts.w == nil)
+    o._autoH = true  -- 高度始终自动计算
+    o.visible = opts.visible ~= false
+    o.enabled = opts.enabled ~= false
+    
+    -- 宽度处理
+    local style = { size = o.size }
+    if opts.w then
+        o.w = opts.w
+        o._autoW = false
+    else
+        o.w = font_measure(o.text, style)
+        o._autoW = true
+    end
+    
+    -- 高度处理(根据是否换行)
+    local lh = font_line_height(style)
+    if o.wordWrap and not o._autoW then
+        -- 启用换行且指定了宽度,计算多行高度
+        local lines = wrap_text_lines(o.text, o.w, style)
+        o.h = #lines * lh
+        o._lines = lines
+    else
+        -- 单行高度
+        o.h = lh
+        o._lines = nil
+    end
+    
+    return o
+end
+
+function Label:setText(t)
+    self.text = tostring(t or "")
+    local style = { size = self.size }
+    local lh = font_line_height(style)
+    
+    -- 更新宽度(如果自动)
+    if self._autoW then
+        self.w = font_measure(self.text, style)
+    end
+    
+    -- 更新高度和行缓存
+    if self.wordWrap and not self._autoW then
+        local lines = wrap_text_lines(self.text, self.w, style)
+        self.h = #lines * lh
+        self._lines = lines
+    else
+        self.h = lh
+        self._lines = nil
+    end
+end
+
+function Label:setSize(sz)
+    self.size = tonumber(sz) or self.size
+    local style = { size = self.size }
+    local lh = font_line_height(style)
+    
+    -- 更新宽度(如果自动)
+    if self._autoW then
+        self.w = font_measure(self.text or "", style)
+    end
+    
+    -- 更新高度和行缓存
+    if self.wordWrap and not self._autoW then
+        local lines = wrap_text_lines(self.text or "", self.w, style)
+        self.h = #lines * lh
+        self._lines = lines
+    else
+        self.h = lh
+        self._lines = nil
+    end
+end
+
+function Label:draw()
+    if not self.visible then return end
+    
+    local style = { size = self.size }
+    
+    -- 若指定自定义字体指针,走默认后端路径(不支持换行)
+    if self.font and lcd and lcd.setFont then
+        lcd.setFont(self.font)
+        local lh = font_line_height(nil)
+        lcd.drawStr(self.x, self.y + lh, self.text, self.color)
+        return
+    end
+    
+    -- 换行模式
+    if self.wordWrap and not self._autoW then
+        local lines = self._lines or wrap_text_lines(self.text, self.w, style)
+        local lh = font_line_height(style)
+        for i = 1, #lines do
+            local yPos = self.y + (i - 1) * lh
+            draw_text_direct(self.x, yPos, lines[i], { color = self.color, style = style })
+        end
+        return
+    end
+    
+    -- 无换行模式:截断显示
+    if not self._autoW then
+        -- 有指定宽度限制,需要截断
+        local displayText = self.text
+        local tw = font_measure(displayText, style)
+        if tw > self.w then
+            -- 逐字符截断,直到宽度合适
+            local truncated = ""
+            local i = 1
+            while i <= #displayText do
+                local char, charLen = get_utf8_char(displayText, i)
+                local testText = truncated .. char
+                if font_measure(testText, style) <= self.w then
+                    truncated = testText
+                    i = i + charLen
+                else
+                    break
+                end
+            end
+            displayText = truncated
+        end
+        draw_text_direct(self.x, self.y, displayText, { color = self.color, style = style })
+    else
+        -- 自动宽度,直接显示
+        draw_text_direct(self.x, self.y, self.text, { color = self.color, style = style })
+    end
+end
+
+function Label:handleEvent(evt, x, y)
+    return false -- 文本不拦截事件
+end
+
+-- Picture组件 - 显示单图或轮播多图
+local Picture = {}
+Picture.__index = Picture
+
+function Picture:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.w = opts.w or 80
+    o.h = opts.h or 80
+    o.src = opts.src
+    o.sources = opts.sources
+    o.index = opts.index or 1
+    o.autoplay = not not opts.autoplay
+    o.interval = opts.interval or 1000
+    o._last_switch = now_ms()
+    o.visible = opts.visible ~= false
+    o.enabled = opts.enabled ~= false
+    o._imageCache = {}  -- v1.6.1:缓存图片加载状态,避免重复检查和重复打印警告
+    return o
+end
+
+function Picture:setSources(list)
+    self.sources = list
+    self.index = 1
+end
+
+function Picture:next()
+    if self.sources and #self.sources > 0 then
+        self.index = self.index % #self.sources + 1
+    end
+end
+
+function Picture:prev()
+    if self.sources and #self.sources > 0 then
+        self.index = (self.index - 2) % #self.sources + 1
+    end
+end
+
+function Picture:play() 
+    self.autoplay = true 
+end
+
+function Picture:pause() 
+    self.autoplay = false 
+end
+
+function Picture:draw()
+    if not self.visible then return end
+    -- 自动轮播
+    if self.autoplay and self.sources and #self.sources > 1 then
+        local t = now_ms()
+        if (t - self._last_switch) >= self.interval then
+            self:next()
+            self._last_switch = t
+        end
+    end
+    -- 选择当前图片路径
+    local path
+    if self.sources and #self.sources > 0 then
+        path = self.sources[self.index]
+    else
+        path = self.src
+    end
+    
+    -- v1.6.1修复:检查文件是否存在,并改进占位符显示(使用缓存避免重复检查)
+    if type(path) == "string" and path ~= "" and path:lower():sub(-4) == ".jpg" then
+        -- 检查缓存
+        if self._imageCache[path] == nil then
+            -- 未缓存,首次检查文件是否存在
+            if io and io.exists and io.exists(path) then
+                self._imageCache[path] = true  -- 缓存:文件存在
+            else
+                self._imageCache[path] = false  -- 缓存:文件不存在
+                log.warn("Picture", "图片文件不存在:", path)
+            end
+        end
+        
+        -- 根据缓存状态处理
+        if self._imageCache[path] == true then
+            lcd.showImage(self.x, self.y, path)
+        else
+            -- 文件不存在(已缓存),显示占位符
+            draw_image_placeholder(self.x, self.y, self.w, self.h, 0x4208, COLOR_WHITE)
+        end
+    elseif path then
+        -- path不是jpg或无效路径,显示占位符
+        draw_image_placeholder(self.x, self.y, self.w, self.h, 0x4208, COLOR_WHITE)
+    end
+    -- 如果path为nil,不显示任何内容(不绘制占位符)
+end
+
+function Picture:handleEvent(evt, x, y)
+    return false -- 默认不消费事件
+end
+
+-- MessageBox组件 - 消息框,包含标题、文本和按钮组
+local MessageBox = {}
+MessageBox.__index = MessageBox
+
+function MessageBox:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 20
+    o.y = opts.y or 40
+    o.w = opts.width or opts.w or 280
+    o.h = opts.height or opts.h or 160
+    o.title = opts.title or "Info"
+    o.message = opts.message or ""
+    o.wordWrap = opts.wordWrap ~= false  -- v1.6.1修复:默认启用自动换行,除非显式传入false
+    o.textSize = opts.textSize or opts.size  -- 文本字号
+    local dark = (current_theme == "dark")
+    o.borderColor = opts.borderColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.bgColor = opts.bgColor or (dark and COLOR_BLACK or COLOR_WHITE)
+    o.buttons = opts.buttons or { "OK" }
+    o.onResult = opts.onResult
+    o.visible = opts.visible ~= false  -- v1.6.1修复:支持从opts读取visible参数,默认true
+    o.enabled = opts.enabled ~= false  -- v1.6.1修复:支持从opts读取enabled参数,默认true
+
+    -- 内部按钮布局
+    o._btns = {}
+    local btn_w = 80
+    local gap = 12
+    local total_w = #o.buttons * btn_w + (#o.buttons - 1) * gap
+    local bx = o.x + (o.w - total_w) // 2
+    local by = o.y + o.h - 12 - 36
+    for i = 1, #o.buttons do
+        local label = o.buttons[i]
+        local b = Button:new({ x = bx, y = by, w = btn_w, h = 36, text = label })
+        b.onClick = function()
+            if o.onResult then o.onResult(label) end
+            o.visible = false
+            -- v1.6.1修复:不再禁用enabled,允许MessageBox复用
+        end
+        o._btns[#o._btns + 1] = b
+        bx = bx + btn_w + gap
+    end
+    
+    -- 计算message文本可用区域
+    o._msgPadding = 10  -- 左右内边距
+    o._msgMaxWidth = o.w - o._msgPadding * 2
+    o._msgStartY = 36  -- message文本起始Y(相对于MessageBox)
+    -- v1.6.1修复:根据是否有按钮动态计算可用高度
+    if #o.buttons > 0 then
+        o._msgMaxHeight = o.h - 12 - 36 - o._msgStartY  -- 有按钮:预留底部边距12 + 按钮高度36
+    else
+        o._msgMaxHeight = o.h - 10 - o._msgStartY  -- 无按钮:只保留底部边距10
+    end
+    
+    return o
+end
+
+function MessageBox:draw()
+    if not self.visible then return end
+    fill_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, self.bgColor)
+    stroke_rect(self.x, self.y, self.x + self.w - 1, self.y + self.h - 1, self.borderColor)
+    
+    -- 绘制标题
+    draw_text_direct(self.x + 10, self.y + 8, self.title, { color = self.textColor, style = { size = self.textSize } })
+    
+    -- 绘制message文本
+    local msgX = self.x + self._msgPadding
+    local msgY = self.y + self._msgStartY
+    local style = { size = self.textSize }
+    
+    if self.wordWrap then
+        -- 换行模式:在固定高度内显示多行,超出截断
+        local lines = wrap_text_lines(self.message, self._msgMaxWidth, style)
+        local lh = font_line_height(style)
+        local maxLines = math.floor(self._msgMaxHeight / lh)
+        
+        for i = 1, math.min(#lines, maxLines) do
+            local yPos = msgY + (i - 1) * lh
+            draw_text_direct(msgX, yPos, lines[i], { color = self.textColor, style = style })
+        end
+    else
+        -- 无换行模式:单行显示
+        draw_text_direct(msgX, msgY, self.message, { color = self.textColor, style = style })
+    end
+    
+    -- 绘制按钮
+    for i = 1, #self._btns do 
+        self._btns[i]:draw() 
+    end
+end
+
+function MessageBox:handleEvent(evt, x, y)
+    if not self.enabled then return false end
+    for i = 1, #self._btns do
+        local b = self._btns[i]
+        if hit_test(x, y, { x = b.x, y = b.y, w = b.w, h = b.h }) then
+            return b:handleEvent(evt, x, y)
+        end
+    end
+    return true -- 拦截其它事件
+end
+
+-- v1.6.1新增:MessageBox复用方法
+function MessageBox:show()
+    self.visible = true
+    self.enabled = true
+end
+
+function MessageBox:hide()
+    self.visible = false
+end
+
+function MessageBox:setTitle(title)
+    self.title = tostring(title or "")
+end
+
+function MessageBox:setMessage(message)
+    self.message = tostring(message or "")
+    -- 如果启用了换行,更新行缓存
+    if self.wordWrap then
+        local style = { size = self.textSize }
+        self._lines = wrap_text_lines(self.message, self._msgMaxWidth, style)
+    end
+end
+
+-- Window组件 - 窗口容器,支持子组件管理和子页面导航
+local Window = {}
+Window.__index = Window
+
+function Window:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    local sw, sh = lcd.getSize()
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.w = opts.w or sw
+    o.h = opts.h or sh
+    o.backgroundImage = opts.backgroundImage
+    local dark = (current_theme == "dark")
+    o.backgroundColor = opts.backgroundColor or (dark and COLOR_WIN11_DARK_DIALOG_BG or COLOR_WIN11_LIGHT_DIALOG_BG)
+    o.children = {}
+    o.visible = opts.visible ~= false
+    o.enabled = opts.enabled ~= false
+    o._managed = nil
+    o:enableSubpageManager()
+    -- 滚动配置(0.1 版:纵向/横向)
+    o._scroll = nil
+    return o
+end
+
+function Window:add(child)
+    self.children[#self.children + 1] = child
+end
+
+function Window:remove(child)
+    for i = #self.children, 1, -1 do
+        if self.children[i] == child then 
+            table.remove(self.children, i) 
+            return true 
+        end
+    end
+    return false
+end
+
+function Window:clear()
+    self.children = {}
+end
+
+function Window:setBackgroundImage(path)
+    self.backgroundImage = path
+end
+
+function Window:setBackgroundColor(color)
+    self.backgroundColor = color
+    self.backgroundImage = nil
+end
+
+function Window:draw()
+    -- 背景
+    if self.backgroundImage then
+        lcd.showImage(self.x, self.y, self.backgroundImage)
+    else
+        lcd.fill(self.x, self.y, self.x + self.w, self.y + self.h, self.backgroundColor)
+    end
+    -- 子组件
+    local offX, offY = 0, 0
+    if self._scroll and self._scroll.enabled then
+        if self._scroll.direction == "vertical" then
+            offY = self._scroll.offsetY or 0
+        elseif self._scroll.direction == "horizontal" then
+            offX = self._scroll.offsetX or 0
+        elseif self._scroll.direction == "both" then
+            offX = self._scroll.offsetX or 0
+            offY = self._scroll.offsetY or 0
+        end
+    end
+    for i = 1, #self.children do
+        local c = self.children[i]
+        if c and c.visible ~= false and c.draw then
+            local ox, oy = c.x, c.y
+            if self._scroll and self._scroll.enabled then c.x = ox + offX c.y = oy + offY end
+            c:draw()
+            if self._scroll and self._scroll.enabled then c.x, c.y = ox, oy end
+        end
+    end
+end
+
+function Window:handleEvent(evt, x, y)
+    if not self.enabled then return false end
+    if not hit_test(x, y, { x = self.x, y = self.y, w = self.w, h = self.h }) then return false end
+    -- 简易滚动(0.1):vertical/horizontal
+    if self._scroll and self._scroll.enabled then
+        local sc = self._scroll
+        local contentW = sc.contentWidth or self.w
+        local contentH = sc.contentHeight or self.h
+        local minX = math.min(0, self.w - (contentW or self.w))
+        local maxX = 0
+        local minY = math.min(0, self.h - (contentH or self.h))
+        local maxY = 0
+        if evt == "TOUCH_DOWN" then
+            sc.startX = x
+            sc.startY = y
+            sc.baseOffsetX = sc.offsetX or 0
+            sc.baseOffsetY = sc.offsetY or 0
+            sc.dragging = false
+            sc.captured = false
+            -- 透传按下给命中的子组件,便于组件进入按下态;若后续进入拖拽会被取消
+            local tx = x - (sc.offsetX or 0)
+            local ty = y - (sc.offsetY or 0)
+            sc.downTarget = nil
+            for i = #self.children, 1, -1 do
+                local c = self.children[i]
+                if c and c.enabled ~= false and c.handleEvent and 
+                   hit_test(tx, ty, { x = c.x, y = c.y, w = c.w, h = c.h }) then
+                    sc.downTarget = c
+                    c:handleEvent("TOUCH_DOWN", tx, ty)
+                    break
+                end
+            end
+            return true
+        elseif evt == "MOVE_Y" or evt == "MOVE_X" then
+            local dx = (x - (sc.startX or x))
+            local dy = (y - (sc.startY or y))
+            if not sc.dragging then
+                local m = math.max(math.abs(dx), math.abs(dy))
+                if m >= (sc.threshold or 10) then
+                    sc.dragging = true
+                    sc.captured = true
+                    -- 进入拖拽,取消先前按下态
+                    if sc.downTarget and sc.downTarget.handleEvent then
+                        local tx = x - (sc.offsetX or 0)
+                        local ty = y - (sc.offsetY or 0)
+                        sc.downTarget:handleEvent("LONG_PRESS", tx, ty)
+                    end
+                    sc.downTarget = nil
+                else
+                    -- v1.6.1修复:未达拖拽阈值时(观望期),转发MOVE给downTarget让其实时更新状态
+                    if sc.downTarget and sc.downTarget.handleEvent then
+                        local tx = x - (sc.offsetX or 0)
+                        local ty = y - (sc.offsetY or 0)
+                        sc.downTarget:handleEvent(evt, tx, ty)
+                    end
+                end
+            end
+            if sc.dragging then
+                local nx = sc.baseOffsetX + dx
+                local ny = sc.baseOffsetY + dy
+                if sc.direction == "vertical" then
+                    if ny < minY then ny = minY end
+                    if ny > maxY then ny = maxY end
+                    sc.offsetY = ny
+                elseif sc.direction == "horizontal" then
+                    if nx < minX then nx = minX end
+                    if nx > maxX then nx = maxX end
+                    sc.offsetX = nx
+                else -- both
+                    if nx < minX then nx = minX end
+                    if nx > maxX then nx = maxX end
+                    if ny < minY then ny = minY end
+                    if ny > maxY then ny = maxY end
+                    sc.offsetX, sc.offsetY = nx, ny
+                end
+                return true
+            end
+            return true
+        elseif evt == "SINGLE_TAP" or evt == "LONG_PRESS" then
+            if sc.dragging then
+                -- 滑动期间禁用点击;若启用分页,则在抬手时做“就近吸附”(无论是否触发 SWIPE)
+                if sc.pagingEnabled and (sc.direction == "horizontal" or sc.direction == "both") then
+                    local pageW = sc.pageWidth or self.w
+                    local totalW = sc.contentWidth or self.w
+                    local pages = math.max(1, math.floor((totalW + pageW - 1) / pageW))
+                    local cur = math.floor((-(sc.offsetX or 0) + pageW / 2) / pageW)
+                    if cur < 0 then cur = 0 end
+                    if cur > pages - 1 then cur = pages - 1 end
+                    sc.offsetX = -cur * pageW
+                end
+                sc.dragging = false
+                sc.captured = false
+                sc.downTarget = nil
+                return true
+            end
+            -- 未拖拽:将事件分发给子组件(坐标转内容坐标)
+            local tx = x - (sc.offsetX or 0)
+            local ty = y - (sc.offsetY or 0)
+            for i = #self.children, 1, -1 do
+                local c = self.children[i]
+                if c and c.enabled ~= false and c.handleEvent and 
+                   hit_test(tx, ty, { x = c.x, y = c.y, w = c.w, h = c.h }) then
+                    if c:handleEvent(evt, tx, ty) then sc.downTarget = nil return true end
+                end
+            end
+            sc.downTarget = nil
+            return true
+        elseif evt == "SWIPE_LEFT" or evt == "SWIPE_RIGHT" or evt == "SWIPE_UP" or evt == "SWIPE_DOWN" then
+            -- 抬手后的滑动手势:结束拖拽,并在需要时做分页吸附(仅横向)
+            sc.dragging = false
+            sc.captured = false
+            if sc.pagingEnabled and (sc.direction == "horizontal" or sc.direction == "both") then
+                local pageW = sc.pageWidth or self.w
+                local totalW = sc.contentWidth or self.w
+                local pages = math.max(1, math.floor((totalW + pageW - 1) / pageW))
+                -- 当前页(offsetX 为负值向右移动内容)
+                local cur = math.floor((-(sc.offsetX or 0) + pageW / 2) / pageW)
+                if evt == "SWIPE_LEFT" then cur = cur + 1 elseif evt == "SWIPE_RIGHT" then cur = cur - 1 end
+                if cur < 0 then cur = 0 end
+                if cur > pages - 1 then cur = pages - 1 end
+                sc.offsetX = -cur * pageW
+            end
+            return true
+        else
+            -- 其他事件(如 MOVE_X/SWIPE_*)在 0.1 版忽略或按需拦截
+            return true
+        end
+    end
+    -- 非滚动窗口:正常分发
+    for i = #self.children, 1, -1 do
+        local c = self.children[i]
+        -- v1.6.1修复:添加_capture检查,让已捕获的组件(如按下的Button)能收到移出范围的MOVE事件
+        if c and c.enabled ~= false and c.handleEvent and 
+           (c._capture == true or hit_test(x, y, { x = c.x, y = c.y, w = c.w, h = c.h })) then
+            if c:handleEvent(evt, x, y) then return true end
+        end
+    end
+    return true -- 拦截窗口区域内未被子组件消费的事件
+end
+
+-- 启用简易滚动(0.1)
+function Window:enableScroll(opts)
+    opts = opts or {}
+    self._scroll = {
+        enabled = true,
+        direction = opts.direction or "vertical",
+        contentWidth = tonumber(opts.contentWidth or self.w) or self.w,
+        contentHeight = tonumber(opts.contentHeight or self.h) or self.h,
+        offsetX = 0,
+        offsetY = 0,
+        threshold = tonumber(opts.threshold or 10) or 10,
+        pagingEnabled = not not opts.pagingEnabled,
+        pageWidth = tonumber(opts.pageWidth or self.w) or self.w,
+        dragging = false,
+        captured = false,
+    }
+    return self
+end
+
+function Window:setContentSize(w, h)
+    if not self._scroll then return end
+    if w then self._scroll.contentWidth = tonumber(w) or self._scroll.contentWidth end
+    if h then self._scroll.contentHeight = tonumber(h) or self._scroll.contentHeight end
+end
+
+-- 启用子页面管理
+function Window:enableSubpageManager(opts)
+    opts = opts or {}
+    if not self._managed then
+        self._managed = { 
+            pages = {}, 
+            backEventName = opts.backEventName or "NAV.BACK", 
+            onBack = opts.onBack 
+        }
+        sys.subscribe(self._managed.backEventName, function()
+            if self._managed.onBack then pcall(self._managed.onBack) end
+            local anyVisible = false
+            for _, pg in pairs(self._managed.pages) do
+                if pg and pg.visible ~= false then anyVisible = true break end
+            end
+            if not anyVisible then
+                self.visible = true
+                self.enabled = true
+            end
+        end)
+    end
+    return self
+end
+
+-- 配置子页面工厂
+function Window:configureSubpages(factories)
+    if not self._managed then self:enableSubpageManager() end
+    self._managed.factories = self._managed.factories or {}
+    for k, v in pairs(factories or {}) do
+        self._managed.factories[k] = v
+    end
+    return self
+end
+
+-- 显示子页面
+function Window:showSubpage(name, factory)
+    if not self._managed then error("enableSubpageManager must be called before showSubpage") end
+    for key, pg in pairs(self._managed.pages) do
+        if pg and pg.visible ~= false then
+            pg.visible = false
+            pg.enabled = false
+        end
+    end
+    if not self._managed.pages[name] then
+        local f = factory
+        if not f and self._managed.factories then f = self._managed.factories[name] end
+        if not f then error("no factory for subpage '" .. tostring(name) .. "'") end
+        self._managed.pages[name] = f()
+        self._managed.pages[name]._parentWindow = self
+        core.add(self._managed.pages[name])
+    end
+    self.visible = false
+    self._managed.pages[name].visible = true
+    self._managed.pages[name].enabled = true
+end
+
+-- 返回上级页面
+function Window:back()
+    if self._parentWindow then
+        self.visible = false
+        self.enabled = false
+        local parent = self._parentWindow
+        local anyVisible = false
+        if parent._managed and parent._managed.pages then
+            for _, pg in pairs(parent._managed.pages) do
+                if pg and pg.visible ~= false then anyVisible = true break end
+            end
+        end
+        if not anyVisible then
+            parent.visible = true
+            parent.enabled = true
+        end
+    end
+end
+
+-- 关闭子页面
+function Window:closeSubpage(name, opts)
+    if not self._managed or not self._managed.pages then return false end
+    opts = opts or {}
+    local pg = self._managed.pages[name]
+    if not pg then return false end
+    pg.visible = false
+    pg.enabled = false
+    if opts.destroy == true then
+        core.remove(pg)
+        self._managed.pages[name] = nil
+        collectgarbage("collect")
+    end
+    local anyVisible = false
+    for _, p in pairs(self._managed.pages) do
+        if p and p.visible ~= false then anyVisible = true break end
+    end
+    if not anyVisible then
+        self.visible = true
+        self.enabled = true
+    end
+    return true
+end
+
+-- ProgressBar组件 - 进度条,支持百分比显示和主题适配
+local ProgressBar = {}
+ProgressBar.__index = ProgressBar
+
+function ProgressBar:new(opts)
+    opts = opts or {}
+    local o = setmetatable({}, self)
+    o.x = opts.x or 0
+    o.y = opts.y or 0
+    o.w = opts.width or opts.w or 200
+    o.h = opts.height or opts.h or 24
+    o.progress = math.max(0, math.min(100, opts.progress or 0))
+    o.showPercentage = opts.showPercentage ~= false
+    o.text = opts.text
+    o.textSize = opts.textSize or opts.size
+    local dark = (current_theme == "dark")
+    o.backgroundColor = opts.backgroundColor or (dark and COLOR_GRAY or 0xC618)
+    o.progressColor = opts.progressColor or (dark and COLOR_BLUE or 0x001F)
+    o.borderColor = opts.borderColor or (dark and COLOR_WHITE or 0x8410)
+    o.textColor = opts.textColor or (dark and COLOR_WHITE or COLOR_BLACK)
+    o.visible = opts.visible ~= false
+    o.enabled = opts.enabled ~= false
+    return o
+end
+
+function ProgressBar:setProgress(value)
+    self.progress = math.max(0, math.min(100, value))
+end
+
+function ProgressBar:getProgress()
+    return self.progress
+end
+
+function ProgressBar:setText(text)
+    self.text = text
+end
+
+function ProgressBar:draw()
+    if not self.visible then return end
+    
+    -- 绘制背景(可选:只绘制内区,避免与边框重复像素)
+    local padding = 1
+    fill_rect(self.x + padding, self.y + padding, self.x + self.w - padding, self.y + self.h - padding, self.backgroundColor)
+    
+    -- 绘制边框
+    stroke_rect(self.x, self.y, self.x + self.w, self.y + self.h, self.borderColor)
+    
+    -- 计算进度条填充
+    padding = 1
+    local inner_left = self.x + padding
+    local inner_top = self.y + padding
+    local inner_right = self.x + self.w - padding  -- 包含式
+    local inner_bottom = self.y + self.h - padding -- 包含式
+    local inner_width = inner_right - inner_left
+    local fill_width = math.floor(inner_width * (self.progress / 100))
+    if fill_width > 0 then
+        local x1 = inner_left
+        local x2 = inner_left + fill_width
+        fill_rect(x1, inner_top, x2, inner_bottom, self.progressColor)
+    end
+    
+    -- 绘制文本
+    if self.showPercentage or self.text then
+        local display_text = self.text or (self.progress .. "%")
+        draw_text_in_rect_centered(self.x, self.y, self.w, self.h, display_text, {
+            color = self.textColor,
+            style = { size = self.textSize },
+            padding = 2
+        })
+    end
+end
+
+function ProgressBar:handleEvent(evt, x, y)
+    return false -- 进度条默认不处理触摸事件
+end
+
+
+-- ================================
+-- 主模块导出
+-- ================================
+
+local M = {}
+
+-- 核心API导出
+M.init = core.init
+M.add = core.add
+M.remove = core.remove
+M.clear = core.clear
+M.render = core.render
+M.handleTouchEvent = core.handleTouchEvent
+M.debug = core.debug
+M.getTheme = core.getTheme
+
+-- 硬件支持
+M.hw = hw
+
+-- 事件系统
+M.event = event
+
+-- 组件构造函数
+M.Button = function(opts) return Button:new(opts) end
+M.CheckBox = function(opts) return CheckBox:new(opts) end
+M.Label = function(opts) return Label:new(opts) end
+M.Picture = function(opts) return Picture:new(opts) end
+M.MessageBox = function(opts) return MessageBox:new(opts) end
+M.Window = function(opts) return Window:new(opts) end
+M.ProgressBar = function(opts) return ProgressBar:new(opts) end
+
+-- 字体 API 导出
+M.font = {
+    set = function(style) return font_set(style) end,
+    measure = function(text, style) return font_measure(text, style) end,
+    lineHeight = function(style) return font_line_height(style) end
+}
+
+-- 对外导出常用颜色常量,便于在业务侧直接使用
+M.COLOR_WHITE = COLOR_WHITE
+M.COLOR_BLACK = COLOR_BLACK
+M.COLOR_GRAY  = COLOR_GRAY
+M.COLOR_BLUE  = COLOR_BLUE
+M.COLOR_RED   = COLOR_RED
+M.COLOR_GREEN = COLOR_GREEN
+M.COLOR_YELLOW = COLOR_YELLOW
+M.COLOR_CYAN = COLOR_CYAN
+M.COLOR_MAGENTA = COLOR_MAGENTA
+M.COLOR_ORANGE = COLOR_ORANGE
+M.COLOR_PINK = COLOR_PINK
+-- Windows 11 Light模式颜色
+M.COLOR_WIN11_LIGHT_DIALOG_BG = COLOR_WIN11_LIGHT_DIALOG_BG
+M.COLOR_WIN11_LIGHT_BUTTON_BG = COLOR_WIN11_LIGHT_BUTTON_BG
+M.COLOR_WIN11_LIGHT_BUTTON_BORDER = COLOR_WIN11_LIGHT_BUTTON_BORDER
+-- Windows 11 Dark模式颜色
+M.COLOR_WIN11_DARK_DIALOG_BG = COLOR_WIN11_DARK_DIALOG_BG
+M.COLOR_WIN11_DARK_BUTTON_BG = COLOR_WIN11_DARK_BUTTON_BG
+M.COLOR_WIN11_DARK_BUTTON_BORDER = COLOR_WIN11_DARK_BUTTON_BORDER
+
+return M

+ 1 - 1
script/libs/exfotawifi.lua

@@ -71,7 +71,7 @@ end
 
 -- 下载升级文件,支持断点续传
 local function download_file(url)
-    local file_path = "/ram/fotawifi.bin"
+    local file_path = "/luadb/fotawifi.bin"
     local downloaded_size = 0
 
     -- 检查文件是否存在,获取已下载的大小

+ 212 - 0
script/libs/exlcd.lua

@@ -0,0 +1,212 @@
+-- exlcd.lua
+--[[
+@module  exlcd
+@summary LCD显示拓展库
+@version 1.0
+@date    2025.09.17
+@author  江访
+@usage
+核心业务逻辑为:
+1、初始化LCD显示屏,支持多种显示芯片
+2、管理屏幕背光亮度及开关状态
+3、管理屏幕休眠和唤醒状态
+4、提供屏幕状态管理功能
+
+本文件的对外接口有4个:
+1、exlcd.init(args)   -- LCD初始化函数
+2、exlcd.bkl(level)   -- 设置背光亮度接口,level 亮度级别(0-100)
+3、exlcd.sleep()      -- 屏幕休眠
+4、exlcd.wakeup()     -- 屏幕唤醒
+]]
+
+local exlcd = {}
+
+-- 屏幕状态管理表
+local screen_state = {
+    is_sleeping = false,   -- 是否休眠中标识
+    last_brightness = 100,  -- 默认亮度100%
+    backlight_on = true,   -- 背光默认开启
+    lcd_config = nil       -- 存储LCD配置
+}
+
+-- LCD初始化函数
+-- @param args LCD参数配置表
+-- @return 初始化成功状态
+function exlcd.init(args)
+
+    if type(args) ~= "table" then
+        log.error("exlcd", "参数必须为表")
+        return false
+    end
+
+    -- 检查必要参数
+    if not args.LCD_MODEL then
+        log.error("exlcd", "缺少必要参数: LCD_MODEL")
+        return false
+    end
+
+    -- LCD型号映射表
+    local lcd_models = {
+        AirLCD_1000 = "st7796",
+        AirLCD_1001 = "st7796",
+        Air780EHM_LCD_1 = "st7796",
+        Air780EHM_LCD_2 = "st7796",
+        Air780EHM_LCD_3 = "st7796",
+        Air780EHM_LCD_4 = "st7796",
+        AirLCD_1020 = "nv3052c"
+    }
+
+    -- 确定LCD型号
+    local lcd_model = lcd_models[args.LCD_MODEL] or args.LCD_MODEL
+
+    -- 存储LCD配置供其他函数使用
+    screen_state.lcd_config = {
+        pin_pwr = args.pin_pwr,
+        pin_pwm = args.pin_pwm,
+        model = lcd_model
+    }
+
+    -- 设置电源引脚 (可选)
+    if args.pin_vcc then
+        gpio.setup(args.pin_vcc, 1, gpio.PULLUP)
+        gpio.set(args.pin_vcc, 1)
+    end
+
+    -- 设置背光电源引脚 (可选)
+    if args.pin_pwr then
+        gpio.setup(args.pin_pwr, 1, gpio.PULLUP)
+        gpio.set(args.pin_pwr, 1)  -- 默认开启背光
+    end
+
+    -- 设置PWM背光引脚 (可选)
+    if args.pin_pwm then
+        pwm.setup(args.pin_pwm, 1000, screen_state.last_brightness)
+        pwm.open(args.pin_pwm, 1000, screen_state.last_brightness)
+    end
+
+    -- 屏幕初始化 (spi_dev和init_in_service为可选参数)
+    local lcd_init = lcd.init(
+        lcd_model,
+        args,
+        args.spi_dev and args.spi_dev or nil,
+        args.init_in_service and args.init_in_service or nil
+    )
+    log.info("exlcd", "LCD初始化", lcd_init)
+    return lcd_init
+end
+
+-- 设置背光亮度接口
+-- 使用背光PWM模式控制亮度
+-- @param level 亮度级别(0-100)
+
+function exlcd.bkl(level)
+    -- 检查屏幕状态和PWM配置
+    if screen_state.is_sleeping then
+        log.warn("exlcd", "屏幕处于休眠状态,无法调节背光")
+        return false
+    end
+    if not screen_state.lcd_config.pin_pwm then
+        log.error("exlcd", "PWM配置不存在,无法调节背光")
+        return false
+    end
+
+    -- 确保GPIO已关闭 
+    gpio.close(screen_state.lcd_config.pin_pwr)
+
+    -- 设置并开启PWM
+    pwm.stop(screen_state.lcd_config.pin_pwm)
+    pwm.close(screen_state.lcd_config.pin_pwm)
+    pwm.setup(screen_state.lcd_config.pin_pwm, 1000, 100)
+    pwm.open(screen_state.lcd_config.pin_pwm, 1000, level)
+    screen_state.last_brightness = level
+    screen_state.backlight_on = (level > 0)
+    log.info("exlcd", "背光设置为", level, "%")
+    return true
+end
+
+
+
+-- function exlcd.bkl(level)
+--     -- 检查屏幕状态
+--     if screen_state.is_sleeping then
+--         log.warn("exlcd", "屏幕处于休眠状态,无法调节背光")
+--         return
+--     end
+
+--     -- 确保亮度在有效范围内
+--     -- level = level or screen_state.last_brightness
+--     -- level = math.max(0, math.min(100, level or 0))
+
+--     if screen_state.lcd_config.pin_pwr then
+--          gpio.close(screen_state.lcd_config.pin_pwr)
+--     end
+
+--     -- 设置PWM背光 (如果配置了PWM引脚)
+--     if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
+--         --pwm.setup(screen_state.lcd_config.pin_pwm, 1000, level)
+--         pwm.open(screen_state.lcd_config.pin_pwm, 1000, level)
+--     -- 如果没有PWM但配置了电源引脚,则通过开关控制
+--     elseif screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
+--         gpio.set(screen_state.lcd_config.pin_pwr, level > 0 and 1 or 0)
+--     end
+
+--     screen_state.last_brightness = level
+--     screen_state.backlight_on = (level > 0)
+--     log.info("exlcd", "背光设置为", level, "%")
+-- end
+
+-- 屏幕休眠
+function exlcd.sleep()
+    if not screen_state.is_sleeping then
+        -- 关闭PWM背光 (如果配置了)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
+            pwm.close(screen_state.lcd_config.pin_pwm)
+        end
+
+        -- 关闭背光电源 (如果配置了)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
+            gpio.setup(screen_state.lcd_config.pin_pwr, 1, gpio.PULLUP)
+            gpio.set(screen_state.lcd_config.pin_pwr, 0)
+        end
+
+        -- 执行LCD睡眠
+        lcd.sleep()
+        screen_state.is_sleeping = true
+        log.info("exlcd", "LCD进入休眠状态")
+    end
+end
+
+-- 屏幕唤醒
+function exlcd.wakeup()
+    if screen_state.is_sleeping then
+        -- 开启背光电源 (如果配置了)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwr then
+            gpio.set(screen_state.lcd_config.pin_pwr, 1)
+        end
+
+        -- 唤醒LCD
+        lcd.wakeup()
+        sys.wait(100)  -- 等待100ms稳定
+
+        -- 恢复背光设置 (如果配置了PWM引脚)
+        if screen_state.lcd_config and screen_state.lcd_config.pin_pwm then
+            pwm.setup(screen_state.lcd_config.pin_pwm, 1000, screen_state.last_brightness)
+            pwm.open(screen_state.lcd_config.pin_pwm, 1000, screen_state.last_brightness)
+        end
+
+        screen_state.is_sleeping = false
+        log.info("exlcd", "LCD唤醒")
+    end
+end
+
+
+function exlcd.get_brightness()
+    return screen_state.last_brightness
+end
+
+-- 获取当前休眠状态
+function exlcd.is_sleeping()
+    return screen_state.is_sleeping
+end
+
+return exlcd

+ 20 - 0
script/libs/exnetif.lua

@@ -22,6 +22,7 @@ local eth_ping_ip
 local local_network_mode
 local need_ping = true
 local single_network_mode = false
+local auto_socket_switch = true
 
 local ping_time = 10000
 -- 连接状态
@@ -93,6 +94,9 @@ local function apply_priority()
                 log.info("设置网卡", type_to_string(net_type))
                 states_cbfnc(type_to_string(net_type), net_type) -- 默认网卡改变的回调函数
                 socket.dft(net_type)
+                if auto_socket_switch then
+                    socket.close_all(current_active)
+                end
                 current_active = net_type
             end
             break
@@ -213,6 +217,10 @@ local function setup_eth(config)
     if config.need_ping~=nil then
         need_ping = config.need_ping
     end
+    if config.auto_socket_switch ~=nil then
+        auto_socket_switch = config.auto_socket_switch
+        -- log.info("设置自动关闭非当前网卡socket连接", auto_socket_switch)
+    end
     eth_ping_ip = config.ping_ip
     if type(config.ping_time) == "number" then
         ping_time = config.ping_time
@@ -286,6 +294,10 @@ local function setup_eth_user1(config)
     if config.need_ping~=nil then
         need_ping = config.need_ping
     end
+    if config.auto_socket_switch ~=nil then
+        auto_socket_switch = config.auto_socket_switch
+        -- log.info("设置自动关闭非当前网卡socket连接", auto_socket_switch)
+    end
     eth_ping_ip = config.ping_ip
     if type(config.ping_time) == "number" then
         ping_time = config.ping_time
@@ -349,6 +361,10 @@ local function set_wifi_info(config)
     if config.need_ping~=nil then
         need_ping = config.need_ping
     end
+    if config.auto_socket_switch ~=nil then
+        auto_socket_switch = config.auto_socket_switch
+        -- log.info("设置自动关闭非当前网卡socket连接", auto_socket_switch)
+    end
     wifi_ping_ip = config.ping_ip
     if type(config.ping_time) == "number" then
         ping_time = config.ping_time
@@ -560,6 +576,10 @@ function exnetif.set_priority_order(networkConfigs)
             -- 开启4G
             table.insert(new_priority, socket.LWIP_GP)
             available[socket.LWIP_GP] = connection_states.CONNECTING
+            if config.auto_socket_switch ~=nil then
+                auto_socket_switch = config.auto_socket_switch
+                -- log.info("设置自动关闭非当前网卡socket连接", auto_socket_switch)
+            end
         end
     end
 

+ 308 - 0
script/libs/extp.lua

@@ -0,0 +1,308 @@
+-- extp.lua - 触摸系统模块
+--[[
+@module  extp
+@summary 触摸系统拓展库
+@version 1.0
+@date    2025.09.17
+@author  江访
+@usage
+核心业务逻辑为:
+1、初始化触摸设备,支持多种触摸芯片
+2、处理原始触摸数据并解析为各种手势事件
+3、通过统一消息接口发布触摸事件
+4、提供消息发布控制功能
+5、提供滑动和长按阈值配置功能
+
+支持的触摸事件类型包括:
+1、RAW_DATA - 原始触摸数据
+2、TOUCH_DOWN - 按下事件
+3、MOVE_X - 水平移动
+4、MOVE_Y - 垂直移动
+5、SWIPE_LEFT - 向左滑动
+6、SWIPE_RIGHT - 向右滑动
+7、SWIPE_UP - 向上滑动
+8、SWIPE_DOWN - 向下滑动
+9、SINGLE_TAP - 单击
+10、LONG_PRESS - 长按
+
+触摸判断逻辑:
+1、按下至抬手只能触发事件5-10中的一个事件
+2、移动像素超过滑动判定阈值,
+   如果触发的是水平移动MOVE_X,抬手只会返回SWIPE_LEFT和SWIPE_RIGHT事件,
+   如果触发的是垂直移动MOVE_Y,抬手只会返回SWIPE_UP和SWIPE_DOWN事件,
+3、按下至抬手像素移动超过滑动判定阈值,
+   如果时间小于500ms判定为单击,按下至抬手的时间大于500ms判定为长按
+
+
+本文件的对外接口有5个:
+1、extp.init(args)                      -- 触摸设备初始化函数
+2、extp.setPublishEnabled(msg_type, enabled) -- 设置消息发布状态
+3、extp.getPublishEnabled(msg_type)          -- 获取消息发布状态
+4、extp.setSlideThreshold(threshold)         -- 设置滑动判定阈值
+5、extp.setLongPressThreshold(threshold)     -- 设置长按判定阈值
+
+所有触摸事件均通过sys.publish("baseTouchEvent", event_type, ...)发布
+]]
+local extp = {}
+
+-- 触摸状态变量
+local state = "IDLE"            -- 当前状态:IDLE(空闲), DOWN(按下), MOVE(移动)
+local touch_down_x = 0          -- 按下时的X坐标
+local touch_down_y = 0          -- 按下时的Y坐标
+local touch_down_time = 0       -- 按下时的时间戳
+local slide_threshold = 45      -- 滑动判定阈值(像素)
+local long_press_threshold = 500 -- 长按判定阈值(毫秒)
+local slide_direction = nil     -- 滑动方向(用于MOVE状态)
+
+-- 消息发布控制表,默认全部打开
+local publish_control = {
+    RAW_DATA = true,            -- 原始触摸数据
+    TOUCH_DOWN = true,          -- 按下事件
+    MOVE_X = true,              -- 水平移动
+    MOVE_Y = true,              -- 垂直移动
+    SWIPE_LEFT = true,          -- 向左滑动
+    SWIPE_RIGHT = true,         -- 向右滑动
+    SWIPE_UP = true,            -- 向上滑动
+    SWIPE_DOWN = true,          -- 向下滑动
+    SINGLE_TAP = true,          -- 单击
+    LONG_PRESS = true           -- 长按
+}
+
+-- 定义支持的触摸芯片配置
+local tp_configs = {
+    cst820 = { i2c_speed = i2c.FAST, tp_model = "cst820" },
+    cst9220 = { i2c_speed = i2c.SLOW, tp_model = "cst9220" },
+    gt9157 = { i2c_speed = i2c.SLOW, tp_model = "gt9157" },
+    AirLCD_1001 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
+    AirLCD_1020 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
+    Air780EHM_LCD_3 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
+    Air780EHM_LCD_4 = { i2c_speed = i2c.SLOW, tp_model = "gt911" },
+    jd9261t = { i2c_speed = i2c.SLOW, tp_model = "jd9261t" },
+    jd9261t_inited = { i2c_speed = i2c.SLOW, tp_model = "jd9261t_inited" },
+}
+
+-- 设置消息发布状态
+-- @param msg_type 消息类型 ("RAW_DATA", "TOUCH_DOWN", "MOVE_X", "MOVE_Y", "SWIPE_LEFT", "SWIPE_RIGHT", "SWIPE_UP", "SWIPE_DOWN", "SINGLE_TAP", "LONG_PRESS", 或 "all")
+-- @param enabled 是否启用 (true/false)
+-- @return boolean 操作是否成功
+function extp.setPublishEnabled(msg_type, enabled)
+    if msg_type == "all" then
+        for k, _ in pairs(publish_control) do
+            publish_control[k] = enabled
+        end
+        log.info("extp", "所有消息发布", enabled and "启用" or "禁用")
+        return true
+    elseif publish_control[msg_type] ~= nil then
+        publish_control[msg_type] = enabled
+        log.info("extp", msg_type, "消息发布", enabled and "启用" or "禁用")
+        return true
+    else
+        log.error("extp", "未知的消息类型:", msg_type)
+        return false
+    end
+end
+
+-- 获取消息发布状态
+-- @param msg_type 消息类型 ("RAW_DATA", "TOUCH_DOWN", "MOVE_X", "MOVE_Y", "SWIPE_LEFT", "SWIPE_RIGHT", "SWIPE_UP", "SWIPE_DOWN", "SINGLE_TAP", "LONG_PRESS")
+-- @return boolean|table 发布状态 (true/false) 或所有状态表(当msg_type为nil时)
+function extp.getPublishEnabled(msg_type)
+    if msg_type == nil then
+        return publish_control
+    elseif publish_control[msg_type] ~= nil then
+        return publish_control[msg_type]
+    else
+        log.error("extp", "未知的消息类型:", msg_type)
+        return false
+    end
+end
+
+-- 设置滑动判定阈值
+-- @param threshold number 滑动判定阈值(像素)
+-- @return boolean 操作是否成功
+function extp.setSlideThreshold(threshold)
+    if type(threshold) == "number" and threshold > 0 then
+        slide_threshold = threshold
+        log.info("extp", "滑动判定阈值设置为:", threshold)
+        return true
+    else
+        log.error("extp", "无效的滑动阈值:", threshold)
+        return false
+    end
+end
+
+-- 设置长按判定阈值
+-- @param threshold number 长按判定阈值(毫秒)
+-- @return boolean 操作是否成功
+function extp.setLongPressThreshold(threshold)
+    if type(threshold) == "number" and threshold > 0 then
+        long_press_threshold = threshold
+        log.info("extp", "长按判定阈值设置为:", threshold)
+        return true
+    else
+        log.error("extp", "无效的长按阈值:", threshold)
+        return false
+    end
+end
+
+-- 触摸回调函数
+-- 参数: tp_device-触摸设备对象, tp_data-触摸数据
+local function tp_callback(tp_device, tp_data)
+    -- 发布原始数据(如果启用)
+    if publish_control.RAW_DATA then
+        sys.publish("TP", tp_device, tp_data)  --当前消息
+        -- sys.publish("baseTouchEvent", "RAW_DATA", tp_device, tp_data)
+    end
+    -- 兼容多种数据结构:数组[1]或直接单点表;字段名兼容 x/x_coordinate, y/y_coordinate
+    local p = nil
+    if type(tp_data) == "table" then
+        p = tp_data[1] or tp_data
+    end
+    if type(p) ~= "table" then return end
+
+    local event_type = p.event or p.type or p.evt
+    local x = p.x or p.x_coordinate or 0
+    local y = p.y or p.y_coordinate or 0
+    local ms_h,times = mcu.ticks2(1)
+    local timestamp = times or p.ts or 0
+    if not event_type then return end
+    
+
+    if event_type == 2 then  -- 抬手事件
+        if state == "DOWN" or state == "MOVE" then
+            local moveX = x - touch_down_x
+            local moveY = y - touch_down_y
+
+            if moveX < -slide_threshold then
+                if publish_control.SWIPE_LEFT then
+                    sys.publish("baseTouchEvent", "SWIPE_LEFT", moveX, 0)
+                end
+            elseif moveX > slide_threshold then
+                if publish_control.SWIPE_RIGHT then
+                    sys.publish("baseTouchEvent", "SWIPE_RIGHT", moveX, 0)
+                end
+            elseif moveY < -slide_threshold then
+                if publish_control.SWIPE_UP then
+                    sys.publish("baseTouchEvent", "SWIPE_UP", 0, moveY)
+                end
+            elseif moveY > slide_threshold then
+                if publish_control.SWIPE_DOWN then
+                    sys.publish("baseTouchEvent", "SWIPE_DOWN", 0, moveY)
+                end
+            else
+                -- 计算按下时间
+                local press_time = timestamp - touch_down_time
+
+                -- 判断是单击还是长按
+                if press_time < long_press_threshold then
+                    if publish_control.SINGLE_TAP then
+                        sys.publish("baseTouchEvent", "SINGLE_TAP", touch_down_x, touch_down_y)
+                    end
+                else
+                    if publish_control.LONG_PRESS then
+                        sys.publish("baseTouchEvent", "LONG_PRESS", touch_down_x, touch_down_y)
+                    end
+                end
+            end
+            state = "IDLE"
+        end
+    elseif event_type == 1 or event_type == 3 then  -- 按下或移动事件
+        if state == "IDLE" and event_type == 1 then
+            -- 从空闲状态接收到按下事件
+            state = "DOWN"
+            touch_down_x = x
+            touch_down_y = y
+            touch_down_time = timestamp
+            slide_direction = nil
+
+            -- 发布按下事件
+            if publish_control.TOUCH_DOWN then
+                sys.publish("baseTouchEvent", "TOUCH_DOWN", x, y)
+            end
+        elseif state == "DOWN" and event_type == 3 then
+            -- 在按下状态下接收到移动事件
+            if math.abs(x - touch_down_x) >= slide_threshold or math.abs(y - touch_down_y) >= slide_threshold then
+                state = "MOVE"
+                -- 确定滑动方向
+                if math.abs(x - touch_down_x) > math.abs(y - touch_down_y) then
+                    -- 水平滑动
+                    if x - touch_down_x < 0 then
+                        slide_direction = "LEFT"
+                    else
+                        slide_direction = "RIGHT"
+                    end
+                else
+                    -- 垂直滑动
+                    if y - touch_down_y < 0 then
+                        slide_direction = "UP"
+                    else
+                        slide_direction = "DOWN"
+                    end
+                end
+            end
+        elseif state == "MOVE" and event_type == 3 then
+            -- 在移动状态下接收到移动事件
+            -- 根据滑动方向发布相应的移动事件
+            if slide_direction == "LEFT" or slide_direction == "RIGHT" then
+                -- 水平滑动,发布MOVE_X事件
+                if publish_control.MOVE_X then
+                    sys.publish("baseTouchEvent", "MOVE_X", x - touch_down_x, 0)
+                end
+            else
+                -- 垂直滑动,发布MOVE_Y事件
+                if publish_control.MOVE_Y then
+                    sys.publish("baseTouchEvent", "MOVE_Y", 0, y - touch_down_y)
+                end
+            end
+        end
+    end
+end
+
+-- 初始化触摸功能
+-- @param args table 初始化参数表,包含以下字段:
+--   TP_MODEL: string 触摸芯片型号 ("cst820", "cst9220", "gt9157", "jd9261t", "AirLCD_1001", "Air780EHM_LCD_3", "Air780EHM_LCD_4")
+--   i2c_id: number I2C总线ID
+--   pin_rst: number 复位引脚
+--   pin_int: number 中断引脚
+-- @return boolean 初始化是否成功
+function extp.init(args)
+    if type(args) ~= "table" then
+        log.error("extp", "参数必须为表")
+        return false
+    end
+
+    -- 检查必要参数
+    if not args.TP_MODEL then
+        log.error("extp", "缺少必要参数: TP_MODEL")
+        return false
+    end
+
+    local TP_MODEL, tp_i2c_id, tp_pin_rst, tp_pin_int = args.TP_MODEL, args.i2c_id, args.pin_rst, args.pin_int
+
+    -- 检查是否支持该型号
+    local config = tp_configs[TP_MODEL]
+    if not config then
+        log.error("extp", "不支持的触摸型号:", TP_MODEL)
+        return false
+    end
+
+    -- 统一初始化流程
+    if type(tp_i2c_id) ~= "userdata" and  config.i2c_speed ~= nil then
+        i2c.setup(tp_i2c_id, config.i2c_speed)
+    end
+
+    local tp_device = tp.init(config.tp_model, {port=tp_i2c_id, pin_rst=tp_pin_rst, pin_int=tp_pin_int,w =  1000,h =  800}, tp_callback)
+    log.info("tp_device",tp_device)
+    if tp_device ~= nil then return true end
+
+    -- 若硬件触摸初始化失败,尝试PC触摸回退
+    log.warn("extp", "触摸初始化失败,尝试PC触摸回退")
+    local ok_pc, dev_pc = pcall(tp.init, "pc", { port = 0 }, tp_callback)
+    if ok_pc and dev_pc then
+        log.info("extp", "PC触摸回退成功")
+        return true
+    end
+    log.error("extp", "PC触摸回退失败")
+    return false
+end
+
+return extp

+ 26 - 0
script/libs/screen_data_table.lua

@@ -0,0 +1,26 @@
+-- screen_data_table.lua )
+-- 此文件只包含屏幕相关配置数据
+
+local screen_data_table = {
+    lcdargs = {
+        LCD_MODEL = "AirLCD_1001",
+        pin_vcc = 24,
+        pin_rst = 36,
+        pin_pwr = 25,
+        pin_pwm = 2,
+        port = lcd.HWID_0,
+        direction = 0,
+        w = 320,
+        h = 480,
+        xoffset = 0,
+        yoffset = 0,
+    },
+    touch = {
+        TP_MODEL = "Air780EHM_LCD_4", -- 触摸芯片型号
+        i2c_id = 1,               -- I2C总线ID
+        pin_rst = 255,              -- 触摸芯片复位引脚(非必须)
+        pin_int = 22              -- 触摸芯片中断引脚
+    },
+}
+
+return screen_data_table