Jelajahi Sumber

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

# Conflicts:
#	module/Air8000/demo/airtalk/main.lua
#	module/Air8000/demo/airtalk/readme.md
13917187172 2 bulan lalu
induk
melakukan
42191c4641
100 mengubah file dengan 5541 tambahan dan 440 penghapusan
  1. 5 0
      bsp/pc/build_macos.sh
  2. 5 0
      bsp/pc/build_macos_gui.sh
  3. 15 5
      bsp/pc/ffmpeg_x86/ffmpeg.c
  4. 4 0
      bsp/pc/include/luat_conf_bsp.h
  5. 39 2
      bsp/pc/port/driver/luat_tp_pc.c
  6. 2 1
      bsp/pc/port/luat_cmds.c
  7. 2 2
      bsp/pc/port/luat_malloc_mini.c
  8. 4 0
      bsp/pc/port/luat_mobile_pc.c
  9. 32 31
      bsp/pc/port/network/luat_network_adapter_libuv.c
  10. 928 0
      bsp/pc/test/092.exftp/exftp.lua
  11. 178 0
      bsp/pc/test/092.exftp/main.lua
  12. 9 1
      bsp/pc/xmake.lua
  13. 2 1
      components/airlink/src/luat_airlink.c
  14. 3 3
      components/common/c_common.h
  15. 53 33
      components/fatfs/diskio_spitf.c
  16. 4 4
      components/i2c-tools/i2c_tools.c
  17. 1 1
      components/i2c-tools/i2c_utils.c
  18. 1 1
      components/i2c-tools/i2c_utils.h
  19. 1 0
      components/mobile/luat_mobile_common.c
  20. 1 1
      components/network/adapter/luat_lib_socket.c
  21. 1 1
      components/network/adapter/luat_network_adapter.c
  22. 14 14
      components/network/libemqtt/luat_lib_mqtt.c
  23. 47 5
      components/network/libemqtt/luat_mqtt_client.c
  24. 23 12
      components/network/libhttp/luat_http_client.c
  25. 12 7
      components/network/libhttp/luat_lib_http.c
  26. 41 9
      components/network/netdrv/binding/luat_lib_netdrv.c
  27. 15 2
      components/network/netdrv/include/luat_netdrv.h
  28. 18 0
      components/network/netdrv/include/luat_netdrv_drv.h
  29. 0 2
      components/network/netdrv/include/luat_netdrv_napt.h
  30. 21 0
      components/network/netdrv/include/luat_netdrv_openvpn.h
  31. 119 0
      components/network/netdrv/include/luat_netdrv_openvpn_client.h
  32. 10 8
      components/network/netdrv/src/luat_netdrv.c
  33. 6 6
      components/network/netdrv/src/luat_netdrv_ch390h.c
  34. 0 0
      components/network/netdrv/src/luat_netdrv_ch390h_api.c
  35. 0 0
      components/network/netdrv/src/luat_netdrv_ch390h_task.c
  36. 311 0
      components/network/netdrv/src/luat_netdrv_openvpn.c
  37. 867 0
      components/network/netdrv/src/luat_netdrv_openvpn_client.c
  38. 1 1
      components/network/websocket/luat_lib_websocket.c
  39. 9 3
      components/ui/sdl2/luat_sdl2.c
  40. 1 0
      components/ui/sdl2/luat_sdl2.h
  41. 3 0
      luat/include/luat_pm.h
  42. 23 0
      luat/modules/luat_lib_gpio.c
  43. 9 1
      luat/modules/luat_lib_pm.c
  44. 8 1
      luat/modules/luat_lib_rtos.c
  45. 14 0
      luat/modules/luat_lib_zbuff.c
  46. 1 1
      module/Air780E/demo/errDump/main.lua
  47. 1 1
      module/Air780E/demo/iconv/main.lua
  48. 1 1
      module/Air780E/demo/socket/EC618/main.lua
  49. 1 1
      module/Air780E/demo/socket/EC618_W5500/main.lua
  50. 1 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1000/exeasyui/hw_drv/hw_default_font_drv.lua
  51. 1 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1000/exeasyui/hw_drv/hw_gtfont_drv.lua
  52. 1 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1000/exeasyui/hw_drv/hw_hzfont_drv.lua
  53. 1 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1000/exeasyui/ui/ui_main.lua
  54. 1 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1010/exeasyui/hw_drv/hw_default_font_drv.lua
  55. 1 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1010/exeasyui/hw_drv/hw_gtfont_drv.lua
  56. 1 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1010/exeasyui/hw_drv/hw_hzfont_drv.lua
  57. 1 1
      module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1010/lcd/ui/home_page.lua
  58. 1 1
      module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/Air780EHM_Air780EGH/main.lua
  59. 2 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/Air780EHM_Air780EGH/readme.md
  60. 1 1
      module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/Air780EHV/main.lua
  61. 2 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/Air780EHV/readme.md
  62. 1 1
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota/fota2(使用libfota2扩展库)/iot_server/psm_power_fota.lua
  63. 1 1
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota/fota2(使用libfota2扩展库)/iot_server/update.lua
  64. 1 1
      module/Air780EHM_Air780EHV_Air780EGH/demo/fota/fota2(使用libfota2扩展库)/self_server/psm_power_fota.lua
  65. 0 75
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/combination/readme.md
  66. 5 4
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/lowpower.lua
  67. 3 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/main.lua
  68. 4 4
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/normal.lua
  69. 2 2
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/psm.lua
  70. 155 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/readme.md
  71. 1 1
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/tcp_client_main.lua
  72. 0 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/tcp_client_receiver.lua
  73. 0 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/tcp_client_sender.lua
  74. 165 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/vibration.lua
  75. 0 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/single/gnss.lua
  76. 1 1
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/single/main.lua
  77. 5 5
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/single/readme.md
  78. 179 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/libgnss/gnss.lua
  79. 61 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/libgnss/main.lua
  80. 67 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/libgnss/readme.md
  81. 7 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/readme.md
  82. 0 155
      module/Air780EHM_Air780EHV_Air780EGH/demo/u8g2/main.lua
  83. 50 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/ht1621/ht1621_drv.lua
  84. 61 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/ht1621/key_drv.lua
  85. 83 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/ht1621/main.lua
  86. 214 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/ht1621/readme.md
  87. 339 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/ht1621/ui_main.lua
  88. 75 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/hw_drv/hw_default_font_drv.lua
  89. 90 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/hw_drv/hw_gtfont_drv.lua
  90. 66 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/hw_drv/key_drv.lua
  91. 99 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/main.lua
  92. 248 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/readme.md
  93. 147 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/ui/component_page.lua
  94. 110 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/ui/default_font_page.lua
  95. 148 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/ui/gtfont_page.lua
  96. 111 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/ui/home_page.lua
  97. 160 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/ui/ui_main.lua
  98. 0 1
      module/Air780EPM/demo/WebSocket/websocket_sender.lua
  99. 1 2
      module/Air780EPM/demo/accessory_board/AirLCD_1000/exeasyui/hw_drv/hw_default_font_drv.lua
  100. 1 2
      module/Air780EPM/demo/accessory_board/AirLCD_1000/exeasyui/ui/ui_main.lua

+ 5 - 0
bsp/pc/build_macos.sh

@@ -0,0 +1,5 @@
+xmake clean -a
+export VM_64bit=1
+export LUAT_USE_GUI=n
+xmake f -p macosx -y
+xmake -w -y

+ 5 - 0
bsp/pc/build_macos_gui.sh

@@ -0,0 +1,5 @@
+xmake clean -a
+export VM_64bit=1
+export LUAT_USE_GUI=y
+xmake f -p macosx -y
+xmake -w -y

+ 15 - 5
bsp/pc/ffmpeg_x86/ffmpeg.c

@@ -1,4 +1,5 @@
-#include "ffmpeg.h"
+#include "ffmpeg.h"
+#include <libavutil/opt.h>
 
 #ifdef _WIN32
 // FFmpeg DLL句柄定义 (仅Windows)
@@ -202,7 +203,12 @@ int luat_ffmpeg_play_file(const char *path) {
 
     // 确保 channel_layout 已设置
     if (!codec_ctx->channel_layout) {
+#ifdef _WIN32
         codec_ctx->channel_layout = av_get_default_channel_layout(codec_ctx->channels);
+#else
+        if (codec_ctx->channels == 1) codec_ctx->channel_layout = AV_CH_LAYOUT_MONO;
+        else codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
+#endif
     }
 
     // 打开解码器
@@ -217,15 +223,19 @@ int luat_ffmpeg_play_file(const char *path) {
     enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;
     int out_sample_rate = 44100;
 
-    swr_ctx = swr_alloc_set_opts(NULL,
-                                  out_ch_layout, out_sample_fmt, out_sample_rate,
-                                  codec_ctx->channel_layout, codec_ctx->sample_fmt, codec_ctx->sample_rate,
-                                  0, NULL);
+    swr_ctx = swr_alloc();
     if (!swr_ctx) {
         avcodec_free_context(&codec_ctx);
         avformat_close_input(&fmt_ctx);
         return -1;
     }
+    
+    av_opt_set_int(swr_ctx, "in_channel_layout",    codec_ctx->channel_layout, 0);
+    av_opt_set_int(swr_ctx, "in_sample_rate",       codec_ctx->sample_rate, 0);
+    av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", codec_ctx->sample_fmt, 0);
+    av_opt_set_int(swr_ctx, "out_channel_layout",   out_ch_layout, 0);
+    av_opt_set_int(swr_ctx, "out_sample_rate",      out_sample_rate, 0);
+    av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", out_sample_fmt, 0);
 
     // 初始化重采样器
     if (swr_init(swr_ctx) < 0) {

+ 4 - 0
bsp/pc/include/luat_conf_bsp.h

@@ -249,4 +249,8 @@
 #define LUAT_UART_MAX_DEVICE_COUNT 128
 #define LUAT_USE_PSRAM 1
 
+#ifndef LUAT_USE_LWIP
+#undef LUAT_USE_MOBILE
+#endif
+
 #endif

+ 39 - 2
bsp/pc/port/driver/luat_tp_pc.c

@@ -3,6 +3,8 @@
 #include "luat_tp.h"
 #include "luat_mem.h"
 #include "luat_mcu.h"
+#include "luat_rtos.h"
+#include "luat_sdl2.h"
 
 #include "uv.h"
 
@@ -11,6 +13,8 @@
 #define LUAT_LOG_TAG "tp_pc"
 #include "luat_log.h"
 
+#define TP_PC_FLUSH_INTERVAL_MS 30
+
 // 前置声明
 static int luat_tp_pc_init(luat_tp_config_t* conf);
 static int luat_tp_pc_read(luat_tp_config_t* conf, uint8_t* data);
@@ -36,14 +40,42 @@ typedef struct tp_pc_state
 
     // 队列机制 - 新增
     uint8_t event_queue_size; // 事件队列大小
-    luat_tp_data_t event_queue[8]; // 事件队列,最多保存8个事件
+    luat_tp_data_t event_queue[64]; // 事件队列,最多保存64个事件
     uint8_t queue_head; // 队列头
     uint8_t queue_tail; // 队列尾
     uint8_t queue_enabled; // 是否启用队列机制
+    luat_rtos_timer_t flush_timer; // SDL 事件泵刷新定时器,PC 平台专属
 } tp_pc_state_t;
 
 static tp_pc_state_t s_tp_state;
 
+static LUAT_RT_RET_TYPE tp_flush_timer_cb(LUAT_RT_CB_PARAM) {
+    // 定时调用 SDL2 flush 保持事件泵活跃,即使界面没有新的脏区
+    luat_sdl2_pump_events();
+}
+
+// 启动 SDL 刷新定时器,保证窗口事件泵持续运行
+static void start_tp_flush_timer(void) {
+    if (s_tp_state.flush_timer) {
+        return;
+    }
+    if (luat_rtos_timer_create(&s_tp_state.flush_timer) == 0) {
+        luat_rtos_timer_start(s_tp_state.flush_timer, TP_PC_FLUSH_INTERVAL_MS, 1, tp_flush_timer_cb, NULL);
+    } else {
+        LLOGW("pc tp unable to create refresh timer");
+    }
+}
+
+// 停止并释放 SDL 刷新定时器,避免重复创建
+static void stop_tp_flush_timer(void) {
+    if (!s_tp_state.flush_timer) {
+        return;
+    }
+    luat_rtos_timer_stop(s_tp_state.flush_timer);
+    luat_rtos_timer_delete(s_tp_state.flush_timer);
+    s_tp_state.flush_timer = NULL;
+}
+
 // 队列管理函数 - 兼容原有框架
 static int enqueue_event(luat_tp_data_t* event) {
     if (!event || !s_tp_state.queue_enabled) return -1;
@@ -279,6 +311,9 @@ static int luat_tp_pc_init(luat_tp_config_t* conf) {
     SDL_AddEventWatch(tp_sdl_watch, conf);
     LLOGI("pc tp init ok, w=%d h=%d tp_num=%d, queue_enabled=%d, queue_size=%d",
            conf->w, conf->h, conf->tp_num, s_tp_state.queue_enabled, s_tp_state.event_queue_size);
+
+    // 启动 SDL 刷新定时器,保证窗口事件泵持续运行
+    start_tp_flush_timer();
     return 0;
 }
 
@@ -356,7 +391,9 @@ static void luat_tp_pc_deinit(luat_tp_config_t* conf) {
             s_tp_state.queue_enabled = 0;
             uv_mutex_unlock(&s_tp_state.lock);
         }
-
+        // 停止并释放 SDL 刷新定时器,避免重复创建
+        stop_tp_flush_timer();
+    
         LLOGI("pc tp deinit complete");
     }
 }

+ 2 - 1
bsp/pc/port/luat_cmds.c

@@ -5,6 +5,7 @@
 #include "lundump.h"
 #include "luat_mock.h"
 #include "luat_luadb2.h"
+#include <stdlib.h>
 
 #define LUAT_LOG_TAG "fs"
 #include "luat_log.h"
@@ -444,7 +445,7 @@ void *check_file_path(const char *path)
 		memcpy(buff, path, strlen(path));
 		#else
 		memcpy(buff, path, strlen(path) - 1);
-		#endif;
+		#endif
 		dp = opendir(buff);
 		// LLOGD("目录打开 %p", dp);
 		if (dp != NULL)

+ 2 - 2
bsp/pc/port/luat_malloc_mini.c

@@ -125,9 +125,9 @@ void luat_meminfo_sys(size_t *total, size_t *used, size_t *max_used) {
 
 void luat_heap_opt_init(LUAT_HEAP_TYPE_E type){
     if (type == LUAT_HEAP_PSRAM && psram_ptr == NULL) {
-        psram_ptr = malloc(2*1024*1024);
+        psram_ptr = malloc(8*1024*1024);
         luat_bget_init(&psram_bget);
-        luat_bpool(&psram_bget, psram_ptr, 2*1024*1024);
+        luat_bpool(&psram_bget, psram_ptr, 8*1024*1024);
     }
     else if (type == LUAT_HEAP_SRAM && sram_ptr == NULL) {
         sram_ptr = malloc(1024*1024);

+ 4 - 0
bsp/pc/port/luat_mobile_pc.c

@@ -4,7 +4,9 @@
 #include "luat_str.h"
 #include "luat_mcu.h"
 #include "luat_crypto.h"
+#ifdef LUAT_USE_LWIP
 #include "lwip/ip_addr.h"
+#endif
 
 // #define LUAT_LOG_TAG "mobile"
 
@@ -151,10 +153,12 @@ int luat_mobile_get_flymode(int index)
 {
     return 0;
 }
+#ifdef LUAT_USE_LWIP
 int luat_mobile_get_local_ip(int sim_id, int cid, ip_addr_t *ip_v4, ip_addr_t *ip_v6)
 {
     return 0;
 }
+#endif
 int luat_mobile_get_cell_info(luat_mobile_cell_info_t  *info)
 {
     return 0;

+ 32 - 31
bsp/pc/port/network/luat_network_adapter_libuv.c

@@ -148,7 +148,7 @@ static void cb_nw_task_async(uv_async_t *async) {
     free_uv_handle(async);
 }
 
-static void cb_to_nw_task(uint32_t event_id, uint32_t param1, uint32_t param2, uint32_t param3)
+static void cb_to_nw_task(uint32_t event_id, size_t param1, size_t param2, size_t param3)
 {
     int ret = 0;
     task_event_async_t *e = luat_heap_malloc(sizeof(task_event_async_t));
@@ -170,7 +170,7 @@ static void cb_to_nw_task(uint32_t event_id, uint32_t param1, uint32_t param2, u
     luat_network_cb_param_t param = {.tag = 0, .param = NULL};
     if ((e->event.ID > EV_NW_DNS_RESULT))
     {
-        e->event.Param3 = sockets[e->event.Param1].param;
+        e->event.Param3 = (size_t)sockets[e->event.Param1].param;
         param.tag = sockets[e->event.Param1].tag;
     }
     memcpy(&e->param, &param, sizeof(luat_network_cb_param_t));
@@ -181,7 +181,7 @@ static void cb_to_nw_task(uint32_t event_id, uint32_t param1, uint32_t param2, u
 
 static int libuv_set_dns_server(uint8_t server_index, luat_ip_addr_t *ip, void *user_data);
 
-static void libuv_callback_to_nw_task(uint8_t adapter_index, uint32_t event_id, uint32_t param1, uint32_t param2, uint32_t param3);
+static void libuv_callback_to_nw_task(uint8_t adapter_index, uint32_t event_id, size_t param1, size_t param2, size_t param3);
 
 static int libuv_socket_check(int socket_id, uint64_t tag, void *user_data)
 {
@@ -254,7 +254,7 @@ static void on_recv(uv_stream_t *handler,
                     ssize_t nread,
                     const uv_buf_t *buf)
 {
-    int32_t socket_id = (int32_t)handler->data;
+    int32_t socket_id = (int32_t)(intptr_t)handler->data;
     int ret = 0;
     LLOGD("socket[%d] on_recv %d", socket_id, nread);
     // if (sockets[socket_id].state == SC_CLOSED)
@@ -276,7 +276,7 @@ static void on_recv(uv_stream_t *handler,
             if (sockets[socket_id].state != SC_CLOSING && sockets[socket_id].state != SC_CLOSED) {
                 set_socket_state(socket_id, SC_CLOSING);
                 // LLOGD("发送EV_NW_SOCKET_REMOTE_CLOSE消息");
-                cb_to_nw_task(EV_NW_SOCKET_REMOTE_CLOSE, socket_id, 0, sockets[socket_id].param);
+                cb_to_nw_task(EV_NW_SOCKET_REMOTE_CLOSE, socket_id, 0, (size_t)sockets[socket_id].param);
             }
         }
         else
@@ -285,7 +285,7 @@ static void on_recv(uv_stream_t *handler,
             // uv_shutdown()
             set_socket_state(socket_id, SC_CLOSING);
             // LLOGD("发送EV_NW_SOCKET_ERROR消息");
-            cb_to_nw_task(EV_NW_SOCKET_ERROR, socket_id, 0, sockets[socket_id].param);
+            cb_to_nw_task(EV_NW_SOCKET_ERROR, socket_id, 0, (size_t)sockets[socket_id].param);
         }
         // uv_close(handler, on_close);
         return;
@@ -309,7 +309,7 @@ static void on_recv(uv_stream_t *handler,
         if (ptr == NULL)
         {
             LLOGD("socket[%d] 内存不足, 无法存放更多接收到的数据", socket_id);
-            cb_to_nw_task(EV_NW_SOCKET_ERROR, socket_id, 0, sockets[socket_id].param);
+            cb_to_nw_task(EV_NW_SOCKET_ERROR, socket_id, 0, (size_t)sockets[socket_id].param);
             return;
         }
         sockets[socket_id].recv_buff = ptr;
@@ -317,7 +317,7 @@ static void on_recv(uv_stream_t *handler,
         sockets[socket_id].recv_size += nread;
     }
     luat_heap_free(buf->base);
-    cb_to_nw_task(EV_NW_SOCKET_RX_NEW, socket_id, nread, sockets[socket_id].param);
+    cb_to_nw_task(EV_NW_SOCKET_RX_NEW, socket_id, nread, (size_t)sockets[socket_id].param);
     return;
 }
 
@@ -327,7 +327,7 @@ static void on_recv_udp(uv_udp_t *udp,
                         const struct sockaddr *addr,
                         unsigned flags)
 {
-    int32_t socket_id = (int32_t)udp->data;
+    int32_t socket_id = (int32_t)(intptr_t)udp->data;
     LLOGD("socket[%d] UDP接收回调 %d", socket_id, nread);
     if (nread < 0)
     {
@@ -374,14 +374,14 @@ static void on_recv_udp(uv_udp_t *udp,
             head = head->next;
         }
     }
-    cb_to_nw_task(EV_NW_SOCKET_RX_NEW, socket_id, nread, sockets[socket_id].param);
+    cb_to_nw_task(EV_NW_SOCKET_RX_NEW, socket_id, nread, (size_t)sockets[socket_id].param);
     // LLOGD("完成on_recv_udp函数");
 }
 
 static void on_connect(uv_connect_t *req, int status)
 {
     // LLOGD("on_connect %d", status);
-    int32_t socket_id = (int32_t)req->data;
+    int32_t socket_id = (int32_t)(intptr_t)req->data;
     int ret = 0;
     if (status != 0)
     {
@@ -432,7 +432,7 @@ static void udp_connect_async(uv_async_t *async)
     int socket_id = c->socket_id;
     // ret = uv_udp_connect(&sockets[socket_id].udp, (const struct sockaddr *)&c->addr);
     // memcpy(&sockets[socket_id].remote, (const struct sockaddr *)&c->addr, sizeof(const struct sockaddr));
-    on_connect(&sockets[socket_id].udp, ret);
+    on_connect((uv_connect_t*)&sockets[socket_id].udp, ret);
     free_uv_handle(async);
 }
 
@@ -453,7 +453,7 @@ static int libuv_socket_connect(int socket_id, uint64_t tag, uint16_t local_port
     char addr[17] = {'\0'};
     uv_ip4_name(&saddr, addr, 16);
     LLOGI("socket[%d] connect to %s:%d %s", socket_id, addr, remote_port, sockets[socket_id].is_tcp ? "TCP" : "UDP");
-    sockets[socket_id].c.data = (void *)socket_id;
+    sockets[socket_id].c.data = (void *)(intptr_t)socket_id;
     if (sockets[socket_id].is_tcp)
     {
         ret = uv_tcp_connect(&sockets[socket_id].c, &sockets[socket_id].tcp, (const struct sockaddr *)&saddr, on_connect);
@@ -507,7 +507,7 @@ static int libuv_socket_accept(int socket_id, uint64_t tag, luat_ip_addr_t *remo
 
 static void on_close(uv_handle_t *handle)
 {
-    int32_t socket_id = (int32_t)handle->data;
+    int32_t socket_id = (int32_t)(intptr_t)handle->data;
     // LLOGD("on_close %d", socket_id);
     if (socket_id < 0 || socket_id >= MAX_SOCK_NUM)
     {
@@ -521,13 +521,14 @@ static void on_close(uv_handle_t *handle)
     }
     // sockets[socket_id].state = SC_CLOSED;
     set_socket_state(socket_id, SC_CLOSED);
-    cb_to_nw_task(EV_NW_SOCKET_CLOSE_OK, socket_id, 0, sockets[socket_id].param);
+    cb_to_nw_task(EV_NW_SOCKET_CLOSE_OK, socket_id, 0, (size_t)sockets[socket_id].param);
     sockets[socket_id].tag = 0;
 }
 
-static void on_shutdown(uv_shutdown_t *handle)
+static void on_shutdown(uv_shutdown_t *handle, int status)
 {
-    int32_t socket_id = (int32_t)handle->data;
+    (void)status;
+    int32_t socket_id = (int32_t)(intptr_t)handle->data;
     LLOGD("socket[%d] on_shutdown", socket_id);
     if (socket_id < 0 || socket_id >= MAX_SOCK_NUM)
     {
@@ -542,14 +543,14 @@ static void on_shutdown(uv_shutdown_t *handle)
     }
     // sockets[socket_id].state = SC_CLOSED;
     set_socket_state(socket_id, SC_CLOSED);
-    cb_to_nw_task(EV_NW_SOCKET_CLOSE_OK, socket_id, 0, sockets[socket_id].param);
+    cb_to_nw_task(EV_NW_SOCKET_CLOSE_OK, socket_id, 0, (size_t)sockets[socket_id].param);
     sockets[socket_id].tag = 0;
     luat_heap_free(handle);
 }
 
 static void udp_async_close(uv_async_t *handle)
 {
-    int socket_id = (int)handle->data;
+    int socket_id = (int)(intptr_t)handle->data;
     free_uv_handle(handle);
     on_close(&sockets[socket_id].udp);
 }
@@ -575,7 +576,7 @@ static int close_socket(int socket_id, const char *tag)
         if (ret)
             LLOGI("socket[%d] uv_udp_recv_stop %d %s", socket_id, ret, uv_err_name(ret));
         uv_async_t *async = luat_heap_malloc(sizeof(uv_async_t));
-        async->data = (void *)socket_id;
+        async->data = (void *)(intptr_t)socket_id;
         uv_async_init(main_loop, async, udp_async_close);
         ret = uv_async_send(async);
         if (ret) {
@@ -655,7 +656,7 @@ static int libuv_socket_receive(int socket_id, uint64_t tag, uint8_t *buf, uint3
         }
         memcpy(buf, sockets[socket_id].recv_buff, len);
         size_t newsize = sockets[socket_id].recv_size - len;
-        if (newsize == NULL)
+        if (newsize == 0)
         {
             luat_heap_free(sockets[socket_id].recv_buff);
             sockets[socket_id].recv_buff = NULL;
@@ -707,19 +708,19 @@ static void on_sent(uv_write_t *req, int status)
     tmp += sizeof(uv_write_t);
     uint32_t len = 0;
     memcpy(&len, tmp, 4);
-    int socket_id = (int32_t)req->data;
+    int socket_id = (int32_t)(intptr_t)req->data;
     LLOGD("socket[%d] tcp sent %d %d", socket_id, status, len);
     luat_heap_free(req);
 
     if (status == 0)
     {
         // LLOGD("发送成功, 执行TX_OK消息");
-        cb_to_nw_task(EV_NW_SOCKET_TX_OK, socket_id, len, sockets[socket_id].param);
+        cb_to_nw_task(EV_NW_SOCKET_TX_OK, socket_id, len, (size_t)sockets[socket_id].param);
     }
     else
     {
         // LLOGD("发送失败, 执行ERROR消息");
-        cb_to_nw_task(EV_NW_SOCKET_ERROR, socket_id, 0, sockets[socket_id].param);
+        cb_to_nw_task(EV_NW_SOCKET_ERROR, socket_id, 0, (size_t)sockets[socket_id].param);
     }
 }
 
@@ -735,12 +736,12 @@ static void on_sent_udp(uv_udp_send_t *req, int status)
     if (status == 0)
     {
         // LLOGD("发送成功, 执行TX_OK消息");
-        cb_to_nw_task(EV_NW_SOCKET_TX_OK, socket_id, len, sockets[socket_id].param);
+        cb_to_nw_task(EV_NW_SOCKET_TX_OK, socket_id, len, (size_t)sockets[socket_id].param);
     }
     else
     {
         // LLOGD("发送成功, 执行ERROR消息");
-        cb_to_nw_task(EV_NW_SOCKET_ERROR, socket_id, 0, sockets[socket_id].param);
+        cb_to_nw_task(EV_NW_SOCKET_ERROR, socket_id, 0, (size_t)sockets[socket_id].param);
     }
     luat_heap_free(req);
 }
@@ -778,7 +779,7 @@ static int libuv_socket_send(int socket_id, uint64_t tag, const uint8_t *buf, ui
         tmp = (char *)req;
         tmp += sizeof(uv_write_t);
         memcpy(tmp, &len, 4);
-        req->data = (void *)socket_id;
+        req->data = (void *)(intptr_t)socket_id;
         ret = uv_write(req, (uv_stream_t *)&sockets[socket_id].tcp, &buff, 1, on_sent);
         if (ret) {
             luat_heap_free(req);
@@ -792,7 +793,7 @@ static int libuv_socket_send(int socket_id, uint64_t tag, const uint8_t *buf, ui
         tmp = (char *)send_req;
         tmp += sizeof(uv_udp_send_t);
         memcpy(tmp, &len, 4);
-        send_req->data = (void *)socket_id;
+        send_req->data = (void *)(intptr_t)socket_id;
         #ifdef LUAT_USE_LWIP
         send_addr.sin_addr.s_addr = ip_2_ip4(remote_ip)->addr;
         #else
@@ -909,7 +910,7 @@ static void on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo
     if (status < 0)
     {
         LLOGI("dns query failed %s", query->domain);
-        cb_to_nw_task(EV_NW_DNS_RESULT, 0, 0, query->param);
+        cb_to_nw_task(EV_NW_DNS_RESULT, 0, 0, (size_t)query->param);
         luat_heap_free(query);
         return;
     }
@@ -920,7 +921,7 @@ static void on_resolved(uv_getaddrinfo_t *resolver, int status, struct addrinfo
     luat_dns_ip_result *ip_result = zalloc(sizeof(luat_dns_ip_result));
     network_set_ip_ipv4(&ip_result->ip, ((struct sockaddr_in *)res->ai_addr)->sin_addr.s_addr);
     ip_result->ttl_end = 60;
-    cb_to_nw_task(EV_NW_DNS_RESULT, 1, (int)ip_result, query->param);
+    cb_to_nw_task(EV_NW_DNS_RESULT, 1, (size_t)ip_result, (size_t)query->param);
     luat_heap_free(query);
     uv_freeaddrinfo(res);
 }
@@ -951,7 +952,7 @@ static int libuv_dns(const char *domain_name, uint32_t len, void *param, void *u
     {
         LLOGI("uv_getaddrinfo %d", r);
         luat_heap_free(query);
-        cb_to_nw_task(EV_NW_DNS_RESULT, 0, 0, param);
+        cb_to_nw_task(EV_NW_DNS_RESULT, 0, 0, (size_t)param);
     }
     return r;
 }

+ 928 - 0
bsp/pc/test/092.exftp/exftp.lua

@@ -0,0 +1,928 @@
+--[[
+@module  exftp
+@summary FTP客户端库
+@version 1.0
+@date    2025.12.18
+@author  刘斌
+@tag    LUAT_USE_NETWORK
+@usage  啦啦啦啦啦啦啦啦啦啦啦啦啦
+]]
+
+local exftp = {}
+local TAG = "exftp"
+
+-- FTP响应码常量
+local FTP_CODE = {
+    SERVICE_READY = 220,        -- 服务就绪
+    USER_OK = 331,              -- 用户名正确,需要密码
+    LOGIN_OK = 230,              -- 登录成功
+    ENTER_PASSIVE = 227,         -- 进入被动模式
+    FILE_ACTION_OK = 250,        -- 文件操作成功
+    PATHNAME_CREATED = 257,      -- 路径名已创建
+    DATA_CONN_OPEN = 150,        -- 数据连接已打开
+    DATA_CONN_ALREADY_OPEN = 125, -- 数据连接已打开,传输开始
+    TRANSFER_COMPLETE = 226,     -- 传输完成
+    CLOSING_DATA = 226,         -- 关闭数据连接
+}
+
+-- 状态常量
+local STATE = {
+    DISCONNECTED = 0,   -- 未连接
+    CONNECTED = 1,      -- 已连接但未认证
+    AUTHENTICATED = 2,  -- 已认证
+    TRANSFERRING = 3,   -- 正在传输
+}
+
+-- 创建FTP客户端实例
+-- @param adapter 网络适配器,nil表示使用默认
+-- @param host FTP服务器地址
+-- @param port FTP服务器端口,默认21
+-- @return ftp客户端实例
+function exftp.create(adapter, host, port)
+    if not host then
+        log.error(TAG, "host不能为空")
+        return nil
+    end
+
+    port = port or 21
+
+    local self = {
+        adapter = adapter,
+        host = host,
+        port = port,
+        state = STATE.DISCONNECTED,
+        cmd_netc = nil,      -- 命令通道socket
+        data_netc = nil,     -- 数据通道socket
+        cmd_rx_buff = nil,   -- 命令通道接收缓冲区
+        data_rx_buff = nil,  -- 数据通道接收缓冲区
+        topic = nil,         -- 事件通知topic
+        username = nil,
+        password = nil,
+        last_response = nil, -- 最后一次响应
+        last_code = nil,     -- 最后一次响应码
+        debug_on = false,    -- 调试开关
+        data_closed = false, -- 数据通道是否已关闭
+    }
+
+    -- 创建命令通道socket
+    self.cmd_rx_buff = zbuff.create(1024)
+
+    local function cmd_socket_cb(netc, event)
+        if not self.cmd_netc then
+            return
+        end
+
+        if event == socket.ON_LINE then
+            -- TCP连接建立(参考httpplus,直接publish topic)
+            sys.publish(self.topic)
+        elseif event == socket.TX_OK then
+            -- 数据发送完成
+            sys.publish(self.topic)
+        elseif event == socket.EVENT then
+            -- 收到数据
+            local ok = socket.rx(netc, self.cmd_rx_buff)
+            if ok and self.cmd_rx_buff:used() > 0 then
+                sys.publish(self.topic)
+            end
+        elseif event == socket.CLOSED then
+            sys.publish(self.topic)
+        end
+    end
+
+    -- 创建命令通道socket,如果指定适配器无效,尝试使用默认适配器
+    self.cmd_netc = socket.create(self.adapter, cmd_socket_cb)
+    if not self.cmd_netc then
+        log.error(TAG, "创建命令通道socket失败")
+        return nil
+    end
+
+    -- 使用socket对象作为topic(参考httpplus)
+    self.topic = tostring(self.cmd_netc)
+
+    socket.config(self.cmd_netc)
+
+    -- 连接命令通道
+    if not socket.connect(self.cmd_netc, self.host, self.port) then
+        log.error(TAG, "连接FTP服务器失败")
+        socket.release(self.cmd_netc)
+        return nil
+    end
+
+    -- 设置元表
+    setmetatable(self, {__index = exftp})
+
+    -- 等待连接建立(参考httpplus)
+    local ret = sys.waitUntil(self.topic, 5000)
+    if ret == false then
+        log.error(TAG, "等待连接建立超时")
+        socket.close(self.cmd_netc)
+        socket.release(self.cmd_netc)
+        return nil
+    end
+
+    -- 等待服务器发送服务就绪响应,FTP服务器在连接建立后会立即发送220响应
+    ret = sys.waitUntil(self.topic, 5000)
+    if ret == false then
+        log.error(TAG, "等待服务就绪响应超时")
+        socket.close(self.cmd_netc)
+        socket.release(self.cmd_netc)
+        return nil
+    end
+
+    -- 检查是否有数据
+    if not self.cmd_rx_buff or self.cmd_rx_buff:used() == 0 then
+        log.error(TAG, "未收到服务就绪响应数据")
+        socket.close(self.cmd_netc)
+        socket.release(self.cmd_netc)
+        return nil
+    end
+
+    -- 解析初始响应
+    local code, msg = self:_parse_response()
+    if not code or code ~= FTP_CODE.SERVICE_READY then
+        log.error(TAG, "FTP服务未就绪", code, msg)
+        socket.close(self.cmd_netc)
+        socket.release(self.cmd_netc)
+        return nil
+    end
+
+    self.state = STATE.CONNECTED
+
+    return self
+end
+
+-- 解析FTP响应
+-- @return code 响应码,nil表示解析失败
+-- @return msg
+function exftp:_parse_response()
+    if not self.cmd_rx_buff or self.cmd_rx_buff:used() == 0 then
+        return nil
+    end
+
+    local response = self.cmd_rx_buff:query()
+    if not response or #response < 3 then
+        return nil
+    end
+
+    -- FTP响应格式: "220 Service ready\r\n" 或 "150 Opening data connection\r\n"
+    local lines = {}
+    for line in response:gmatch("[^\r\n]+") do
+        table.insert(lines, line)
+    end
+
+    if #lines == 0 then
+        return nil
+    end
+
+    -- 从后往前查找,找到第一个FTP响应码行
+    local last_line = nil
+    for i = #lines, 1, -1 do
+        local line = lines[i]
+        if #line >= 3 then
+            local code_str = line:sub(1, 3)
+            if tonumber(code_str) then
+                last_line = line
+                break
+            end
+        end
+    end
+
+    if not last_line then
+        return nil
+    end
+
+    local code_str = last_line:sub(1, 3)
+    local code = tonumber(code_str)
+    if not code then
+        return nil
+    end
+
+    -- 提取消息部分
+    local msg = last_line:match("^%d+%s+(.+)")
+    if not msg then
+        msg = ""
+    end
+
+    self.last_code = code
+    self.last_response = response
+
+    -- 清空缓冲区(已处理完)
+    self.cmd_rx_buff:del()
+
+    return code, msg
+end
+
+-- 发送FTP命令并等待响应
+-- @param cmd FTP命令
+-- @param timeout 超时时间(毫秒),默认5000
+-- @return code 响应码,nil表示失败
+function exftp:_send_cmd(cmd, timeout)
+    timeout = timeout or 5000
+
+    -- 发送命令(FTP命令必须以\r\n结尾)
+    local tx_ok = socket.tx(self.cmd_netc, cmd .. "\r\n")
+    if tx_ok == false then
+        log.error(TAG, "发送命令失败", cmd)
+        return nil
+    end
+
+    -- 等待数据发送完成(TX_OK事件)
+    local ret = sys.waitUntil(self.topic, timeout)
+    if ret == false then
+        log.error(TAG, "等待命令发送完成超时", cmd)
+        return nil
+    end
+
+    -- 等待服务器响应(EVENT事件,收到数据)
+    ret = sys.waitUntil(self.topic, timeout)
+    if ret == false then
+        log.error(TAG, "等待响应超时", cmd)
+        return nil
+    end
+
+    -- 检查是否有响应数据
+    if not self.cmd_rx_buff or self.cmd_rx_buff:used() == 0 then
+        log.error(TAG, "未收到响应数据", cmd)
+        return nil
+    end
+
+    -- 解析响应
+    return self:_parse_response()
+end
+
+-- 进入被动模式(PASV)
+-- @return ip 数据通道IP地址
+-- @return port 数据通道端口
+function exftp:_enter_pasv()
+    local code, msg = self:_send_cmd("PASV", 5000)
+    if not code or code ~= FTP_CODE.ENTER_PASSIVE then
+        log.error(TAG, "PASV命令失败", "code:", code, "msg:", msg, "完整响应:", self.last_response)
+        return nil, nil
+    end
+
+    -- 解析PASV响应: "227 Entering Passive Mode (192,168,1,100,123,45)"
+    local pasv_info = msg:match("%(([^)]+)%)")
+    if not pasv_info then
+        log.error(TAG, "PASV响应格式错误", msg)
+        return nil, nil
+    end
+
+    local parts = {}
+    for part in pasv_info:gmatch("([^,]+)") do
+        table.insert(parts, tonumber(part:match("%d+")))
+    end
+
+    if #parts ~= 6 then
+        log.error(TAG, "PASV响应解析失败", pasv_info)
+        return nil, nil
+    end
+
+    local ip = string.format("%d.%d.%d.%d", parts[1], parts[2], parts[3], parts[4])
+    local port = parts[5] * 256 + parts[6]
+
+    -- -- 检查IP地址是否可路由
+    -- if ip:match("^172%.") or
+    --     ip:match("^192%.") or
+    --     ip:match("^10%.") or
+    --     ip == "127.0.0.1" or
+    --     ip:match("^169%.254%.") or
+    --     ip:match("^0%.") then
+    --     -- 使用服务器地址代替
+    --     if self.debug_on then
+    --         log.debug(TAG, "PASV返回不可路由地址,使用服务器地址", ip, self.host)
+    --     end
+    --     ip = self.host
+    -- end
+
+    if self.debug_on then
+        log.debug(TAG, "PASV成功", ip, port)
+    end
+
+    return ip, port
+end
+
+-- 建立数据通道连接
+-- @return true成功,false失败
+function exftp:_connect_data_channel()
+    -- 如果已有数据通道,先关闭
+    if self.data_netc then
+        socket.close(self.data_netc)
+        socket.release(self.data_netc)
+        self.data_netc = nil
+    end
+
+    -- 进入被动模式
+    local data_ip, data_port = self:_enter_pasv()
+    if not data_ip or not data_port then
+        return false
+    end
+
+    -- 创建数据通道socket
+    self.data_rx_buff = zbuff.create(2048)
+    self.data_closed = false  -- 重置关闭标志
+
+    local function data_socket_cb(netc, event)
+        if not self.data_netc then
+            return
+        end
+
+        -- 使用数据通道自己的topic(参考httpplus)
+        local data_topic = self.data_topic or tostring(self.data_netc)
+
+        if event == socket.ON_LINE then
+            -- TCP连接建立(参考httpplus,直接publish topic)
+            sys.publish(data_topic)
+        elseif event == socket.TX_OK then
+            -- 数据发送完成
+            sys.publish(data_topic)
+        elseif event == socket.EVENT then
+            -- 收到数据
+            local ok = socket.rx(netc, self.data_rx_buff)
+            if not ok or (self.data_rx_buff and self.data_rx_buff:used() == 0) then
+                -- 连接关闭或没有数据
+                if not self.data_closed then
+                    self.data_closed = true
+                    if self.debug_on then
+                        log.debug(TAG, "数据通道关闭")
+                    end
+                end
+            end
+            sys.publish(data_topic)
+        elseif event == socket.CLOSED then
+            if not self.data_closed then
+                self.data_closed = true
+                if self.debug_on then
+                    log.debug(TAG, "数据通道关闭")
+                end
+            end
+            sys.publish(data_topic)
+        end
+    end
+
+    -- 创建数据通道socket,如果指定适配器无效,尝试使用默认适配器
+    self.data_netc = socket.create(self.adapter, data_socket_cb)
+    if not self.data_netc and self.adapter ~= nil then
+        -- 如果指定适配器失败,尝试使用默认适配器(兼容PC模拟器)
+        log.warn(TAG, "数据通道使用指定适配器失败,尝试使用默认适配器", self.adapter)
+        self.data_netc = socket.create(nil, data_socket_cb)
+    end
+    if not self.data_netc then
+        log.error(TAG, "创建数据通道socket失败")
+        return false
+    end
+
+    -- 使用数据通道socket对象作为topic(参考httpplus)
+    self.data_topic = tostring(self.data_netc)
+
+    socket.config(self.data_netc)
+
+    -- 连接数据通道
+    if not socket.connect(self.data_netc, data_ip, data_port) then
+        log.error(TAG, "连接数据通道失败", data_ip, data_port)
+        socket.release(self.data_netc)
+        self.data_netc = nil
+        return false
+    end
+
+    -- 等待连接建立(参考httpplus)
+    local ret = sys.waitUntil(self.data_topic, 5000)
+    if ret == false then
+        log.error(TAG, "数据通道连接超时")
+        socket.close(self.data_netc)
+        socket.release(self.data_netc)
+        self.data_netc = nil
+        return false
+    end
+
+    if self.debug_on then
+        log.debug(TAG, "数据通道连接成功")
+    end
+
+    return true
+end
+
+-- 关闭数据通道
+function exftp:_close_data_channel()
+    self.data_closed = true  -- 标记为已关闭
+    if self.data_netc then
+        socket.close(self.data_netc)
+        socket.release(self.data_netc)
+        self.data_netc = nil
+    end
+    if self.data_rx_buff then
+        self.data_rx_buff = nil
+    end
+end
+
+-- 清理传输错误(关闭数据通道、关闭文件、恢复状态)
+-- @param fd 文件句柄,可为nil
+function exftp:_cleanup_transfer_error(fd)
+    self:_close_data_channel()
+    if fd then
+        fd:close()
+    end
+    self.state = STATE.AUTHENTICATED
+end
+
+-- 检查传输命令响应码是否有效
+-- @param code 响应码
+-- @return true有效,false无效
+function exftp:_is_valid_transfer_response(code)
+    return code == FTP_CODE.DATA_CONN_OPEN or
+           code == FTP_CODE.DATA_CONN_ALREADY_OPEN or
+           code == FTP_CODE.FILE_ACTION_OK
+end
+
+-- 等待传输完成响应(226)
+-- @return true收到传输完成响应,false未收到
+function exftp:_wait_transfer_complete()
+    local has_226 = false
+    if self.cmd_rx_buff and self.cmd_rx_buff:used() > 0 then
+        local code, msg = self:_parse_response()
+        if code and code == FTP_CODE.TRANSFER_COMPLETE then
+            has_226 = true
+            if self.debug_on then
+                log.debug(TAG, "传输完成", msg)
+            end
+        end
+    end
+
+    -- 如果没有226响应,等待接收
+    if not has_226 then
+        local ret = sys.waitUntil(self.topic, 10000)
+        if ret == false then
+            log.warn(TAG, "等待传输完成响应超时")
+        else
+            -- 检查是否有226响应
+            if self.cmd_rx_buff and self.cmd_rx_buff:used() > 0 then
+                local code, msg = self:_parse_response()
+                if code and code == FTP_CODE.TRANSFER_COMPLETE then
+                    has_226 = true
+                    if self.debug_on then
+                        log.debug(TAG, "传输完成", msg)
+                    end
+                end
+            end
+        end
+    end
+
+    return has_226
+end
+
+-- 认证
+-- @param username 用户名
+-- @param password 密码
+-- @return true成功,false失败
+function exftp:auth(username, password)
+    if self.state ~= STATE.CONNECTED then
+        log.error(TAG, "状态错误,无法认证", self.state)
+        return false
+    end
+
+    self.username = username
+    self.password = password
+
+    -- 发送USER命令
+    local code, msg = self:_send_cmd("USER " .. username, 5000)
+    if not code then
+        return false
+    end
+
+    if code ~= FTP_CODE.USER_OK and code ~= FTP_CODE.LOGIN_OK then
+        log.error(TAG, "USER命令失败", code, msg)
+        return false
+    end
+
+    -- 如果已经是LOGIN_OK,说明不需要密码
+    if code == FTP_CODE.LOGIN_OK then
+        self.state = STATE.AUTHENTICATED
+        return true
+    end
+
+    -- 发送PASS命令
+    code, msg = self:_send_cmd("PASS " .. password, 5000)
+    if not code or code ~= FTP_CODE.LOGIN_OK then
+        log.error(TAG, "PASS命令失败", code, msg)
+        return false
+    end
+
+    self.state = STATE.AUTHENTICATED
+
+    return true
+end
+
+-- 上传文件
+-- @param local_path 本地文件路径
+-- @param remote_path 远程文件路径
+-- @param opts 选项,{timeout=超时时间(毫秒), buffer_size=缓冲区大小(字节)}
+-- @return true成功,false失败
+function exftp:upload(local_path, remote_path, opts)
+    opts = opts or {}
+    local timeout = opts.timeout or 5 * 60 * 1000  -- 默认5分钟
+    local buffer_size = opts.buffer_size  -- 用户指定的缓冲区大小
+
+    if self.state ~= STATE.AUTHENTICATED then
+        return false
+    end
+
+    -- 检查本地文件
+    local fd = io.open(local_path, "rb")
+    if not fd then
+        return false
+    end
+
+    local file_size = io.fileSize(local_path)
+    if not file_size or file_size == 0 then
+        fd:close()
+        return false
+    end
+
+    if self.debug_on then
+        log.debug(TAG, "开始上传文件", local_path, "大小:", file_size, "字节")
+    end
+
+    -- 建立数据通道
+    if not self:_connect_data_channel() then
+        fd:close()
+        return false
+    end
+
+    self.state = STATE.TRANSFERRING
+
+    -- 发送STOR命令
+    local code, msg = self:_send_cmd("STOR " .. remote_path, 10000)
+    if not code then
+        self:_cleanup_transfer_error(fd)
+        return false
+    end
+
+    if not self:_is_valid_transfer_response(code) then
+        self:_cleanup_transfer_error(fd)
+        return false
+    end
+
+    -- 创建上传缓冲区(参考httpplus)
+    if not buffer_size then
+        buffer_size = 1024 * 128
+    end
+    local fbuf = zbuff.create(buffer_size, 0, zbuff.HEAP_PSRAM)
+
+    if not fbuf then
+        fbuf = zbuff.create(1024 * 64, 0, zbuff.HEAP_PSRAM)  -- 默认64KB,增大缓冲区
+    end
+
+    if not fbuf then
+        fbuf = zbuff.create(1024 * 24, 0, zbuff.HEAP_PSRAM)  -- 降级到24KB
+    end
+
+    if not fbuf then
+        fbuf = zbuff.create(1024 * 8, 0, zbuff.HEAP_PSRAM)   -- 降级到8KB
+    end
+
+    if not fbuf then
+        self:_cleanup_transfer_error(fd)
+        return false
+    end
+
+    if self.debug_on then
+        log.debug(TAG, "上传缓冲区大小", fbuf:len())
+    end
+
+    -- 分块上传(并行操作)
+    local total_sent = 0
+    local start_time = os.time()
+    local is_closed = false
+    local upload_completed = false
+
+    -- 预读第一个数据块
+    fbuf:seek(0)
+    local ok, current_flen = fd:fill(fbuf)
+    if not ok or current_flen <= 0 then
+        upload_completed = true
+    else
+        fbuf:seek(current_flen)
+    end
+
+    while not is_closed and not upload_completed do
+
+        -- 发送当前缓冲区的数据
+        if current_flen > 0 then
+            local tx_ok = socket.tx(self.data_netc, fbuf)
+            if tx_ok == false then
+                log.error(TAG, "发送数据失败")
+                is_closed = true
+                break
+            end
+            total_sent = total_sent + current_flen
+
+            -- 显示进度
+            if self.debug_on and total_sent % (1024 * 1024) == 0 then
+                log.debug(TAG, "已上传", total_sent, "/", file_size, string.format("%.1f%%", total_sent * 100 / file_size))
+            end
+        end
+
+        -- 并行优化:发送的同时预读下一个数据块
+        fbuf:seek(0)
+        local ok, next_flen = fd:fill(fbuf)
+        if not ok or next_flen <= 0 then
+            next_flen = 0  -- 没有更多数据
+            if current_flen == 0 then
+                upload_completed = true
+                break
+            end
+        else
+            fbuf:seek(next_flen)
+        end
+
+        -- 等待当前发送完成
+        local ret = sys.waitUntil(self.data_topic, 300)
+        if ret == false then
+            log.warn(TAG, "等待TX_OK超时,继续发送")
+        end
+
+        -- 切换到下一个数据块
+        current_flen = next_flen
+
+        -- 检查超时
+        if os.time() - start_time > timeout / 1000 then
+            log.error(TAG, "上传超时")
+            is_closed = true
+            break
+        end
+    end
+
+    fd:close()
+
+    -- 关闭数据通道(关闭后服务器会通过命令通道发送226响应)
+    self:_close_data_channel()
+
+    -- 等待传输完成响应
+    local transfer_success = self:_wait_transfer_complete()
+
+    self.state = STATE.AUTHENTICATED
+
+    -- 判断上传是否成功
+    local success = (not is_closed and total_sent == file_size) or transfer_success
+
+    if success then
+        if self.debug_on then
+            log.debug(TAG, "上传成功", total_sent, "/", file_size)
+        end
+        return true
+    else
+        log.error(TAG, "上传失败", "is_closed:", is_closed, "total_sent:", total_sent, "file_size:", file_size, "transfer_success:", transfer_success)
+        return false
+    end
+end
+
+-- 下载文件
+-- @param remote_path 远程文件路径
+-- @param local_path 本地文件路径
+-- @param opts 选项,{timeout=超时时间(毫秒)}
+-- @return true成功,false失败
+function exftp:download(remote_path, local_path, opts)
+    opts = opts or {}
+    local timeout = opts.timeout or 5 * 60 * 1000  -- 默认5分钟
+
+    if self.state ~= STATE.AUTHENTICATED then
+        return false
+    end
+
+    -- 先获取远程文件大小(使用SIZE命令)
+    local code, msg = self:_send_cmd("SIZE " .. remote_path, 5000)
+    if not code or code ~= 213 then
+        log.error(TAG, "获取远程文件大小失败", code, msg)
+        return false
+    end
+
+    -- SIZE命令成功,响应格式:213 <size>
+    local remote_file_size = tonumber(msg:match("%d+"))
+    if not remote_file_size then
+        log.error(TAG, "解析远程文件大小失败", msg)
+        return false
+    end
+
+    if self.debug_on then
+        log.debug(TAG, "远程文件大小", remote_file_size, "字节")
+    end
+
+    -- 建立数据通道
+    if not self:_connect_data_channel() then
+        return false
+    end
+
+    self.state = STATE.TRANSFERRING
+
+    -- 发送RETR命令
+    local code, msg = self:_send_cmd("RETR " .. remote_path, 10000)
+    if not code then
+        self:_cleanup_transfer_error()
+        return false
+    end
+
+    if code ~= FTP_CODE.DATA_CONN_OPEN and
+       code ~= FTP_CODE.DATA_CONN_ALREADY_OPEN and
+       code ~= FTP_CODE.FILE_ACTION_OK then
+        self:_cleanup_transfer_error()
+        return false
+    end
+
+    -- 创建本地文件
+    local fd = io.open(local_path, "w+b")
+    if not fd then
+        self:_cleanup_transfer_error()
+        return false
+    end
+
+    -- 接收数据并写入文件(流式写入,避免大文件占用内存)
+    local total_received = 0
+    local start_time = os.time()
+    local is_closed = false
+    local last_data_time = os.time()
+
+    while not is_closed and not self.data_closed do
+        -- 先尝试读取已有数据
+        if self.data_rx_buff and self.data_rx_buff:used() > 0 then
+            local data = self.data_rx_buff:query()
+            fd:write(data)
+            total_received = total_received + #data
+            self.data_rx_buff:del()  -- 清空缓冲区
+            last_data_time = os.time()
+
+            if self.debug_on and total_received % (1024 * 100) == 0 then
+                log.debug(TAG, "已下载", total_received, "/", remote_file_size)
+            end
+        end
+
+        -- 检查是否已接收完所有数据
+        if total_received >= remote_file_size then
+            if self.debug_on then
+                log.debug(TAG, "已接收完所有数据", total_received, "/", remote_file_size)
+            end
+            -- 延时100ms后主动断开数据通道
+            sys.wait(100)
+            self:_close_data_channel()
+            break
+        end
+
+        -- 等待数据或事件(使用数据通道的topic)
+        local ret = sys.waitUntil(self.data_topic, 1000)
+        if ret == false then
+            log.warn(TAG, "等待数据超时")
+        else
+            -- 收到事件(可能是数据到达或连接关闭)
+            -- 检查缓冲区是否有数据
+            if self.data_rx_buff and self.data_rx_buff:used() > 0 then
+                last_data_time = os.time()
+            end
+        end
+
+        -- 检查超时
+        if os.time() - start_time > timeout / 1000 then
+            log.error(TAG, "下载超时")
+            is_closed = true
+            break
+        end
+    end
+
+    -- 最后再读取一次可能剩余的数据
+    if self.data_rx_buff and self.data_rx_buff:used() > 0 then
+        local data = self.data_rx_buff:query()
+        fd:write(data)
+        total_received = total_received + #data
+        self.data_rx_buff:del()
+    end
+
+    fd:close()
+
+    -- 如果数据通道还未关闭,关闭它(关闭后服务器会通过命令通道发送226响应)
+    if not self.data_closed then
+        self:_close_data_channel()
+    end
+
+    -- 等待传输完成响应(已通过文件大小判断完成,可能已经有226响应了)
+    if self.cmd_rx_buff and self.cmd_rx_buff:used() > 0 then
+        local saved_data = self.cmd_rx_buff:query()
+        if saved_data and saved_data:find("226") then
+            -- 已有226响应,直接解析
+            self:_parse_response()
+            if self.debug_on then
+                log.debug(TAG, "已检测到226响应,无需等待")
+            end
+        else
+            -- 没有226响应,等待接收
+            self:_wait_transfer_complete()
+        end
+    else
+        -- 缓冲区为空,等待接收
+        self:_wait_transfer_complete()
+    end
+
+    self.state = STATE.AUTHENTICATED
+
+    if self.debug_on then
+        log.debug(TAG, "下载完成", total_received, remote_file_size and ("/" .. remote_file_size) or "")
+    end
+
+    return true
+end
+
+-- 切换工作目录
+-- @param path 目标目录路径(绝对路径或相对路径)
+-- @return true成功,false失败
+function exftp:chdir(path)
+    if self.state ~= STATE.AUTHENTICATED then
+        return false
+    end
+
+    local code, msg = self:_send_cmd("CWD " .. path, 5000)
+    if not code then
+        return false
+    end
+
+    if code ~= FTP_CODE.FILE_ACTION_OK then
+        return false
+    end
+
+    return true
+end
+
+-- 获取当前工作目录
+-- @return true成功,false失败
+-- @return path 当前目录路径
+function exftp:pwd()
+    if self.state ~= STATE.AUTHENTICATED then
+        return false
+    end
+
+    local code, msg = self:_send_cmd("PWD", 5000)
+    if not code then
+        return false, nil
+    end
+
+    -- PWD响应格式: "257 "/path/to/dir" is current directory"
+    if code == 257 then
+        -- 提取路径(可能在引号中)
+        local path = msg:match('"([^"]+)"') or msg:match("'([^']+)'") or msg:match("%s+([^%s]+)")
+        if path then
+            if self.debug_on then
+                log.debug(TAG, "当前目录", path)
+            end
+            return true, path
+        end
+    end
+
+    return false, nil
+end
+
+-- 返回上一级目录
+-- @return true成功,false失败
+function exftp:cdup()
+    if self.state ~= STATE.AUTHENTICATED then
+        return false
+    end
+
+    local code, msg = self:_send_cmd("CDUP", 5000)
+    if not code then
+        return false
+    end
+
+    if code ~= FTP_CODE.FILE_ACTION_OK then
+        return false
+    end
+
+    return true
+end
+
+-- 设置调试开关
+-- @param onoff true开启,false关闭
+function exftp:debug(onoff)
+    self.debug_on = onoff or false
+    if self.cmd_netc then
+        socket.debug(self.cmd_netc, onoff)
+    end
+    if self.data_netc then
+        socket.debug(self.data_netc, onoff)
+    end
+end
+
+-- 关闭连接
+function exftp:close()
+    self:_close_data_channel()
+
+    if self.cmd_netc then
+        socket.close(self.cmd_netc)
+        socket.release(self.cmd_netc)
+        self.cmd_netc = nil
+    end
+
+    if self.cmd_rx_buff then
+        self.cmd_rx_buff = nil
+    end
+
+    self.state = STATE.DISCONNECTED
+
+end
+
+return exftp
+

+ 178 - 0
bsp/pc/test/092.exftp/main.lua

@@ -0,0 +1,178 @@
+--[[
+@module  exftp测试程序
+@summary exftp库功能测试
+@version 1.0
+@date    2025.12.28
+@usage
+测试exftp库的所有功能,包括:
+1. 创建FTP客户端
+2. 匿名登录
+3. 路径切换
+4. 文件上传
+5. 文件下载
+6. 关闭连接
+]]
+
+_G.sys = require("sys")
+require "sysplus"
+-- 加载exftp库
+local exftp = require "exftp"
+
+-- FTP服务器配置
+local FTP_CONFIG = {
+    host = "192.168.1.119",
+    port = 21,
+    username = "anonymous",
+    password = "",
+}
+
+-- 测试文件配置
+local TEST_FILES = {
+    upload = {
+        local_path = "/samples-master.zip",
+        remote_path = "/samples-master.zip",
+    },
+    download = {
+        remote_path = "/smalldownload.txt",
+        local_path = "/smalldownload.txt",
+    }
+}
+
+-- -- 等待网络就绪
+-- local function wait_network_ready()
+--     log.info("等待网络就绪...")
+--     while not socket.adapter(socket.dft()) do
+--         log.warn("等待IP_READY", socket.dft())
+--         sys.waitUntil("IP_READY", 1000)
+--     end
+--     log.info("网络已就绪", socket.dft())
+-- end
+
+-- 主测试函数
+local function exftp_test_task()
+    log.info("exftp功能测试")
+
+    -- 等待网络就绪
+    sys.waitUntil("IP_READY")
+
+    -- 检查上传文件是否存在(文件应该已经存在于目录中)
+    local file_size = io.fileSize(TEST_FILES.upload.local_path)
+    if not file_size then
+        log.error("上传文件不存在", TEST_FILES.upload.local_path)
+        return
+    end
+
+    -- 1. 创建FTP客户端
+    log.info("=====================创建FTP客户端==============================")
+    log.info("1. 创建FTP客户端")
+    log.info("服务器", FTP_CONFIG.host, "端口", FTP_CONFIG.port)
+
+    -- 使用默认适配器
+    local ftpc = exftp.create(nil, FTP_CONFIG.host, FTP_CONFIG.port)
+    if not ftpc then
+        log.error("创建FTP客户端失败")
+        return
+    end
+    log.info("FTP客户端创建成功")
+
+    -- -- 可选:开启调试
+    -- ftpc:debug(true)
+
+    -- 2. 登录
+    log.info("=====================登录=============================")
+    if not ftpc:auth(FTP_CONFIG.username, FTP_CONFIG.password) then
+        log.error("登录失败")
+        ftpc:close()
+        return
+    end
+    log.info("登录成功")
+
+    -- 3. 路径切换功能
+    log.info("=====================路径切换功能=============================")
+    -- 获取当前目录
+    local ok, current_dir = ftpc:pwd()
+    if ok then
+        log.info("当前目录", current_dir)
+    else
+        log.warn("获取当前目录失败", current_dir)
+    end
+
+    -- 切换到/test目录
+    log.info("切换到 /test 目录")
+    if ftpc:chdir("/test") then
+        log.info("切换目录成功")
+
+        -- 再次获取当前目录
+        ok, current_dir = ftpc:pwd()
+        if ok then
+            log.info("当前目录", current_dir)
+        end
+
+        -- 返回上一级
+        if ftpc:cdup() then
+            log.info("返回上一级目录成功")
+        end
+    else
+        log.warn("切换目录失败")
+    end
+
+    -- 4. 文件上传
+    log.info("=====================文件上传=============================")
+    log.info("本地文件", TEST_FILES.upload.local_path)
+    log.info("远程文件", TEST_FILES.upload.remote_path)
+
+    local ok = ftpc:upload(
+        TEST_FILES.upload.local_path,
+        TEST_FILES.upload.remote_path,
+        {
+            timeout = 30 * 1000,  -- 30秒超时
+            -- buffer_size = n * 1024
+        }
+    )
+
+    if ok then
+        log.info("文件上传成功")
+    else
+        log.error("文件上传失败")
+    end
+
+    -- 5. 文件下载
+    log.info("=====================文件下载=============================")
+    log.info("远程文件", TEST_FILES.download.remote_path)
+    log.info("本地文件", TEST_FILES.download.local_path)
+
+    ok = ftpc:download(
+        TEST_FILES.download.remote_path,
+        TEST_FILES.download.local_path,
+        {
+            timeout = 30 * 1000  -- 30秒超时
+        }
+    )
+
+    if ok then
+        log.info("文件下载成功")
+
+        -- 读取下载的文件内容(如果文件较小)
+        local fd = io.open(TEST_FILES.download.local_path, "r")
+        if fd then
+            local file_size = io.fileSize(TEST_FILES.download.local_path)
+            fd:close()
+            log.info("下载文件大小", file_size, "字节")
+        end
+    else
+        log.error("文件下载失败")
+    end
+
+    -- 6. 关闭连接
+    log.info("=====================关闭连接=============================")
+    ftpc:close()
+    log.info("FTP连接已关闭")
+    log.info("测试完成!")
+end
+
+-- 启动测试任务
+sys.taskInit(exftp_test_task)
+
+-- 运行系统
+sys.run()
+

+ 9 - 1
bsp/pc/xmake.lua

@@ -57,6 +57,7 @@ elseif is_host("macos") then
 end
 
 
+add_includedirs(luatos.."components/mbedtls3/include",{public = true})
 add_includedirs("include",{public = true})
 add_includedirs(luatos.."lua/include",{public = true})
 add_includedirs(luatos.."luat/include",{public = true})
@@ -79,10 +80,15 @@ target("luatos-lua")
     -- add_files(luatos.."luat/modules/*.c")
 
     if is_plat("linux", "macosx") then
+        add_linkdirs("/opt/homebrew/lib", "/usr/local/lib")
         add_links("pthread", "m", "dl")
         add_links("avformat", "avcodec", "avutil", "swresample")    -- FFmpeg
     end
 
+    -- i2c-tools
+    add_includedirs(luatos.."components/i2c-tools")
+    add_files(luatos.."components/i2c-tools/*.c")
+    
     add_files(luatos.."luat/modules/luat_base.c"
             ,luatos.."luat/modules/luat_lib_fs.c"
             ,luatos.."luat/modules/luat_lib_rtos.c"
@@ -99,7 +105,9 @@ target("luatos-lua")
             ,luatos.."luat/modules/luat_lib_rtc.c"
             ,luatos.."luat/modules/luat_lib_gpio.c"
             ,luatos.."luat/modules/luat_lib_spi.c"
+            ,luatos.."luat/modules/luat_lib_softspi.c"
             ,luatos.."luat/modules/luat_lib_i2c.c"
+            ,luatos.."luat/modules/luat_lib_softi2c.c"
             ,luatos.."luat/modules/luat_lib_i2s.c"
             ,luatos.."luat/modules/luat_lib_wdt.c"
             ,luatos.."luat/modules/luat_lib_pm.c"
@@ -197,7 +205,7 @@ target("luatos-lua")
     add_files(luatos.."components/mobile/*.c")
 
     --ffmpeg
-    add_includedirs("ffmpeg_x86/include")
+    -- add_includedirs("ffmpeg_x86/include")
     add_includedirs("ffmpeg_x86")
     add_files("ffmpeg_x86/ffmpeg.c")
 

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

@@ -7,6 +7,7 @@
 #include "luat_crypto.h"
 #include "luat_netdrv.h"
 #include "luat_netdrv_whale.h"
+#include "luat_netdrv_drv.h"
 #include "luat_mcu.h"
 #include "luat_hmeta.h"
 
@@ -595,7 +596,7 @@ static void netdrv_airlink_setup(void* params) {
 	// 自动新增STA和AP的netdrv
 	// 自动新增STA和AP的netdrv
 	luat_netdrv_conf_t conf = {0};
-	conf.impl = 64;
+	conf.impl = LUAT_NETDRV_IMPL_WHALE;
 	// 注册STA
 	conf.id = NW_ADAPTER_INDEX_LWIP_WIFI_STA;
 	luat_netdrv_setup(&conf);

+ 3 - 3
components/common/c_common.h

@@ -285,9 +285,9 @@ typedef uint64_t LongInt;
 typedef struct
 {
 	uint32_t ID;
-	uint32_t Param1;
-	uint32_t Param2;
-	uint32_t Param3;
+	size_t Param1;
+	size_t Param2;
+	size_t Param3;
 }OS_EVENT;
 
 typedef struct

+ 53 - 33
components/fatfs/diskio_spitf.c

@@ -203,6 +203,7 @@ typedef struct
 	uint8_t SDSC;
 	uint8_t ResetCnt;
 	uint8_t CmdCnt;
+	uint8_t ExtraClockNeeded; 		// 是否在发送命令前需要发送一个时钟: 0=no, 1=yes */
 }luat_spitf_ctrl_t;
 
 #define SPI_TF_WAIT(x) luat_rtos_task_sleep(x)
@@ -248,6 +249,11 @@ static int32_t luat_spitf_cmd(luat_spitf_ctrl_t *spitf, uint8_t Cmd, uint32_t Ar
 	uint8_t i, TxLen, DummyLen;
 	int32_t Result = -ERROR_OPERATION_FAILED;
 	luat_spitf_cs(spitf, 1);
+	if (spitf->ExtraClockNeeded)
+	{
+		uint8_t _dummy = 0xff;
+		luat_spi_send(spitf->SpiID, (const char *)&_dummy, 1);
+	}
 	spitf->TempData[0] = 0x40|Cmd;
 	BytesPutBe32(spitf->TempData + 1, Arg);
 	spitf->TempData[5] = CRC7(spitf->TempData, 5);
@@ -277,6 +283,50 @@ static int32_t luat_spitf_cmd(luat_spitf_ctrl_t *spitf, uint8_t Cmd, uint32_t Ar
 			DummyLen = TxLen - i - 1;
 			memcpy(spitf->ExternResult, &spitf->TempData[i + 1], DummyLen);
 			spitf->ExternLen = DummyLen;
+// 			if (spitf->SDHCState == 0xC1 || spitf->SDHCState == 0xC2)
+// 			{
+// 				LLOGE("SD response 0x%02x on CMD%d ARG 0x%08x SDSC=%d CmdCnt=%d", spitf->SDHCState, Cmd, Arg, spitf->SDSC, spitf->CmdCnt);
+// 				DBG_HexPrintf(spitf->TempData, TxLen);
+// 				if (spitf->ExternLen) DBG_HexPrintf(spitf->ExternResult, spitf->ExternLen);
+// 			}
+			/* 如果得到0xC1/0xC2且当前未使用额外时钟,则使用额外时钟重试一次(处理总线干扰,如CH390) */
+			if ((spitf->SDHCState == 0xC1 || spitf->SDHCState == 0xC2) && !(spitf->ExtraClockNeeded))
+			{
+				LLOGD("Got 0x%02x, retrying CMD%d with extra clock to check for bus interference", spitf->SDHCState, Cmd);
+				luat_spitf_cs(spitf, 0);
+				luat_spitf_cs(spitf, 1);
+				{
+					uint8_t _dummy = 0xff;
+					luat_spi_send(spitf->SpiID, (const char *)&_dummy, 1);
+				}
+
+				spitf->TempData[0] = 0x40|Cmd;
+				BytesPutBe32(spitf->TempData + 1, Arg);
+				spitf->TempData[5] = CRC7(spitf->TempData, 5);
+				TxLen = 6 + spitf->CmdCnt;
+				memset(spitf->TempData + 6, 0xff, TxLen - 6);
+				luat_spi_transfer(spitf->SpiID, (const char *)spitf->TempData, TxLen, (char *)spitf->TempData, TxLen);
+
+				for (i = 7; i < TxLen; i++)
+				{
+					if (spitf->TempData[i] != 0xff)
+					{
+						spitf->SDHCState = spitf->TempData[i];
+						DummyLen = TxLen - i - 1;
+						memcpy(spitf->ExternResult, &spitf->TempData[i + 1], DummyLen);
+						spitf->ExternLen = DummyLen;
+						if ((spitf->SDHCState == !spitf->IsInitDone) || !spitf->SDHCState)
+						{
+							Result = ERROR_NONE;
+						}
+						break;
+					}
+				}
+				if (spitf->SDHCState != 0xC1 && spitf->SDHCState != 0xC2)
+				{
+					spitf->ExtraClockNeeded = 1;
+				}
+			}
 			break;
 		}
 	}
@@ -526,6 +576,7 @@ static void luat_spitf_init(luat_spitf_ctrl_t *spitf)
 		spitf->TempData = luat_heap_malloc(__SDHC_BLOCK_LEN__ + 8);
 	}
 	luat_spi_change_speed(spitf->SpiID, 400000);
+	spitf->ExtraClockNeeded = 0; /* default */
 	spitf->IsInitDone = 0;
 	spitf->SDHCState = 0xff;
 	spitf->Info->CardCapacity = 0;
@@ -779,7 +830,6 @@ READ_CONFIG_ERROR:
 static void luat_spitf_read_blocks(luat_spitf_ctrl_t *spitf, uint8_t *Buf, uint32_t StartLBA, uint32_t BlockNums)
 {
 	uint8_t Retry = 0;
-	uint8_t err_Retry = 0;
 	uint8_t error = 1;
 	uint32_t address;
 	Buffer_StaticInit(&spitf->DataBuf, Buf, BlockNums);
@@ -826,22 +876,7 @@ SDHC_SPIREADBLOCKS_CHECK:
 	if (error)
 	{
 		LLOGD("read error %x,%u,%u",spitf->SDHCState, spitf->DataBuf.Pos, spitf->DataBuf.MaxLen);
-        if (spitf->SDHCState == 0xC1 || spitf->SDHCState == 0xC2) {
-			err_Retry++;
-            if (err_Retry > 3)
-			{
-
-				spitf->SDHCError = 1;
-				goto SDHC_SPIREADBLOCKS_ERROR;
-			}
-			else
-			{
-				spitf->SDHCError = 0;
-				spitf->IsInitDone = 1;
-				spitf->SDHCState = 0;
-			}
-			goto SDHC_SPIREADBLOCKS_START;
-		}
+		LLOGE("CMD returned 0x%02x StartLBA=%u address=0x%08x SDSC=%d", spitf->SDHCState, StartLBA, address, spitf->SDSC);
 	}
 	if (spitf->DataBuf.Pos != spitf->DataBuf.MaxLen)
 	{
@@ -893,22 +928,7 @@ SDHC_SPIWRITEBLOCKS_START:
 	}
 	if (luat_spitf_cmd(spitf, CMD25, address, 0))
 	{
-		if (spitf->SDHCState == 0xC1 || spitf->SDHCState == 0xC2) {
-			err_Retry++;
-            if (err_Retry > 3)
-			{
-
-				spitf->SDHCError = 1;
-				goto SDHC_SPIWRITEBLOCKS_ERROR;
-			}
-			else
-			{
-				spitf->SDHCError = 0;
-				spitf->IsInitDone = 1;
-				spitf->SDHCState = 0;
-			}
-			goto SDHC_SPIWRITEBLOCKS_START;
-		}
+		LLOGE("CMD25 returned 0x%02x StartLBA=%u address=0x%08x SDSC=%d", spitf->SDHCState, StartLBA, address, spitf->SDSC);
 		goto SDHC_SPIWRITEBLOCKS_ERROR;
 	}
 	if (luat_spitf_write_data(spitf))

+ 4 - 4
components/i2c-tools/i2c_tools.c

@@ -13,14 +13,14 @@ void i2c_tools(const char * data,size_t len){
     if (memcmp("send", command, 4) == 0){
         int i2c_id = atoi(strtok(NULL, " "));
         i2c_init(i2c_id,0);
-        uint8_t address = strtonum(strtok(NULL, " "));
+        uint8_t address = i2c_tools_strtonum(strtok(NULL, " "));
         uint8_t send_buff[16];
         uint8_t len = 0;
         while (1){
             char* buff = strtok(NULL, " ");
             if (buff == NULL)
                 break;
-            send_buff[len] = strtonum(buff);
+            send_buff[len] = i2c_tools_strtonum(buff);
             len++;
         }
         if(i2c_write(address, send_buff, len)!=1){
@@ -29,8 +29,8 @@ void i2c_tools(const char * data,size_t len){
     }else if(memcmp("recv",command,4) == 0){
         int i2c_id = atoi(strtok(NULL, " "));
         i2c_init(i2c_id,0);
-        uint8_t address = strtonum(strtok(NULL, " "));
-        uint8_t reg = strtonum(strtok(NULL, " "));
+        uint8_t address = i2c_tools_strtonum(strtok(NULL, " "));
+        uint8_t reg = i2c_tools_strtonum(strtok(NULL, " "));
         uint8_t len = atoi(strtok(NULL, " "));
         if (len == 0)len = 1;
         uint8_t *buffer = (uint8_t *)luat_heap_malloc(len);

+ 1 - 1
components/i2c-tools/i2c_utils.c

@@ -10,7 +10,7 @@
 
 static uint8_t i2c_tools_id = 0;
 
-uint8_t strtonum(const char* str){
+uint8_t i2c_tools_strtonum(const char* str){
     uint8_t data;
     if (strcmp(str, "0x")){
         data = (uint8_t)strtol(str, NULL, 0);

+ 1 - 1
components/i2c-tools/i2c_utils.h

@@ -6,7 +6,7 @@
 
 #define I2C_TOOLS_BUFFER_SIZE 64
 
-uint8_t strtonum(const char* str);
+uint8_t i2c_tools_strtonum(const char* str);
 
 void i2c_help(void);
 uint8_t i2c_init(const uint8_t i2c_id, int speed);

+ 1 - 0
components/mobile/luat_mobile_common.c

@@ -1,4 +1,5 @@
 #include "luat_base.h"
+#include "luat_malloc.h"
 #include "luat_mobile.h"
 #include "luat_rtos.h"
 #include "luat_fs.h"

+ 1 - 1
components/network/adapter/luat_lib_socket.c

@@ -1305,7 +1305,7 @@ static int l_socket_remote_ip(lua_State *L)
 		lua_pushfstring(L, "%s", ipaddr_ntoa(&ctrl->netc->dns_ip[i].ip));
 #else
 		PV_Union uPV;
-		uPV.u32 = &ctrl->netc->dns_ip[i].ip.ipv4;
+		uPV.u32 = ctrl->netc->dns_ip[i].ip.ipv4;
 		lua_pushfstring(L, "%d.%d.%d.%d", uPV.u8[0], uPV.u8[1], uPV.u8[2], uPV.u8[3]);
 #endif
 	}

+ 1 - 1
components/network/adapter/luat_network_adapter.c

@@ -625,7 +625,7 @@ static int network_state_wait_dns(network_ctrl_t *ctrl, OS_EVENT *event, network
 		if (event->Param1)
 		{
 			//更新dns cache
-			ctrl->dns_ip = event->Param2;
+			ctrl->dns_ip = (luat_dns_ip_result *)event->Param2;
 			ctrl->dns_ip_nums = event->Param1;
 #ifdef LUAT_USE_LWIP
 			for(int i = 0; i < ctrl->dns_ip_nums; i++)

+ 14 - 14
components/network/libemqtt/luat_lib_mqtt.c

@@ -74,7 +74,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
     switch (msg->arg1) {
 //		case MQTT_MSG_TCP_TX_DONE:
 //			if (mqtt_ctrl->mqtt_cb) {
-//				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+//				lua_geti(L, LUA_REGISTRYINDEX, (lua_Integer)mqtt_ctrl->mqtt_cb);
 //				if (lua_isfunction(L, -1)) {
 //					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 //					lua_pushstring(L, "tcp_ack");
@@ -92,7 +92,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
 			luat_netdrv_fire_socket_event_netctrl(EV_NW_TIMEOUT, mqtt_ctrl->netc, 4);
 			#endif
 			if (mqtt_ctrl->mqtt_ref) {
-				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+				lua_geti(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 				if (lua_isfunction(L, -1)) {
 					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 					lua_pushstring(L, "error");
@@ -106,7 +106,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
 		}
 		case MQTT_MSG_PINGRESP : {
 			if (mqtt_ctrl->mqtt_cb) {
-				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+				lua_geti(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 				if (lua_isfunction(L, -1)) {
 					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 					lua_pushstring(L, "pong");
@@ -129,7 +129,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
 			luat_mqtt_msg_t *mqtt_msg =(luat_mqtt_msg_t *)msg->arg2;
 			if (mqtt_ctrl->mqtt_cb) {
 //				luat_mqtt_msg_t *mqtt_msg =(luat_mqtt_msg_t *)msg->arg2;
-				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+				lua_geti(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 				if (lua_isfunction(L, -1)) {
 					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 					lua_pushstring(L, "recv");
@@ -171,7 +171,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
         }
         case MQTT_MSG_CONNACK: {
 			if (mqtt_ctrl->mqtt_cb) {
-				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+				lua_geti(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 				if (lua_isfunction(L, -1)) {
 					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 					lua_pushstring(L, "conack");
@@ -189,7 +189,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
 		case MQTT_MSG_PUBACK:
 		case MQTT_MSG_PUBCOMP: {
 			if (mqtt_ctrl->mqtt_cb) {
-				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+				lua_geti(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 				if (lua_isfunction(L, -1)) {
 					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 					lua_pushstring(L, "sent");
@@ -208,7 +208,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
         }
 		case MQTT_MSG_CLOSE: {
 			if (mqtt_ctrl->mqtt_ref) {
-				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+				lua_geti(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 				if (lua_isfunction(L, -1)) {
 					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 					lua_pushstring(L, "close");
@@ -219,7 +219,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
         }
 		case MQTT_MSG_DISCONNECT: {
 			if (mqtt_ctrl->mqtt_cb) {
-				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+				lua_geti(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 				if (lua_isfunction(L, -1)) {
 					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 					lua_pushstring(L, "disconnect");
@@ -238,7 +238,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
         }
 		case MQTT_MSG_SUBACK:
 			if (mqtt_ctrl->mqtt_cb) {
-				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+				lua_geti(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 				if (lua_isfunction(L, -1)) {
 					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 					lua_pushstring(L, "suback");
@@ -250,7 +250,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
 			break;
 		case MQTT_MSG_UNSUBACK:
 			if (mqtt_ctrl->mqtt_cb) {
-				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+				lua_geti(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 				if (lua_isfunction(L, -1)) {
 					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 					lua_pushstring(L, "unsuback");
@@ -263,7 +263,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
 		case MQTT_MSG_CONACK_ERROR:
 		case MQTT_MSG_NET_ERROR:
 			if (mqtt_ctrl->mqtt_ref) {
-				lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+				lua_geti(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 				if (lua_isfunction(L, -1)) {
 					lua_geti(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_ref);
 					lua_pushstring(L, "error");
@@ -631,12 +631,12 @@ event可能出现的值有
 static int l_mqtt_on(lua_State *L) {
 	luat_mqtt_ctrl_t * mqtt_ctrl = get_mqtt_ctrl(L);
 	if (mqtt_ctrl->mqtt_cb != 0) {
-		luaL_unref(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+		luaL_unref(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 		mqtt_ctrl->mqtt_cb = 0;
 	}
 	if (lua_isfunction(L, 2)) {
 		lua_pushvalue(L, 2);
-		mqtt_ctrl->mqtt_cb = luaL_ref(L, LUA_REGISTRYINDEX);
+		mqtt_ctrl->mqtt_cb = (void*)(intptr_t)luaL_ref(L, LUA_REGISTRYINDEX);
 	}
 	return 0;
 }
@@ -755,7 +755,7 @@ static int l_mqtt_close(lua_State *L) {
 	mqtt_disconnect(&(mqtt_ctrl->broker));
 	luat_mqtt_close_socket(mqtt_ctrl);
 	if (mqtt_ctrl->mqtt_cb != 0) {
-		luaL_unref(L, LUA_REGISTRYINDEX, mqtt_ctrl->mqtt_cb);
+		luaL_unref(L, LUA_REGISTRYINDEX, (int)(intptr_t)mqtt_ctrl->mqtt_cb);
 		mqtt_ctrl->mqtt_cb = 0;
 	}
 	luat_mqtt_release_socket(mqtt_ctrl);

+ 47 - 5
components/network/libemqtt/luat_mqtt_client.c

@@ -75,6 +75,8 @@ static LUAT_RT_RET_TYPE conn_timer_callback(LUAT_RT_CB_PARAM){
 
 int luat_mqtt_reconnect(luat_mqtt_ctrl_t *mqtt_ctrl) {
 	mqtt_ctrl->buffer_offset = 0;
+	mqtt_ctrl->mqtt_state = MQTT_STATE_DISCONNECT;
+	LLOGI("mqtt reconnecting... %s %d", mqtt_ctrl->host, mqtt_ctrl->remote_port);
 	int ret = luat_mqtt_connect(mqtt_ctrl);
 	if(ret){
 		LLOGI("reconnect init socket ret=%d\n", ret);
@@ -375,20 +377,38 @@ further:
 	return 0;
 }
 
+static void _close_mqtt(luat_mqtt_ctrl_t *mqtt_ctrl, int code, int code2) {
+	luat_mqtt_close_socket(mqtt_ctrl);
+	l_luat_mqtt_msg_cb(mqtt_ctrl, code, code2);
+}
 
 static int luat_mqtt_msg_cb(luat_mqtt_ctrl_t *mqtt_ctrl) {
     uint8_t msg_tp = MQTTParseMessageType(mqtt_ctrl->mqtt_packet_buffer);
 	uint16_t msg_id = 0;
 	uint8_t qos = 0;
+	// 先进行状态判断
+	// 非ready状态,只能处理connack
+	if (msg_tp != MQTT_MSG_CONNACK && mqtt_ctrl->mqtt_state != MQTT_STATE_READY) {
+		LLOGE("recv msg %d when not connected", msg_tp);
+		_close_mqtt(mqtt_ctrl, MQTT_MSG_CON_ERROR, 96);
+		return -2;
+	}
+	// ready状态,不能处理connack
+	if (msg_tp == MQTT_MSG_CONNACK && mqtt_ctrl->mqtt_state == MQTT_STATE_READY) {
+		LLOGE("recv connack when already connected");
+		_close_mqtt(mqtt_ctrl, MQTT_MSG_CON_ERROR, 95);
+		return -3;
+	}
+	// 根据类型进行处理
+
     switch (msg_tp) {
 		case MQTT_MSG_CONNACK: {
 			LLOGD("MQTT_MSG_CONNACK");
 			if(mqtt_ctrl->mqtt_packet_buffer[3] != 0x00){
 				LLOGW("CONACK 0x%02x",mqtt_ctrl->mqtt_packet_buffer[3]);
 				mqtt_ctrl->error_state = mqtt_ctrl->mqtt_packet_buffer[3];
-                luat_mqtt_close_socket(mqtt_ctrl);
-                l_luat_mqtt_msg_cb(mqtt_ctrl, MQTT_MSG_CONACK_ERROR, mqtt_ctrl->mqtt_packet_buffer[3]);
-                return -1;
+				_close_mqtt(mqtt_ctrl, MQTT_MSG_CONACK_ERROR, mqtt_ctrl->mqtt_packet_buffer[3]);
+                return -4;
             }
 			mqtt_ctrl->mqtt_state = MQTT_STATE_READY;
             l_luat_mqtt_msg_cb(mqtt_ctrl, MQTT_MSG_CONNACK, 0);
@@ -399,8 +419,12 @@ static int luat_mqtt_msg_cb(luat_mqtt_ctrl_t *mqtt_ctrl) {
             break;
         }
         case MQTT_MSG_PUBLISH : {
-			LLOGD("MQTT_MSG_PUBLISH");
 			qos = MQTTParseMessageQos(mqtt_ctrl->mqtt_packet_buffer);
+			if (qos > 2) {
+				LLOGE("bad publish packet qos %d", qos);
+				_close_mqtt(mqtt_ctrl, MQTT_MSG_CON_ERROR, 98);
+				return -5;
+			}
 #ifdef __LUATOS__
 			if (mqtt_ctrl->app_cb)
 			{
@@ -413,12 +437,30 @@ static int luat_mqtt_msg_cb(luat_mqtt_ctrl_t *mqtt_ctrl) {
 			const uint8_t* ptr;
 			uint16_t topic_len = mqtt_parse_pub_topic_ptr(mqtt_ctrl->mqtt_packet_buffer, &ptr);
 			uint32_t payload_len = mqtt_parse_pub_msg_ptr(mqtt_ctrl->mqtt_packet_buffer, &ptr);
-			luat_mqtt_msg_t *mqtt_msg = (luat_mqtt_msg_t *)luat_heap_malloc(sizeof(luat_mqtt_msg_t)+topic_len+payload_len);
+			if (topic_len < 1 || payload_len < 0 || topic_len > 4096 || payload_len > 256 * 1024) {
+				LLOGE("bad publish packet topic len %d payload_len %u", topic_len, payload_len);
+				_close_mqtt(mqtt_ctrl, MQTT_MSG_CON_ERROR, 99);
+                return -6;
+			}
+			if ((topic_len + payload_len + 2) > mqtt_ctrl->buffer_offset) {
+				LLOGE("publish packet topic len %d payload_len %u exceed buffer %d", topic_len, payload_len, mqtt_ctrl->buffer_offset);
+				_close_mqtt(mqtt_ctrl, MQTT_MSG_CON_ERROR, 100);
+				return -7;
+			}
+			size_t msg_size = sizeof(luat_mqtt_msg_t) + topic_len + payload_len;
+			luat_mqtt_msg_t *mqtt_msg = (luat_mqtt_msg_t *)luat_heap_malloc(msg_size);
+			if (mqtt_msg == NULL) {
+				LLOGE("no mem for recv publish msg topic len %d payload_len %u", topic_len, payload_len);
+				_close_mqtt(mqtt_ctrl, MQTT_MSG_CON_ERROR, 99);
+                return -8;
+			}
+			else {
 				mqtt_msg->topic_len = mqtt_parse_pub_topic(mqtt_ctrl->mqtt_packet_buffer, mqtt_msg->data);
 	            mqtt_msg->payload_len = mqtt_parse_publish_msg(mqtt_ctrl->mqtt_packet_buffer, mqtt_msg->data+topic_len);
 	            mqtt_msg->message_id = mqtt_parse_msg_id(mqtt_ctrl->mqtt_packet_buffer);
 	            mqtt_msg->flags = mqtt_ctrl->mqtt_packet_buffer[0];
 	            l_luat_mqtt_msg_cb(mqtt_ctrl, MQTT_MSG_PUBLISH, (int)mqtt_msg);
+			}
 #else
 			l_luat_mqtt_msg_cb(mqtt_ctrl, MQTT_MSG_PUBLISH, 0);
 #endif

+ 23 - 12
components/network/libhttp/luat_http_client.c

@@ -110,6 +110,7 @@ int http_close(luat_http_ctrl_t *http_ctrl){
 		luat_heap_free(http_ctrl->req_auth);
 		http_ctrl->req_auth = NULL;
 	}
+	memset(http_ctrl, 0, sizeof(luat_http_ctrl_t));
 	luat_heap_free(http_ctrl);
 	return 0;
 }
@@ -247,7 +248,7 @@ static void luat_http_callback(luat_http_ctrl_t *http_ctrl){
 
 static int on_header_field(http_parser* parser, const char *at, size_t length){
 	luat_http_ctrl_t *http_ctrl =(luat_http_ctrl_t *)parser->data;
-    LLOGD("on_header_field:%.*s",length,at);
+    // LLOGD("on_header_field:%.*s",length,at);
 	if (http_ctrl->headers_complete){
 		return 0;
 	}
@@ -277,7 +278,7 @@ static int on_header_value(http_parser* parser, const char *at, size_t length){
 
 	char tmp[16] = {0};
 	luat_http_ctrl_t *http_ctrl =(luat_http_ctrl_t *)parser->data;
-	LLOGD("on_header_value:%.*s",length,at);
+	// LLOGD("on_header_value:%.*s",length,at);
 	if (http_ctrl->headers_complete){
 		if (!http_ctrl->luatos_mode) {
 			LLOGD("state %d", http_ctrl->state);
@@ -289,7 +290,7 @@ static int on_header_value(http_parser* parser, const char *at, size_t length){
 		if(http_ctrl->resp_content_len == -1){
 			memcpy(tmp, at, length);
 			http_ctrl->resp_content_len = atoi(tmp);
-			LLOGD("http_ctrl->resp_content_len:%d",http_ctrl->resp_content_len);
+			LLOGD("resp_content_len:%d",http_ctrl->resp_content_len);
 		}
 		http_ctrl->headers = luat_heap_realloc(http_ctrl->headers,http_ctrl->headers_len+length+3);
 		memcpy(http_ctrl->headers+http_ctrl->headers_len,at,length);
@@ -334,6 +335,10 @@ static int on_headers_complete(http_parser* parser){
 		}
 	#endif
 		http_ctrl->headers_complete = 1;
+		// 如果头部解析出content_length为0,直接标记body接收完成
+		if (http_ctrl->resp_content_len == 0){
+			http_ctrl->http_body_is_finally = 1;
+		}
 		luat_http_callback(http_ctrl);
 	} else {
 		if (http_ctrl->state != HTTP_STATE_GET_HEAD){
@@ -441,7 +446,7 @@ static int on_body(http_parser* parser, const char *at, size_t length){
 			http_cb(HTTP_STATE_GET_BODY, (void *)at, length, http_ctrl->http_cb_userdata);
 		}
 	}
-	if (http_ctrl->resp_content_len > 0 && http_ctrl->body_len >= http_ctrl->resp_content_len) {
+	if (http_ctrl->resp_content_len >= 0 && http_ctrl->body_len >= http_ctrl->resp_content_len) {
 		http_ctrl->http_body_is_finally = 1;
 		LLOGD("http body recv done by content_length");
 		http_close_nw(http_ctrl);
@@ -655,13 +660,13 @@ LUAT_RT_RET_TYPE luat_http_timer_callback(LUAT_RT_CB_PARAM){
 }
 
 static void on_tcp_closed(luat_http_ctrl_t *http_ctrl) {
-	LLOGI("on_tcp_closed %p body is done %d header is complete %d", http_ctrl, http_ctrl->http_body_is_finally, http_ctrl->headers_complete);
+	LLOGD("on_tcp_closed %p body is done %d header is complete %d", http_ctrl, http_ctrl->http_body_is_finally, http_ctrl->headers_complete);
 	int ret = 0;
 	http_ctrl->tcp_closed = 1;
 	if (http_ctrl->http_body_is_finally == 0) { // 当没有解析完成
 		// 存在多种可能性
 		// 1. 没有content_length的情况, 又没有chunked
-		if (http_ctrl->resp_content_len == 0 && http_ctrl->headers_complete) {
+		if (http_ctrl->resp_content_len >= 0 && http_ctrl->headers_complete) {
 			http_ctrl->http_body_is_finally = 1;
 		}
 	}
@@ -690,6 +695,10 @@ int32_t luat_lib_http_callback(void *data, void *param){
 	OS_EVENT *event = (OS_EVENT *)data;
 	luat_http_ctrl_t *http_ctrl =(luat_http_ctrl_t *)param;
 	int ret = 0;
+	if (http_ctrl == NULL){
+		LLOGE("http_ctrl is NULL");
+		return -1;
+	}
 	if (!http_ctrl->luatos_mode) {
 	    if (HTTP_STATE_IDLE == http_ctrl->state)
 	    {
@@ -703,7 +712,7 @@ int32_t luat_lib_http_callback(void *data, void *param){
 	}
 
 	//LLOGD("LINK %d ON_LINE %d EVENT %d TX_OK %d CLOSED %d",EV_NW_RESULT_LINK & 0x0fffffff,EV_NW_RESULT_CONNECT & 0x0fffffff,EV_NW_RESULT_EVENT & 0x0fffffff,EV_NW_RESULT_TX & 0x0fffffff,EV_NW_RESULT_CLOSE & 0x0fffffff);
-	LLOGD("luat_lib_http_callback %08X %d %p",event->ID - EV_NW_RESULT_BASE, event->Param1, http_ctrl);
+	LLOGD("nw cb %08X %d %p", event->ID - EV_NW_RESULT_BASE, event->Param1, http_ctrl);
 	if (event->Param1){
 		//LLOGD("LINK %d ON_LINE %d EVENT %d TX_OK %d CLOSED %d",EV_NW_RESULT_LINK & 0x0fffffff,EV_NW_RESULT_CONNECT & 0x0fffffff,EV_NW_RESULT_EVENT & 0x0fffffff,EV_NW_RESULT_TX & 0x0fffffff,EV_NW_RESULT_CLOSE & 0x0fffffff);
 		LLOGE("error event %08X %d host %s port %d",event->ID - EV_NW_RESULT_BASE, event->Param1, http_ctrl->netc->domain_name, http_ctrl->netc->remote_port);
@@ -1283,7 +1292,9 @@ int luat_http_client_start(luat_http_ctrl_t *http_ctrl, const char *url, uint8_t
 
 int http_set_url(luat_http_ctrl_t *http_ctrl, const char* url, const char* method) {
 	const char *tmp = url;
-	if (strcmp("POST", method) != 0 && strcmp("GET", method) != 0 && strcmp("PUT", method) != 0){
+	if (strcmp("POST", method) != 0 && strcmp("GET", method) != 0 
+		&& strcmp("PUT", method) != 0 && strcmp("DELETE", method) != 0
+		&& strcmp("PATCH", method) != 0) {
 		LLOGE("NOT SUPPORT %s",method);
 		return -1;
 	}
@@ -1296,13 +1307,13 @@ int http_set_url(luat_http_ctrl_t *http_ctrl, const char* url, const char* metho
         tmp += strlen("http://");
     }
     else {
-        LLOGI("only http/https supported %s", url);
+        LLOGE("only http/https supported %s", url);
         return -1;
     }
 
 	size_t tmplen = strlen(tmp);
 	if (tmplen < 5) {
-        LLOGI("url too short %s", url);
+        LLOGE("url too short %s", url);
         return -1;
     }
 	#define HOST_MAX_LEN (256)
@@ -1313,7 +1324,7 @@ int http_set_url(luat_http_ctrl_t *http_ctrl, const char* url, const char* metho
     for (size_t i = 0; i < tmplen; i++){
         if (tmp[i] == '/') {
 			if (i > 255) {
-				LLOGI("host too long %s", url);
+				LLOGE("host too long %s", url);
 				return -1;
 			}
             tmpuri = tmp + i;
@@ -1325,7 +1336,7 @@ int http_set_url(luat_http_ctrl_t *http_ctrl, const char* url, const char* metho
 		tmphost[i] = tmp[i];
     }
 	if (strlen(tmphost) < 1) {
-        LLOGI("host not found %s", url);
+        LLOGE("host not found %s", url);
         return -1;
     }
     if (strlen(tmpuri) == 0) {

+ 12 - 7
components/network/libhttp/luat_lib_http.c

@@ -35,7 +35,7 @@ end)
 #define LUAT_HTTP_DEBUG 0
 #endif
 #undef LLOGD
-#define LLOGD(...) if (http_ctrl->debug_onoff) LLOGI(__VA_ARGS__)
+#define LLOGD(fmt, ...) if (http_ctrl->debug_onoff) LLOGI("[%llx]" fmt, http_ctrl->idp, ##__VA_ARGS__)
 
 
 int http_close(luat_http_ctrl_t *http_ctrl);
@@ -342,6 +342,7 @@ static int l_http_request(lua_State *L) {
 
 	network_set_ip_invaild(&http_ctrl->ip_addr);
 	http_ctrl->idp = luat_pushcwait(L);
+	LLOGI("http idp:%llx", http_ctrl->idp);
 
     if (luat_http_client_start_luatos(http_ctrl)) {
         goto error;
@@ -358,13 +359,13 @@ error:
 }
 
 #include "rotable2.h"
-const rotable_Reg_t reg_http[] =
+static const rotable_Reg_t reg_http[] =
 {
 	{"request",			ROREG_FUNC(l_http_request)},
 	{ NULL,             ROREG_INT(0)}
 };
 
-const rotable_Reg_t reg_http_emtry[] =
+static const rotable_Reg_t reg_http_emtry[] =
 {
 	{ NULL,             ROREG_INT(0)}
 };
@@ -391,8 +392,13 @@ int32_t l_http_callback(lua_State *L, void* ptr){
 
     rtos_msg_t* msg = (rtos_msg_t*)lua_topointer(L, -1);
     luat_http_ctrl_t *http_ctrl =(luat_http_ctrl_t *)msg->ptr;
+	if (http_ctrl == NULL){
+		LLOGE("http callback http_ctrl is NULL");
+		return 0;
+	}
 	uint64_t idp = http_ctrl->idp;
-	LLOGI("l_http_callback arg1:%d is_download:%d idp:%d",msg->arg1,http_ctrl->is_download,idp);
+	LLOGD("cb arg1:%d is_download:%d idp:%llx",msg->arg1,http_ctrl->is_download,idp);
+	LLOGD("cb status_code:%d resp_content_len:%d",http_ctrl->parser.status_code,http_ctrl->resp_content_len);
 	if (msg->arg1!=0 && msg->arg1!=HTTP_ERROR_FOTA ){
 		if (msg->arg1 == HTTP_CALLBACK){
 			lua_geti(L, LUA_REGISTRYINDEX, (int)http_ctrl->http_cb);
@@ -427,8 +433,7 @@ int32_t l_http_callback(lua_State *L, void* ptr){
 		temp = strstr(value,"\r\n")+2;
 		header_len = (uint16_t)(value-header)-1;
 		value_len = (uint16_t)(temp-value)-2;
-		LLOGD("header:%.*s",header_len,header);
-		LLOGD("value:%.*s",value_len,value);
+		LLOGD("header: [%.*s]:[%.*s]",header_len,header,value_len,value);
 		lua_pushlstring(L, header,header_len);
 		lua_pushlstring(L, value,value_len);
 		lua_settable(L, -3);
@@ -483,7 +488,7 @@ exit:
 }
 
 void luat_http_client_onevent(luat_http_ctrl_t *http_ctrl, int error_code, int arg) {
-	LLOGD("luat_http_client_onevent %p %d", http_ctrl, error_code);
+	LLOGD("onevent %p %d", http_ctrl, error_code);
 	if (!http_ctrl->luatos_mode) return;
 	if (http_ctrl->timeout_timer && error_code != HTTP_CALLBACK){
 		luat_stop_rtos_timer(http_ctrl->timeout_timer);

+ 41 - 9
components/network/netdrv/binding/luat_lib_netdrv.c

@@ -17,6 +17,7 @@
 #include "luat_rtos.h"
 #include "luat_netdrv.h"
 #include "luat_netdrv_napt.h"
+#include "luat_netdrv_drv.h"
 #include "luat_network_adapter.h"
 #include "luat_netdrv_event.h"
 #include "net_lwip2.h"
@@ -30,9 +31,9 @@
 /*
 初始化指定netdrv设备
 @api netdrv.setup(id, tp, opts)
-@int 网络适配器编号, 例如 socket.LWIP_ETH
-@int 实现方式,如果是设备自带的硬件,那就不需要传, 外挂设备需要传,当前支持CH390H/D
-@int 外挂方式,需要额外的参数,参考示例
+@int 网络适配器编号, 例如 socket.LWIP_ETH, socket.LWIP_USER0
+@int 实现方式,如果是设备自带的硬件,那就不需要传, 外挂设备需要传,当前支持CH390H/D/OPENVPN等
+@table 外挂方式,需要额外的参数,参考示例
 @return boolean 初始化成功与否
 @usage
 -- Air8101初始化内部以太网控制器
@@ -114,6 +115,34 @@ static int l_netdrv_setup(lua_State *L) {
         };
         lua_pop(L, 1);
         #endif
+
+        #ifdef LUAT_USE_NETDRV_OPENVPN
+        // OpenVPN的配置参数
+        if (lua_getfield(L, 3, "ovpn_remote_ip") == LUA_TSTRING) {
+            conf.ovpn_remote_ip = luaL_checklstring(L, -1, &len);
+        };
+        lua_pop(L, 1);
+        if (lua_getfield(L, 3, "ovpn_remote_port") == LUA_TNUMBER) {
+            conf.ovpn_remote_port = luaL_checkinteger(L, -1);
+        };
+        lua_pop(L, 1);
+        if (lua_getfield(L, 3, "ovpn_ca_cert") == LUA_TSTRING) {
+            conf.ovpn_ca_cert = luaL_checklstring(L, -1, &conf.ovpn_ca_cert_len);
+        };
+        lua_pop(L, 1);
+        if (lua_getfield(L, 3, "ovpn_client_cert") == LUA_TSTRING) {
+            conf.ovpn_client_cert = luaL_checklstring(L, -1, &conf.ovpn_client_cert_len);
+        };
+        lua_pop(L, 1);
+        if (lua_getfield(L, 3, "ovpn_client_key") == LUA_TSTRING) {
+            conf.ovpn_client_key = luaL_checklstring(L, -1, &conf.ovpn_client_key_len);
+        };
+        lua_pop(L, 1);
+        if (lua_getfield(L, 3, "ovpn_static_key") == LUA_TSTRING) {
+            conf.ovpn_static_key = (const uint8_t *)luaL_checklstring(L, -1, &conf.ovpn_static_key_len);
+        };
+        lua_pop(L, 1);
+        #endif
     }
     luat_netdrv_t* ret = luat_netdrv_setup(&conf);
     lua_pushboolean(L, ret != NULL);
@@ -658,18 +687,21 @@ static const rotable_Reg_t reg_netdrv[] =
 #endif
 
     //@const CH390 number 南京沁恒CH390系列,支持CH390D/CH390H, SPI通信
-    { "CH390",          ROREG_INT(1)},
-    { "UART",           ROREG_INT(16)}, // UART形式的网卡, 不带MAC, 直接IP包
+    { "CH390",          ROREG_INT(LUAT_NETDRV_IMPL_CH390H)},
+    { "UART",           ROREG_INT(LUAT_NETDRV_IMPL_UART)}, // UART形式的网卡, 不带MAC, 直接IP包
     #ifdef LUAT_USE_NETDRV_WG
-    { "WG",             ROREG_INT(32)}, // Wireguard VPN网卡
+    { "WG",             ROREG_INT(LUAT_NETDRV_IMPL_WG)}, // Wireguard VPN网卡
     #endif
     //@const WHALE number 虚拟网卡
-    { "WHALE",          ROREG_INT(64)}, // 通用WHALE设备
+    { "WHALE",          ROREG_INT(LUAT_NETDRV_IMPL_WHALE)}, // 通用WHALE设备
+    #ifdef LUAT_USE_NETDRV_OPENVPN
+    { "OPENVPN",        ROREG_INT(LUAT_NETDRV_IMPL_OPENVPN)}, // OpenVPN虚拟网卡
+    #endif
 
     //@const CTRL_RESET number 控制类型-复位,当前仅支持CH390H
     { "CTRL_RESET",     ROREG_INT(LUAT_NETDRV_CTRL_RESET)},
-    //@const CTRL_DOWN number 控制类型-关闭CH390H通信并下电PHY,0重新启动,1关闭
-    { "CTRL_DOWN",      ROREG_INT(LUAT_NETDRV_CTRL_DOWN)},
+    //@const CTRL_UPDOWN number 控制类型-1=启动UP,0关闭DOWN
+    { "CTRL_UPDOWN",    ROREG_INT(LUAT_NETDRV_CTRL_UPDOWN)},
     //@const RESET_HARD number 请求对网卡硬复位,当前仅支持CH390H
     { "RESET_HARD",     ROREG_INT(0x101)},
     //@const RESET_SOFT number 请求对网卡软复位,当前仅支持CH390H

+ 15 - 2
components/network/netdrv/include/luat_netdrv.h

@@ -28,7 +28,7 @@ enum {
 
 enum {
     LUAT_NETDRV_CTRL_RESET,
-    LUAT_NETDRV_CTRL_DOWN,
+    LUAT_NETDRV_CTRL_UPDOWN,
 };
 
 typedef struct luat_netdrv_conf
@@ -42,7 +42,7 @@ typedef struct luat_netdrv_conf
     uint16_t mtu;
     uint8_t flags;
     // Wireguard相关参数
-    #ifdef LUAT_USE_NETDRV_WG
+    #if 1
     const char* wg_private_key;
     uint16_t wg_listen_port;
     uint16_t wg_keepalive;
@@ -52,6 +52,19 @@ typedef struct luat_netdrv_conf
     const char* wg_endpoint_ip;
     uint16_t wg_endpoint_port;
     #endif
+    // OpenVPN相关参数
+    #if 1
+    const char* ovpn_remote_ip;     // VPN服务器IP地址
+    uint16_t ovpn_remote_port;      // VPN服务器端口
+    const char* ovpn_ca_cert;       // CA证书 (PEM格式)
+    size_t ovpn_ca_cert_len;
+    const char* ovpn_client_cert;   // 客户端证书 (PEM格式)
+    size_t ovpn_client_cert_len;
+    const char* ovpn_client_key;    // 客户端私钥 (PEM格式)
+    size_t ovpn_client_key_len;
+    const uint8_t* ovpn_static_key; // 静态密钥 (可选)
+    size_t ovpn_static_key_len;
+    #endif
 }luat_netdrv_conf_t;
 
 typedef struct luat_netdrv_statics_item

+ 18 - 0
components/network/netdrv/include/luat_netdrv_drv.h

@@ -0,0 +1,18 @@
+#ifndef LUAT_NETDRV_DRV_H
+#define LUAT_NETDRV_DRV_H
+
+#include "luat_netdrv.h"
+
+#define LUAT_NETDRV_IMPL_CH390H 1
+#define LUAT_NETDRV_IMPL_UART   2
+#define LUAT_NETDRV_IMPL_WHALE  3
+#define LUAT_NETDRV_IMPL_WG     4
+#define LUAT_NETDRV_IMPL_OPENVPN 5
+
+luat_netdrv_t* luat_netdrv_ch390h_setup(luat_netdrv_conf_t *conf);
+luat_netdrv_t* luat_netdrv_uart_setup(luat_netdrv_conf_t *conf);
+luat_netdrv_t* luat_netdrv_whale_setup(luat_netdrv_conf_t *conf);
+luat_netdrv_t* luat_netdrv_wg_setup(luat_netdrv_conf_t *conf);
+luat_netdrv_t* luat_netdrv_openvpn_setup(luat_netdrv_conf_t *conf);
+
+#endif // !LUAT_NETDRV_DRV_H

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

@@ -128,6 +128,4 @@ int luat_netdrv_napt_init_contexts(void);
 void luat_netdrv_napt_enable(int adapter_id);
 void luat_netdrv_napt_disable(void);
 
-#define NAPT_DEBUG 1
-
 #endif

+ 21 - 0
components/network/netdrv/include/luat_netdrv_openvpn.h

@@ -0,0 +1,21 @@
+#ifndef __LUAT_NETDRV_OPENVPN_H__
+#define __LUAT_NETDRV_OPENVPN_H__
+
+#include "luat_netdrv.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * OpenVPN netdrv 初始化函数
+ * @param conf netdrv 配置结构体指针
+ * @return 成功返回 luat_netdrv_t 指针,失败返回 NULL
+ */
+luat_netdrv_t* luat_netdrv_openvpn_setup(luat_netdrv_conf_t *conf);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __LUAT_NETDRV_OPENVPN_H__ */

+ 119 - 0
components/network/netdrv/include/luat_netdrv_openvpn_client.h

@@ -0,0 +1,119 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+#include "lwip/ip_addr.h"
+#include "lwip/netif.h"
+#include "lwip/pbuf.h"
+#include "lwip/udp.h"
+#include "mbedtls/ssl.h"
+#include "mbedtls/x509_crt.h"
+#include "mbedtls/pk.h"
+#include "mbedtls/ctr_drbg.h"
+#include "mbedtls/entropy.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* Event types for OpenVPN client state */
+typedef enum {
+    OVPN_EVENT_CONNECTED = 0,      /* Connection established (static key mode) */
+    OVPN_EVENT_TLS_HANDSHAKE_OK,   /* TLS/DTLS handshake succeeded */
+    OVPN_EVENT_TLS_HANDSHAKE_FAIL, /* TLS/DTLS handshake failed */
+    OVPN_EVENT_KEEPALIVE_TIMEOUT,  /* Keepalive timeout (30s no response) */
+    OVPN_EVENT_AUTH_FAILED,        /* HMAC authentication failed */
+    OVPN_EVENT_DISCONNECTED,       /* Connection closed */
+    OVPN_EVENT_DATA_RX,            /* Data packet received (optional, for activity indication) */
+    OVPN_EVENT_DATA_TX,            /* Data packet sent (optional, for activity indication) */
+} ovpn_event_t;
+
+/* Event callback function type */
+typedef void (*ovpn_event_cb_t)(ovpn_event_t event, void *user_data);
+
+typedef struct {
+    const char *remote_host;      // optional; if NULL, remote_ip must be set
+    ip_addr_t   remote_ip;        // required for now (UDP only)
+    uint16_t    remote_port;      // server port
+    uint8_t     adapter_index;    // defaults to NW_ADAPTER_INDEX_LWIP_USER0
+    uint16_t    tun_mtu;          // defaults to 1500
+    const uint8_t *static_key;    // optional static key material
+    size_t      static_key_len;   // up to 64 bytes stored
+    const char *ca_cert_pem;      // CA certificate (PEM)
+    size_t      ca_cert_len;
+    const char *client_cert_pem;  // client certificate (PEM)
+    size_t      client_cert_len;
+    const char *client_key_pem;   // client private key (PEM)
+    size_t      client_key_len;
+    ovpn_event_cb_t event_cb;     // Event callback function (optional)
+    void       *user_data;        // User-defined data passed to callback
+} ovpn_client_cfg_t;
+
+typedef struct {
+    uint64_t tx_pkts;
+    uint64_t tx_bytes;
+    uint64_t rx_pkts;
+    uint64_t rx_bytes;
+    uint64_t drop_auth;
+    uint64_t drop_replay;
+    uint64_t drop_malformed;
+    uint64_t ping_sent;
+    uint64_t ping_recv;
+} ovpn_client_stats_t;
+
+typedef struct ovpn_client {
+    struct netif netif;
+    struct udp_pcb *udp;
+    ip_addr_t remote_ip;
+    uint16_t remote_port;
+    uint16_t mtu;
+    uint8_t adapter_index;
+    uint8_t started;
+    uint32_t tx_seq;
+    uint32_t rx_max_seq;
+    uint32_t rx_window;
+    uint8_t rx_initialized;
+    uint32_t last_activity_ms;
+    uint32_t last_ping_ms;
+    uint8_t key[64];  /* OVPN_MAX_KEY_LEN */
+    size_t key_len;
+    ovpn_client_stats_t stats;
+    uint8_t debug;
+    uint8_t use_tls;
+    uint8_t tls_ready;
+    mbedtls_ssl_context ssl;
+    mbedtls_ssl_config conf;
+    mbedtls_x509_crt ca;
+    mbedtls_x509_crt client_cert;
+    mbedtls_pk_context client_key;
+    mbedtls_ctr_drbg_context drbg;
+    mbedtls_entropy_context entropy;
+    struct {
+        struct pbuf *pkt;    /* pending UDP packet for DTLS read */
+        uint16_t offset;
+    } rx_pending;
+    struct {
+        uint32_t int_ms;
+        uint32_t fin_ms;
+    } dtls_timer;
+    uint8_t *tls_buf;        /* Pre-allocated TLS temporary buffer (1600 bytes) */
+    ovpn_event_cb_t event_cb; /* Event callback function */
+    void *user_data;         /* User-defined data */
+    /* Certificate data copies (copied from Lua stack to prevent garbage collection) */
+    uint8_t *ca_cert_buf;    /* CA certificate copy */
+    size_t ca_cert_len;
+    uint8_t *client_cert_buf; /* Client certificate copy */
+    size_t client_cert_len;
+    uint8_t *client_key_buf; /* Client private key copy */
+    size_t client_key_len;
+} ovpn_client_t;
+
+int ovpn_client_init(ovpn_client_t *cli, const ovpn_client_cfg_t *cfg);
+int ovpn_client_start(ovpn_client_t *cli);
+void ovpn_client_stop(ovpn_client_t *cli);
+void ovpn_client_get_stats(ovpn_client_t *cli, ovpn_client_stats_t *out);
+void ovpn_client_set_debug(ovpn_client_t *cli, int enable);
+
+#ifdef __cplusplus
+}
+#endif

+ 10 - 8
components/network/netdrv/src/luat_netdrv.c

@@ -5,6 +5,7 @@
 #include "luat_mcu.h"
 #include "lwip/ip.h"
 #include "lwip/tcpip.h"
+#include "luat_netdrv_drv.h"
 
 #ifdef LUAT_USE_AIRLINK
 #include "luat_airlink.h"
@@ -17,11 +18,6 @@ static luat_netdrv_t* drvs[NW_ADAPTER_QTY];
 
 uint32_t g_netdrv_debug_enable;
 
-luat_netdrv_t* luat_netdrv_ch390h_setup(luat_netdrv_conf_t *conf);
-luat_netdrv_t* luat_netdrv_uart_setup(luat_netdrv_conf_t *conf);
-luat_netdrv_t* luat_netdrv_whale_setup(luat_netdrv_conf_t *conf);
-luat_netdrv_t* luat_netdrv_wg_setup(luat_netdrv_conf_t *conf);
-
 luat_netdrv_t* luat_netdrv_setup(luat_netdrv_conf_t *conf) {
     int id = conf->id;
     if (id < 0 || id >= NW_ADAPTER_QTY) {
@@ -32,23 +28,29 @@ luat_netdrv_t* luat_netdrv_setup(luat_netdrv_conf_t *conf) {
         // 注册新的设备?
         #ifdef __LUATOS__
         #ifdef LUAT_USE_NETDRV_CH390H
-        if (conf->impl == 1) { // CH390H
+        if (conf->impl == LUAT_NETDRV_IMPL_CH390H) { // CH390H
             drvs[id] = luat_netdrv_ch390h_setup(conf);
             return drvs[id];
         }
         #endif
         #ifdef LUAT_USE_AIRLINK
-        if (conf->impl == 64) { // WHALE
+        if (conf->impl == LUAT_NETDRV_IMPL_WHALE) { // WHALE
             drvs[id] = luat_netdrv_whale_setup(conf);
             return drvs[id];
         }
         #endif
         #ifdef LUAT_USE_NETDRV_WG
-        if (conf->impl == 32) { // WG
+        if (conf->impl == LUAT_NETDRV_IMPL_WG) { // WG
             drvs[id] = luat_netdrv_wg_setup(conf);
             return drvs[id];
         }
         #endif
+        #ifdef LUAT_USE_NETDRV_OPENVPN
+        if (conf->impl == LUAT_NETDRV_IMPL_OPENVPN) { // OPENVPN
+            drvs[id] = luat_netdrv_openvpn_setup(conf);
+            return drvs[id];
+        }
+        #endif
         #endif
     }
     else {

+ 6 - 6
components/network/netdrv/src/luat_netdrv_ch390h.c

@@ -73,15 +73,15 @@ static int ch390h_ctrl(luat_netdrv_t* drv, void* userdata, int cmd, void* buff,
                 return -3;
             }
             return 0;
-        case LUAT_NETDRV_CTRL_DOWN: {
-            int stop = buff ? (int)(uintptr_t)buff : 1;
-            if (stop) {
+        case LUAT_NETDRV_CTRL_UPDOWN: {
+            int updown = (int)buff;
+            if (updown == 0) {
                 ch->status = CH390H_STATUS_STOPPED;
                 ch->init_done = 0;
                 luat_ch390h_set_rx(ch, 0);
                 luat_ch390h_set_phy(ch, 0);
                 luat_netdrv_set_link_updown(drv, 0);
-                tcpip_callback_with_block((tcpip_callback_fn)ch390h_netif_down_cb, drv->netif, 1);
+                //tcpip_callback_with_block((tcpip_callback_fn)ch390h_netif_down_cb, drv->netif, 1);
                 LLOGI("adapter %d stopped and phy down", ch->adapter_id);
             }
             else {
@@ -90,8 +90,8 @@ static int ch390h_ctrl(luat_netdrv_t* drv, void* userdata, int cmd, void* buff,
                 ch->rx_error_count = 0;
                 ch->tx_busy_count = 0;
                 ch->vid_pid_error_count = 0;
-                luat_netdrv_set_link_updown(drv, 0);
-                tcpip_callback_with_block((tcpip_callback_fn)ch390h_netif_down_cb, drv->netif, 1);
+                luat_netdrv_set_link_updown(drv, 1);
+                //tcpip_callback_with_block((tcpip_callback_fn)ch390h_netif_down_cb, drv->netif, 1);
                 LLOGI("adapter %d restart requested", ch->adapter_id);
             }
             return 0;

+ 0 - 0
components/network/netdrv/src/ch390h_api.c → components/network/netdrv/src/luat_netdrv_ch390h_api.c


+ 0 - 0
components/network/netdrv/src/ch390h_task.c → components/network/netdrv/src/luat_netdrv_ch390h_task.c


+ 311 - 0
components/network/netdrv/src/luat_netdrv_openvpn.c

@@ -0,0 +1,311 @@
+/**
+ * OpenVPN netdrv 适配层
+ * 将 OpenVPN 客户端集成到 netdrv 框架中
+ */
+
+#include "luat_base.h"
+#include "luat_netdrv.h"
+#include "luat_netdrv_openvpn.h"
+#include "luat_mem.h"
+#include "luat_mcu.h"
+#include "luat_crypto.h"
+#include "lwip/netif.h"
+#include "lwip/tcpip.h"
+#include "net_lwip2.h"
+
+/* OpenVPN 客户端头文件 */
+#include "luat_netdrv_openvpn_client.h"
+
+#define LUAT_LOG_TAG "openvpn_netdrv"
+#include "luat_log.h"
+
+/**
+ * OpenVPN netdrv 私有数据结构
+ */
+typedef struct {
+    ovpn_client_t *client;
+    luat_netdrv_conf_t *conf;
+} luat_netdrv_openvpn_ctx_t;
+
+/**
+ * OpenVPN 事件回调转发给 netdrv 框架
+ */
+static void ovpn_netdrv_event_callback(ovpn_event_t event, void *user_data) {
+    luat_netdrv_t *drv = (luat_netdrv_t *)user_data;
+    
+    if (drv == NULL) {
+        return;
+    }
+    
+    const char *event_name = NULL;
+    
+    switch (event) {
+        case OVPN_EVENT_CONNECTED:
+            event_name = "OPENVPN_CONNECTED";
+            LLOGI("[%d] OpenVPN connected", drv->id);
+            break;
+        case OVPN_EVENT_TLS_HANDSHAKE_OK:
+            event_name = "OPENVPN_TLS_HANDSHAKE_OK";
+            LLOGI("[%d] OpenVPN TLS handshake successful", drv->id);
+            break;
+        case OVPN_EVENT_TLS_HANDSHAKE_FAIL:
+            event_name = "OPENVPN_TLS_HANDSHAKE_FAIL";
+            LLOGE("[%d] OpenVPN TLS handshake failed", drv->id);
+            break;
+        case OVPN_EVENT_KEEPALIVE_TIMEOUT:
+            event_name = "OPENVPN_KEEPALIVE_TIMEOUT";
+            LLOGW("[%d] OpenVPN keepalive timeout", drv->id);
+            break;
+        case OVPN_EVENT_AUTH_FAILED:
+            event_name = "OPENVPN_AUTH_FAILED";
+            LLOGE("[%d] OpenVPN authentication failed", drv->id);
+            break;
+        case OVPN_EVENT_DISCONNECTED:
+            event_name = "OPENVPN_DISCONNECTED";
+            LLOGI("[%d] OpenVPN disconnected", drv->id);
+            break;
+        case OVPN_EVENT_DATA_RX:
+            // 数据接收事件,通常不需要日志
+            return;
+        case OVPN_EVENT_DATA_TX:
+            // 数据发送事件,通常不需要日志
+            return;
+        default:
+            LLOGW("[%d] Unknown OpenVPN event: %d", drv->id, event);
+            return;
+    }
+    
+    // 可以在这里添加进一步的处理,比如事件分发到上层
+    // luat_netdrv_notify_event(drv->id, event_name);
+    if (event_name) {
+        #if 0
+        rtos_msg_t msg = {0};
+        msg.handler = l_netdrv_openvpn_handler;
+        msg.arg1 = drv->id;
+        msg.arg2 = event;
+        msg.ptr = (void *)event_name;
+        luat_msgbus_put(&msg, 0);
+        #endif
+    }
+}
+
+/**
+ * OpenVPN netdrv boot 函数(启动网络设备)
+ */
+static int openvpn_boot(luat_netdrv_t *drv, void *userdata) {
+    if (drv == NULL || drv->userdata == NULL) {
+        LLOGE("Invalid OpenVPN netdrv");
+        return -1;
+    }
+    
+    luat_netdrv_openvpn_ctx_t *ctx = (luat_netdrv_openvpn_ctx_t *)drv->userdata;
+    ovpn_client_t *client = ctx->client;
+    
+    if (client == NULL) {
+        LLOGE("[%d] OpenVPN client not initialized", drv->id);
+        return -1;
+    }
+    
+    int ret = ovpn_client_start(client);
+    if (ret != 0) {
+        LLOGE("[%d] OpenVPN client start failed: %d", drv->id, ret);
+        return ret;
+    }
+    
+    LLOGI("[%d] OpenVPN client started successfully", drv->id);
+    return 0;
+}
+
+/**
+ * OpenVPN netdrv shutdown 函数(关闭网络设备)
+ */
+static int openvpn_shutdown(luat_netdrv_t *drv, void *userdata) {
+    if (drv == NULL || drv->userdata == NULL) {
+        return -1;
+    }
+    
+    luat_netdrv_openvpn_ctx_t *ctx = (luat_netdrv_openvpn_ctx_t *)drv->userdata;
+    ovpn_client_t *client = ctx->client;
+    
+    if (client != NULL) {
+        ovpn_client_stop(client);
+        LLOGI("[%d] OpenVPN client stopped", drv->id);
+    }
+    
+    return 0;
+}
+
+/**
+ * OpenVPN netdrv ready 函数(检查是否准备就绪)
+ */
+static int openvpn_ready(luat_netdrv_t *drv, void *userdata) {
+    if (drv == NULL || drv->netif == NULL) {
+        return 0;
+    }
+    
+    // 检查网络接口是否已启用且有有效 IP
+    return netif_is_link_up(drv->netif) && !ip_addr_isany(&drv->netif->ip_addr);
+}
+
+/**
+ * OpenVPN netdrv DHCP 函数(不适用于 OpenVPN)
+ */
+static int openvpn_dhcp(luat_netdrv_t *drv, void *userdata, int enable) {
+    LLOGW("[%d] OpenVPN does not support DHCP", drv->id);
+    return -1;
+}
+
+/**
+ * OpenVPN netdrv debug 函数(调试输出)
+ */
+static int openvpn_debug(luat_netdrv_t *drv, void *userdata, int enable) {
+    if (drv == NULL || drv->userdata == NULL) {
+        return -1;
+    }
+    
+    luat_netdrv_openvpn_ctx_t *ctx = (luat_netdrv_openvpn_ctx_t *)drv->userdata;
+    ovpn_client_t *client = ctx->client;
+    
+    if (client != NULL) {
+        ovpn_client_set_debug(client, enable);
+        LLOGD("[%d] OpenVPN debug %s", drv->id, enable ? "enabled" : "disabled");
+    }
+    
+    return 0;
+}
+
+/**
+ * OpenVPN netdrv 初始化设置
+ * @param conf netdrv 配置结构体指针
+ * @return 成功返回 luat_netdrv_t 指针,失败返回 NULL
+ */
+luat_netdrv_t* luat_netdrv_openvpn_setup(luat_netdrv_conf_t *conf) {
+    if (conf == NULL) {
+        LLOGE("Invalid configuration");
+        return NULL;
+    }
+    
+    LLOGI("Setting up OpenVPN netdrv for adapter %d", conf->id);
+    
+    // 分配 netdrv 结构体内存
+    luat_netdrv_t *drv = (luat_netdrv_t *)luat_heap_malloc(sizeof(luat_netdrv_t));
+    if (drv == NULL) {
+        LLOGE("Failed to allocate memory for netdrv");
+        return NULL;
+    }
+    memset(drv, 0, sizeof(luat_netdrv_t));
+    
+    // 分配 OpenVPN 上下文内存
+    luat_netdrv_openvpn_ctx_t *ctx = (luat_netdrv_openvpn_ctx_t *)luat_heap_malloc(sizeof(luat_netdrv_openvpn_ctx_t));
+    if (ctx == NULL) {
+        LLOGE("Failed to allocate memory for OpenVPN context");
+        luat_heap_free(drv);
+        return NULL;
+    }
+    memset(ctx, 0, sizeof(luat_netdrv_openvpn_ctx_t));
+    
+    // 分配 OpenVPN 客户端内存
+    ovpn_client_t *client = (ovpn_client_t *)luat_heap_malloc(sizeof(ovpn_client_t));
+    if (client == NULL) {
+        LLOGE("Failed to allocate memory for OpenVPN client");
+        luat_heap_free(ctx);
+        luat_heap_free(drv);
+        return NULL;
+    }
+    memset(client, 0, sizeof(ovpn_client_t));
+    
+    // 初始化 OpenVPN 配置
+    ovpn_client_cfg_t ovpn_cfg = {0};
+    
+    // 从 netdrv 配置提取 OpenVPN 相关参数并拷贝
+    // 注意:Lua 脚本调用完成后,参数内存会被释放,所以需要拷贝到 ovpn_client 内部
+    
+    // 拷贝 CA 证书
+    if (conf->ovpn_ca_cert != NULL && conf->ovpn_ca_cert_len > 0) {
+        ovpn_cfg.ca_cert_pem = conf->ovpn_ca_cert;
+        ovpn_cfg.ca_cert_len = conf->ovpn_ca_cert_len;
+    }
+    
+    // 拷贝客户端证书
+    if (conf->ovpn_client_cert != NULL && conf->ovpn_client_cert_len > 0) {
+        ovpn_cfg.client_cert_pem = conf->ovpn_client_cert;
+        ovpn_cfg.client_cert_len = conf->ovpn_client_cert_len;
+    }
+    
+    // 拷贝客户端私钥
+    if (conf->ovpn_client_key != NULL && conf->ovpn_client_key_len > 0) {
+        ovpn_cfg.client_key_pem = conf->ovpn_client_key;
+        ovpn_cfg.client_key_len = conf->ovpn_client_key_len;
+    }
+    
+    // 拷贝静态密钥(可选)
+    if (conf->ovpn_static_key != NULL && conf->ovpn_static_key_len > 0) {
+        ovpn_cfg.static_key = conf->ovpn_static_key;
+        ovpn_cfg.static_key_len = conf->ovpn_static_key_len;
+    }
+    
+    // 设置远程服务器 IP 地址
+    if (conf->ovpn_remote_ip != NULL) {
+        // 从字符串 IP 地址转换为 ip_addr_t
+        // 这里需要在 ovpn_client_init 中处理
+        ovpn_cfg.remote_host = conf->ovpn_remote_ip;
+    }
+    
+    // 设置远程服务器端口
+    if (conf->ovpn_remote_port > 0) {
+        ovpn_cfg.remote_port = conf->ovpn_remote_port;
+    } else {
+        ovpn_cfg.remote_port = 1194;  // OpenVPN 默认端口
+    }
+    
+    // 设置默认适配器索引
+    ovpn_cfg.adapter_index = conf->id;
+    
+    // 设置 MTU(可从 conf 中读取,默认 1500)
+    if (conf->mtu > 0) {
+        ovpn_cfg.tun_mtu = conf->mtu;
+    } else {
+        ovpn_cfg.tun_mtu = 1500;
+    }
+    
+    // 设置事件回调
+    ovpn_cfg.event_cb = ovpn_netdrv_event_callback;
+    ovpn_cfg.user_data = (void *)drv;
+    
+    // 初始化 OpenVPN 客户端
+    int ret = ovpn_client_init(client, &ovpn_cfg);
+    if (ret != 0) {
+        LLOGE("Failed to initialize OpenVPN client: %d", ret);
+        luat_heap_free(client);
+        luat_heap_free(ctx);
+        luat_heap_free(drv);
+        return NULL;
+    }
+    
+    // 保存客户端指针到上下文
+    ctx->client = client;
+    ctx->conf = conf;
+    
+    // 初始化 netdrv 结构体
+    drv->id = conf->id;
+    drv->userdata = (void *)ctx;
+    drv->netif = &client->netif;
+    drv->boot = openvpn_boot;
+    // drv->shutdown = openvpn_shutdown;
+    drv->ready = openvpn_ready;
+    drv->dhcp = openvpn_dhcp;
+    drv->debug = openvpn_debug;
+    
+    // 注册到 netdrv 系统
+    int reg_ret = luat_netdrv_register(conf->id, drv);
+    if (reg_ret != 0) {
+        LLOGE("Failed to register OpenVPN netdrv");
+        luat_heap_free(client);
+        luat_heap_free(ctx);
+        luat_heap_free(drv);
+        return NULL;
+    }
+    
+    LLOGI("OpenVPN netdrv setup completed successfully");
+    return drv;
+}

+ 867 - 0
components/network/netdrv/src/luat_netdrv_openvpn_client.c

@@ -0,0 +1,867 @@
+#include "luat_netdrv_openvpn_client.h"
+
+#include <string.h>
+#include "lwip/def.h"
+#include "lwip/pbuf.h"
+#include "lwip/udp.h"
+#include "lwip/ip4.h"
+#include "lwip/tcpip.h"
+#include "lwip/timeouts.h"
+#include "lwip/sys.h"
+#include "net_lwip2.h"
+#include "luat_malloc.h"
+#include "luat_crypto.h"
+#include "mbedtls/md.h"
+#include "mbedtls/ssl.h"
+#include "mbedtls/x509_crt.h"
+#include "mbedtls/pk.h"
+#include "mbedtls/ctr_drbg.h"
+#include "mbedtls/entropy.h"
+
+#define LUAT_LOG_TAG "openvpn"
+#include "luat_log.h"
+#include "luat_network_adapter.h"
+
+/* lwIP version compatibility handling */
+#include "lwip/opt.h"
+
+/* lwIP 2.0 compatibility: IP4_ADDR_ANY4 */
+#ifndef IP4_ADDR_ANY4
+#define IP4_ADDR_ANY4 IP_ADDR_ANY
+#endif
+
+/* lwIP 2.0 compatibility: IPADDR_TYPE_ANY */
+#ifndef IPADDR_TYPE_ANY
+#if LWIP_IPV6
+#define IPADDR_TYPE_ANY IPADDR_TYPE_V4
+#else
+#define IPADDR_TYPE_ANY IPADDR_TYPE_V4
+#endif
+#endif
+
+/* lwIP 2.0 compatibility: IPADDR_ANY_TYPE_INIT */
+#ifndef IPADDR_ANY_TYPE_INIT
+#if LWIP_IPV6
+#define IPADDR_ANY_TYPE_INIT { IPADDR_TYPE_V4 }
+#else
+#define IPADDR_ANY_TYPE_INIT { 0 }
+#endif
+#endif
+
+/* mbedtls version compatibility: check handshake state */
+/* mbedtls 2.x: ssl.state is public field, mbedtls 3.x: use MBEDTLS_PRIVATE() macro to access private fields */
+#if !defined(MBEDTLS_PRIVATE)
+#define MBEDTLS_PRIVATE(x) x
+#endif
+
+/* mbedtls handshake complete state value */
+#if !defined(MBEDTLS_SSL_HANDSHAKE_OVER)
+/* If not defined, try to define as common value */
+#define MBEDTLS_SSL_HANDSHAKE_OVER 0
+#endif
+
+/* lwIP 2.1 compatibility: NETIF_FLAG_POINTTOPOINT may not exist */
+#ifndef NETIF_FLAG_POINTTOPOINT
+#define NETIF_FLAG_POINTTOPOINT 0
+#endif
+
+/* lwIP 2.1 compatibility: NETIF_FLAG_NOARP may not exist in older versions */
+#ifndef NETIF_FLAG_NOARP
+#define NETIF_FLAG_NOARP 0
+#endif
+
+#ifndef OVPN_MAX_KEY_LEN
+#define OVPN_MAX_KEY_LEN 64
+#endif
+
+#define OVPN_HMAC_LEN 32
+#define OVPN_REPLAY_WINDOW 32
+#define OVPN_PING_INTERVAL_MS 10000
+#define OVPN_DEAD_INTERVAL_MS 30000
+#define OVPN_FLAG_PING 0x01
+#define OVPN_FLAG_PONG 0x02
+
+struct ovpn_data_hdr {
+    uint32_t seq;
+    uint16_t len;
+    uint8_t  flags;
+    uint8_t  rsv;
+};
+
+static err_t ovpn_netif_output_ip4(struct netif *n, struct pbuf *p, const ip4_addr_t *addr);
+#if LWIP_IPV6
+static err_t ovpn_netif_output_ip6(struct netif *n, struct pbuf *p, const ip6_addr_t *addr);
+#endif
+static void ovpn_udp_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port);
+static err_t ovpn_netif_init(struct netif *n);
+static err_t ovpn_send_frame(ovpn_client_t *cli, struct pbuf *payload, uint8_t flags);
+static int ovpn_hmac(const ovpn_client_t *cli, const uint8_t *hdr, size_t hdr_len, const uint8_t *payload, size_t payload_len, uint8_t *out);
+static int ovpn_replay_accept(ovpn_client_t *cli, uint32_t seq);
+static void ovpn_ping_timer(void *arg);
+static int ovpn_tls_init(ovpn_client_t *cli, const ovpn_client_cfg_t *cfg);
+static void ovpn_tls_free(ovpn_client_t *cli);
+static int ovpn_tls_udp_send(void *ctx, const unsigned char *buf, size_t len);
+static int ovpn_tls_udp_recv(void *ctx, unsigned char *buf, size_t len);
+static void ovpn_tls_process_rx(ovpn_client_t *cli);
+
+int ovpn_client_init(ovpn_client_t *cli, const ovpn_client_cfg_t *cfg) {
+    if (!cli || !cfg) {
+        return -1;
+    }
+    memset(cli, 0, sizeof(*cli));
+    cli->remote_ip = cfg->remote_ip;
+    cli->remote_port = cfg->remote_port;
+    cli->adapter_index = cfg->adapter_index ? cfg->adapter_index : NW_ADAPTER_INDEX_LWIP_USER0;
+    cli->mtu = cfg->tun_mtu ? cfg->tun_mtu : 1500;
+    if (cfg->static_key && cfg->static_key_len) {
+        cli->key_len = (cfg->static_key_len > OVPN_MAX_KEY_LEN) ? OVPN_MAX_KEY_LEN : cfg->static_key_len;
+        memcpy(cli->key, cfg->static_key, cli->key_len);
+    }
+    cli->event_cb = cfg->event_cb;
+    cli->user_data = cfg->user_data;
+    
+    if (cfg->ca_cert_pem && cfg->client_cert_pem && cfg->client_key_pem) {
+        // Copy CA certificate to heap memory
+        cli->ca_cert_buf = (uint8_t *)luat_heap_malloc(cfg->ca_cert_len + 1);
+        if (!cli->ca_cert_buf) {
+            return -2;
+        }
+        memcpy(cli->ca_cert_buf, cfg->ca_cert_pem, cfg->ca_cert_len);
+        cli->ca_cert_buf[cfg->ca_cert_len] = '\0';  // Add string terminator
+        cli->ca_cert_len = cfg->ca_cert_len;
+        
+        // Copy client certificate to heap memory
+        cli->client_cert_buf = (uint8_t *)luat_heap_malloc(cfg->client_cert_len + 1);
+        if (!cli->client_cert_buf) {
+            luat_heap_free(cli->ca_cert_buf);
+            cli->ca_cert_buf = NULL;
+            return -2;
+        }
+        memcpy(cli->client_cert_buf, cfg->client_cert_pem, cfg->client_cert_len);
+        cli->client_cert_buf[cfg->client_cert_len] = '\0';
+        cli->client_cert_len = cfg->client_cert_len;
+        
+        // Copy client private key to heap memory
+        cli->client_key_buf = (uint8_t *)luat_heap_malloc(cfg->client_key_len + 1);
+        if (!cli->client_key_buf) {
+            luat_heap_free(cli->ca_cert_buf);
+            luat_heap_free(cli->client_cert_buf);
+            cli->ca_cert_buf = NULL;
+            cli->client_cert_buf = NULL;
+            return -2;
+        }
+        memcpy(cli->client_key_buf, cfg->client_key_pem, cfg->client_key_len);
+        cli->client_key_buf[cfg->client_key_len] = '\0';
+        cli->client_key_len = cfg->client_key_len;
+        
+        // Allocate TLS temporary buffer
+        cli->tls_buf = (uint8_t *)luat_heap_malloc(1600);
+        if (!cli->tls_buf) {
+            luat_heap_free(cli->ca_cert_buf);
+            luat_heap_free(cli->client_cert_buf);
+            luat_heap_free(cli->client_key_buf);
+            cli->ca_cert_buf = NULL;
+            cli->client_cert_buf = NULL;
+            cli->client_key_buf = NULL;
+            return -2;
+        }
+        
+        // Initialize TLS using copied certificate data
+        ovpn_client_cfg_t cfg_copy = *cfg;  // Copy configuration structure
+        cfg_copy.ca_cert_pem = (const char *)cli->ca_cert_buf;
+        cfg_copy.ca_cert_len = cli->ca_cert_len;
+        cfg_copy.client_cert_pem = (const char *)cli->client_cert_buf;
+        cfg_copy.client_cert_len = cli->client_cert_len;
+        cfg_copy.client_key_pem = (const char *)cli->client_key_buf;
+        cfg_copy.client_key_len = cli->client_key_len;
+        
+        if (ovpn_tls_init(cli, &cfg_copy) != 0) {
+            luat_heap_free(cli->ca_cert_buf);
+            luat_heap_free(cli->client_cert_buf);
+            luat_heap_free(cli->client_key_buf);
+            luat_heap_free(cli->tls_buf);
+            cli->ca_cert_buf = NULL;
+            cli->client_cert_buf = NULL;
+            cli->client_key_buf = NULL;
+            cli->tls_buf = NULL;
+            return -2;
+        }
+        cli->use_tls = 1;
+    }
+    cli->last_activity_ms = sys_now();
+    cli->last_ping_ms = cli->last_activity_ms;
+    return 0;
+}
+
+static void ovpn_attach_netif(ovpn_client_t *cli) {
+    if (cli->adapter_index >= NW_ADAPTER_INDEX_LWIP_NETIF_QTY) {
+        cli->adapter_index = NW_ADAPTER_INDEX_LWIP_USER0;
+    }
+    /* lwIP 2.0/2.1/2.2 compatibility: netif_add parameters */
+#if LWIP_VERSION_MAJOR >= 2 && LWIP_VERSION_MINOR >= 1
+    netif_add(&cli->netif, IP4_ADDR_ANY4, IP4_ADDR_ANY4, IP4_ADDR_ANY4, cli, ovpn_netif_init, netif_input);
+#else
+    /* lwIP 2.0: netif_add accepts ip4_addr_t* type */
+    ip4_addr_t ipaddr, netmask, gw;
+    ip4_addr_set_zero(&ipaddr);
+    ip4_addr_set_zero(&netmask);
+    ip4_addr_set_zero(&gw);
+    netif_add(&cli->netif, &ipaddr, &netmask, &gw, cli, ovpn_netif_init, netif_input);
+#endif
+    netif_set_up(&cli->netif);
+    netif_set_link_up(&cli->netif);
+    net_lwip2_set_netif(cli->adapter_index, &cli->netif);
+    net_lwip2_register_adapter(cli->adapter_index);
+}
+
+int ovpn_client_start(ovpn_client_t *cli) {
+    if (!cli) {
+        return -1;
+    }
+    if (cli->started) {
+        return 0;
+    }
+    if (ip_addr_isany(&cli->remote_ip)) {
+        LLOGE("remote ip missing");
+        return -2;
+    }
+    if (!cli->use_tls && cli->key_len == 0) {
+        LLOGE("static key missing");
+        return -3;
+    }
+    /* lwIP 2.0/2.1/2.2 compatibility: use different UDP creation methods based on version */
+#if LWIP_VERSION_MAJOR >= 2 && LWIP_VERSION_MINOR >= 1
+    cli->udp = udp_new_ip_type(IPADDR_TYPE_ANY);
+#else
+    cli->udp = udp_new();
+#endif
+    if (!cli->udp) {
+        LLOGE("udp alloc fail");
+        return -3;
+    }
+    /* lwIP 2.0/2.1/2.2 compatibility: initialize any_addr */
+#if LWIP_VERSION_MAJOR >= 2 && LWIP_VERSION_MINOR >= 1
+    ip_addr_t any_addr = IPADDR_ANY_TYPE_INIT;
+#else
+    ip_addr_t any_addr;
+    ip_addr_set_any(IP_IS_V6(&cli->remote_ip), &any_addr);
+#endif
+    if (udp_bind(cli->udp, &any_addr, 0) != ERR_OK) {
+        udp_remove(cli->udp);
+        cli->udp = NULL;
+        LLOGE("udp bind fail");
+        return -4;
+    }
+    udp_recv(cli->udp, ovpn_udp_recv, cli);
+    ovpn_attach_netif(cli);
+    sys_timeout(OVPN_PING_INTERVAL_MS, ovpn_ping_timer, cli);
+    cli->started = 1;
+    /* Trigger connection established event (static key mode triggers immediately, TLS mode waits for handshake completion) */
+    if (!cli->use_tls && cli->event_cb) {
+        cli->event_cb(OVPN_EVENT_CONNECTED, cli->user_data);
+    }
+    return 0;
+}
+
+void ovpn_client_stop(ovpn_client_t *cli) {
+    if (!cli) {
+        return;
+    }
+    if (cli->udp) {
+        udp_remove(cli->udp);
+        cli->udp = NULL;
+    }
+    sys_untimeout(ovpn_ping_timer, cli);
+    if (cli->started) {
+        netif_set_down(&cli->netif);
+        netif_remove(&cli->netif);
+    }
+    cli->started = 0;
+    if (cli->use_tls) {
+        ovpn_tls_free(cli);
+    }
+    /* Trigger disconnected event */
+    if (cli->event_cb) {
+        cli->event_cb(OVPN_EVENT_DISCONNECTED, cli->user_data);
+    }
+}
+
+static err_t ovpn_send_frame(ovpn_client_t *cli, struct pbuf *payload, uint8_t flags) {
+    if (!cli || !cli->udp) {
+        return ERR_CLSD;
+    }
+    uint16_t payload_len = payload ? (uint16_t)payload->tot_len : 0;
+    if (payload_len > cli->mtu) {
+        return ERR_VAL;
+    }
+    /* Build the logical header used for ping/replay even in TLS mode. */
+    struct ovpn_data_hdr hdr = {0};
+    hdr.seq = lwip_htonl(cli->tx_seq++);
+    hdr.len = lwip_htons(payload_len);
+    hdr.flags = flags;
+
+    if (cli->use_tls) {
+        if (!cli->tls_ready) {
+            return ERR_INPROGRESS;
+        }
+        uint16_t frame_len = (uint16_t)(sizeof(hdr) + payload_len);
+        if (frame_len > 1600 || !cli->tls_buf) {
+            return ERR_VAL;
+        }
+        memcpy(cli->tls_buf, &hdr, sizeof(hdr));
+        if (payload_len) {
+            pbuf_copy_partial(payload, cli->tls_buf + sizeof(hdr), payload_len, 0);
+        }
+        int wret = mbedtls_ssl_write(&cli->ssl, cli->tls_buf, frame_len);
+        if (wret > 0) {
+            cli->stats.tx_pkts++;
+            cli->stats.tx_bytes += payload_len;
+            if (flags == OVPN_FLAG_PING) {
+                cli->stats.ping_sent++;
+            }
+            cli->last_activity_ms = sys_now();
+            if (cli->debug) {
+                LLOGD("tx(tls) seq=%lu len=%u flags=0x%02X", (unsigned long)lwip_ntohl(hdr.seq), payload_len, flags);
+            }
+            /* Trigger data TX event (non-PING packets) */
+            if (payload_len > 0 && cli->event_cb) {
+                cli->event_cb(OVPN_EVENT_DATA_TX, cli->user_data);
+            }
+            return ERR_OK;
+        }
+        return ERR_CLSD;
+    }
+
+    uint16_t frame_len = sizeof(hdr) + payload_len + OVPN_HMAC_LEN;
+    struct pbuf *q = pbuf_alloc(PBUF_TRANSPORT, frame_len, PBUF_RAM);
+    if (!q) {
+        return ERR_MEM;
+    }
+    pbuf_take(q, &hdr, sizeof(hdr));
+    uint8_t *frame_payload = ((uint8_t *)q->payload) + sizeof(hdr);
+    if (payload_len) {
+        pbuf_copy_partial(payload, frame_payload, payload_len, 0);
+    }
+    uint8_t *mac_pos = frame_payload + payload_len;
+    if (ovpn_hmac(cli, (const uint8_t *)&hdr, sizeof(hdr), frame_payload, payload_len, mac_pos) != 0) {
+        pbuf_free(q);
+        return ERR_VAL;
+    }
+    err_t err = udp_sendto(cli->udp, q, &cli->remote_ip, cli->remote_port);
+    if (err == ERR_OK) {
+        cli->stats.tx_pkts++;
+        cli->stats.tx_bytes += payload_len;
+        if (flags == OVPN_FLAG_PING) {
+            cli->stats.ping_sent++;
+        }
+        cli->last_activity_ms = sys_now();
+        if (cli->debug) {
+            LLOGD("tx seq=%lu len=%u flags=0x%02X", (unsigned long)lwip_ntohl(hdr.seq), payload_len, flags);
+        }
+        /* Trigger data TX event (non-PING packets) */
+        if (payload_len > 0 && cli->event_cb) {
+            cli->event_cb(OVPN_EVENT_DATA_TX, cli->user_data);
+        }
+    }
+    pbuf_free(q);
+    return err;
+}
+
+static int ovpn_hmac(const ovpn_client_t *cli, const uint8_t *hdr, size_t hdr_len, const uint8_t *payload, size_t payload_len, uint8_t *out) {
+    const mbedtls_md_info_t *info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
+    mbedtls_md_context_t ctx;
+    if (!info || !out || !cli || cli->key_len == 0) {
+        return -1;
+    }
+    mbedtls_md_init(&ctx);
+    if (mbedtls_md_setup(&ctx, info, 1) != 0) {
+        mbedtls_md_free(&ctx);
+        return -1;
+    }
+    if (mbedtls_md_hmac_starts(&ctx, cli->key, cli->key_len) != 0) {
+        mbedtls_md_free(&ctx);
+        return -1;
+    }
+    mbedtls_md_hmac_update(&ctx, hdr, hdr_len);
+    if (payload_len) {
+        mbedtls_md_hmac_update(&ctx, payload, payload_len);
+    }
+    int ret = mbedtls_md_hmac_finish(&ctx, out);
+    mbedtls_md_free(&ctx);
+    return ret;
+}
+
+static int ovpn_replay_accept(ovpn_client_t *cli, uint32_t seq) {
+    if (!cli) {
+        return 0;
+    }
+    if (!cli->rx_initialized) {
+        cli->rx_initialized = 1;
+        cli->rx_max_seq = seq;
+        cli->rx_window = 1u;
+        return 1;
+    }
+    if (seq > cli->rx_max_seq) {
+        uint32_t shift = seq - cli->rx_max_seq;
+        if (shift >= OVPN_REPLAY_WINDOW) {
+            cli->rx_window = 1u;
+        } else {
+            cli->rx_window = (cli->rx_window << shift) | 1u;
+        }
+        cli->rx_max_seq = seq;
+        return 1;
+    }
+    uint32_t diff = cli->rx_max_seq - seq;
+    if (diff >= OVPN_REPLAY_WINDOW) {
+        return 0;
+    }
+    uint32_t mask = 1u << diff;
+    if (cli->rx_window & mask) {
+        return 0;
+    }
+    cli->rx_window |= mask;
+    return 1;
+}
+
+static void ovpn_ping_timer(void *arg) {
+    ovpn_client_t *cli = (ovpn_client_t *)arg;
+    if (!cli || !cli->started) {
+        return;
+    }
+    uint32_t now = sys_now();
+    if ((now - cli->last_activity_ms) >= OVPN_PING_INTERVAL_MS) {
+        if (!(cli->use_tls && !cli->tls_ready)) {
+            ovpn_send_frame(cli, NULL, OVPN_FLAG_PING);
+            cli->last_ping_ms = now;
+        }
+    }
+    if ((now - cli->last_activity_ms) >= OVPN_DEAD_INTERVAL_MS) {
+        if (cli->debug) {
+            LLOGW("keepalive timeout");
+        }
+        /* Trigger keepalive timeout event (triggered once per timer cycle) */
+        if (cli->event_cb && (now - cli->last_activity_ms) < (OVPN_DEAD_INTERVAL_MS + OVPN_PING_INTERVAL_MS)) {
+            cli->event_cb(OVPN_EVENT_KEEPALIVE_TIMEOUT, cli->user_data);
+        }
+    }
+    sys_timeout(OVPN_PING_INTERVAL_MS, ovpn_ping_timer, cli);
+}
+
+static err_t ovpn_send_payload(ovpn_client_t *cli, struct pbuf *payload) {
+    if (!cli) {
+        return ERR_VAL;
+    }
+    return ovpn_send_frame(cli, payload, 0);
+}
+
+static err_t ovpn_netif_output_ip4(struct netif *n, struct pbuf *p, const ip4_addr_t *addr) {
+    LWIP_UNUSED_ARG(addr);
+    ovpn_client_t *cli = (ovpn_client_t *)n->state;
+    if (!cli) {
+        return ERR_VAL;
+    }
+    return ovpn_send_payload(cli, p);
+}
+
+#if LWIP_IPV6
+static err_t ovpn_netif_output_ip6(struct netif *n, struct pbuf *p, const ip6_addr_t *addr) {
+    LWIP_UNUSED_ARG(addr);
+    ovpn_client_t *cli = (ovpn_client_t *)n->state;
+    if (!cli) {
+        return ERR_VAL;
+    }
+    return ovpn_send_payload(cli, p);
+}
+#endif
+
+static void ovpn_udp_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) {
+    LWIP_UNUSED_ARG(pcb);
+    LWIP_UNUSED_ARG(addr);
+    LWIP_UNUSED_ARG(port);
+    ovpn_client_t *cli = (ovpn_client_t *)arg;
+    if (!cli || !p) {
+        if (p) pbuf_free(p);
+        return;
+    }
+    if (cli->use_tls) {
+        if (cli->rx_pending.pkt) {
+            pbuf_free(cli->rx_pending.pkt);
+        }
+        cli->rx_pending.pkt = p;
+        cli->rx_pending.offset = 0;
+        ovpn_tls_process_rx(cli);
+        return;
+    }
+    if (p->tot_len < (sizeof(struct ovpn_data_hdr) + OVPN_HMAC_LEN)) {
+        cli->stats.drop_malformed++;
+        pbuf_free(p);
+        return;
+    }
+    struct ovpn_data_hdr hdr;
+    pbuf_copy_partial(p, &hdr, sizeof(hdr), 0);
+    uint16_t plen = lwip_ntohs(hdr.len);
+    uint32_t seq = lwip_ntohl(hdr.seq);
+    uint16_t expect_len = (uint16_t)(sizeof(hdr) + plen + OVPN_HMAC_LEN);
+    if (p->tot_len < expect_len) {
+        cli->stats.drop_malformed++;
+        pbuf_free(p);
+        return;
+    }
+    if (!ovpn_replay_accept(cli, seq)) {
+        cli->stats.drop_replay++;
+        pbuf_free(p);
+        return;
+    }
+    struct pbuf *ip = NULL;
+    if (plen) {
+        ip = pbuf_alloc(PBUF_IP, plen, PBUF_RAM);
+        if (!ip) {
+            pbuf_free(p);
+            return;
+        }
+        pbuf_copy_partial(p, ip->payload, plen, sizeof(hdr));
+    }
+    uint8_t mac_calc[OVPN_HMAC_LEN];
+    uint8_t mac_recv[OVPN_HMAC_LEN];
+    pbuf_copy_partial(p, mac_recv, OVPN_HMAC_LEN, sizeof(hdr) + plen);
+    if (ovpn_hmac(cli, (uint8_t *)&hdr, sizeof(hdr), ip ? (uint8_t *)ip->payload : NULL, plen, mac_calc) != 0 ||
+        memcmp(mac_calc, mac_recv, OVPN_HMAC_LEN) != 0) {
+        cli->stats.drop_auth++;
+        /* Trigger authentication failed event */
+        if (cli->event_cb) {
+            cli->event_cb(OVPN_EVENT_AUTH_FAILED, cli->user_data);
+        }
+        if (ip) pbuf_free(ip);
+        pbuf_free(p);
+        return;
+    }
+    cli->last_activity_ms = sys_now();
+    if (hdr.flags & OVPN_FLAG_PING) {
+        cli->stats.ping_recv++;
+        ovpn_send_frame(cli, NULL, OVPN_FLAG_PONG);
+        if (ip) pbuf_free(ip);
+        pbuf_free(p);
+        return;
+    }
+    if (hdr.flags & OVPN_FLAG_PONG) {
+        if (ip) pbuf_free(ip);
+        pbuf_free(p);
+        return;
+    }
+    if (ip) {
+        err_t err = cli->netif.input(ip, &cli->netif);
+        if (err != ERR_OK) {
+            pbuf_free(ip);
+        } else {
+            cli->stats.rx_pkts++;
+            cli->stats.rx_bytes += plen;
+            /* Trigger data RX event */
+            if (cli->event_cb) {
+                cli->event_cb(OVPN_EVENT_DATA_RX, cli->user_data);
+            }
+        }
+    }
+    if (cli->debug) {
+        LLOGD("rx seq=%lu len=%u flags=0x%02X", (unsigned long)seq, plen, hdr.flags);
+    }
+    pbuf_free(p);
+}
+
+static int ovpn_tls_udp_send(void *ctx, const unsigned char *buf, size_t len) {
+    ovpn_client_t *cli = (ovpn_client_t *)ctx;
+    if (!cli || !cli->udp) {
+        return MBEDTLS_ERR_SSL_INTERNAL_ERROR;
+    }
+    /* Allocate a transient pbuf for the DTLS record. */
+    struct pbuf *q = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
+    if (!q) {
+        return MBEDTLS_ERR_SSL_ALLOC_FAILED;
+    }
+    pbuf_take(q, buf, len);
+    err_t err = udp_sendto(cli->udp, q, &cli->remote_ip, cli->remote_port);
+    pbuf_free(q);
+    if (err != ERR_OK) {
+        return MBEDTLS_ERR_SSL_INTERNAL_ERROR;
+    }
+    return (int)len;
+}
+
+static int ovpn_tls_udp_recv(void *ctx, unsigned char *buf, size_t len) {
+    ovpn_client_t *cli = (ovpn_client_t *)ctx;
+    if (!cli || !cli->rx_pending.pkt) {
+        return MBEDTLS_ERR_SSL_WANT_READ;
+    }
+    struct pbuf *p = cli->rx_pending.pkt;
+    uint16_t remain = p->tot_len - cli->rx_pending.offset;
+    uint16_t take = (remain > len) ? (uint16_t)len : remain;
+    pbuf_copy_partial(p, buf, take, cli->rx_pending.offset);
+    cli->rx_pending.offset += take;
+    if (cli->rx_pending.offset >= p->tot_len) {
+        pbuf_free(p);
+        cli->rx_pending.pkt = NULL;
+        cli->rx_pending.offset = 0;
+    }
+    return (int)take;
+}
+
+static void ovpn_tls_set_delay(void *ctx, uint32_t int_ms, uint32_t fin_ms) {
+    ovpn_client_t *cli = (ovpn_client_t *)ctx;
+    cli->dtls_timer.int_ms = int_ms ? (sys_now() + int_ms) : 0;
+    cli->dtls_timer.fin_ms = fin_ms ? (sys_now() + fin_ms) : 0;
+}
+
+static int ovpn_tls_get_delay(void *ctx) {
+    ovpn_client_t *cli = (ovpn_client_t *)ctx;
+    uint32_t now = sys_now();
+    if (cli->dtls_timer.fin_ms == 0) {
+        return -1;
+    }
+    if (now >= cli->dtls_timer.fin_ms) {
+        return 2;
+    }
+    if (cli->dtls_timer.int_ms && now >= cli->dtls_timer.int_ms) {
+        return 1;
+    }
+    return 0;
+}
+
+static int tls_myrand( void *rng_state, unsigned char *output, size_t len ) {
+    (void)rng_state;
+    luat_crypto_trng((char*)output, len);
+    return 0;
+}
+
+
+static int ovpn_tls_init(ovpn_client_t *cli, const ovpn_client_cfg_t *cfg) {
+    /* Initialize DTLS contexts and load credentials. */
+    mbedtls_ssl_init(&cli->ssl);
+    mbedtls_ssl_config_init(&cli->conf);
+    mbedtls_x509_crt_init(&cli->ca);
+    mbedtls_x509_crt_init(&cli->client_cert);
+    mbedtls_pk_init(&cli->client_key);
+    mbedtls_ctr_drbg_init(&cli->drbg);
+    mbedtls_entropy_init(&cli->entropy);
+
+    const char *pers = "ovpn-dtls";
+    int ret = mbedtls_ctr_drbg_seed(&cli->drbg, mbedtls_entropy_func, &cli->entropy,
+                                    (const unsigned char *)pers, strlen(pers));
+    if (ret != 0) {
+        return ret;
+    }
+    ret = mbedtls_x509_crt_parse(&cli->ca, (const unsigned char *)cfg->ca_cert_pem, cfg->ca_cert_len);
+    if (ret != 0) {
+        return ret;
+    }
+    ret = mbedtls_x509_crt_parse(&cli->client_cert, (const unsigned char *)cfg->client_cert_pem, cfg->client_cert_len);
+    if (ret != 0) {
+        return ret;
+    }
+    /* mbedtls 3.x adds RNG parameters to pk_parse_key */
+#if MBEDTLS_VERSION_NUMBER >= 0x03000000
+    ret = mbedtls_pk_parse_key(&cli->client_key, (const unsigned char *)cfg->client_key_pem, cfg->client_key_len, 
+                                NULL, 0, mbedtls_ctr_drbg_random, &cli->drbg);
+#else
+    ret = mbedtls_pk_parse_key(&cli->client_key, (const unsigned char *)cfg->client_key_pem, cfg->client_key_len, tls_myrand, 0);
+#endif
+    if (ret != 0) {
+        return ret;
+    }
+    ret = mbedtls_ssl_config_defaults(&cli->conf,
+                                      MBEDTLS_SSL_IS_CLIENT,
+                                      MBEDTLS_SSL_TRANSPORT_DATAGRAM,
+                                      MBEDTLS_SSL_PRESET_DEFAULT);
+    if (ret != 0) {
+        return ret;
+    }
+    mbedtls_ssl_conf_authmode(&cli->conf, MBEDTLS_SSL_VERIFY_REQUIRED);
+    mbedtls_ssl_conf_ca_chain(&cli->conf, &cli->ca, NULL);
+    ret = mbedtls_ssl_conf_own_cert(&cli->conf, &cli->client_cert, &cli->client_key);
+    if (ret != 0) {
+        return ret;
+    }
+    mbedtls_ssl_conf_rng(&cli->conf, mbedtls_ctr_drbg_random, &cli->drbg);
+    //mbedtls_ssl_conf_min_version(&cli->conf, MBEDTLS_SSL_MAJOR_VERSION_3, MBEDTLS_SSL_MINOR_VERSION_3);
+
+    ret = mbedtls_ssl_setup(&cli->ssl, &cli->conf);
+    if (ret != 0) {
+        return ret;
+    }
+    if (cfg->remote_host) {
+        mbedtls_ssl_set_hostname(&cli->ssl, cfg->remote_host);
+    }
+    mbedtls_ssl_set_bio(&cli->ssl, cli, ovpn_tls_udp_send, ovpn_tls_udp_recv, NULL);
+    mbedtls_ssl_set_timer_cb(&cli->ssl, cli, ovpn_tls_set_delay, ovpn_tls_get_delay);
+    cli->tls_ready = 0;
+    return 0;
+}
+
+static void ovpn_tls_free(ovpn_client_t *cli) {
+    if (!cli || !cli->use_tls) {
+        return;
+    }
+    if (cli->rx_pending.pkt) {
+        pbuf_free(cli->rx_pending.pkt);
+        cli->rx_pending.pkt = NULL;
+    }
+    if (cli->tls_buf) {
+        luat_heap_free(cli->tls_buf);
+        cli->tls_buf = NULL;
+    }
+    // Free certificate data copies
+    if (cli->ca_cert_buf) {
+        luat_heap_free(cli->ca_cert_buf);
+        cli->ca_cert_buf = NULL;
+    }
+    if (cli->client_cert_buf) {
+        luat_heap_free(cli->client_cert_buf);
+        cli->client_cert_buf = NULL;
+    }
+    if (cli->client_key_buf) {
+        luat_heap_free(cli->client_key_buf);
+        cli->client_key_buf = NULL;
+    }
+    mbedtls_ssl_free(&cli->ssl);
+    mbedtls_ssl_config_free(&cli->conf);
+    mbedtls_x509_crt_free(&cli->ca);
+    mbedtls_x509_crt_free(&cli->client_cert);
+    mbedtls_pk_free(&cli->client_key);
+    mbedtls_ctr_drbg_free(&cli->drbg);
+    mbedtls_entropy_free(&cli->entropy);
+}
+
+static void ovpn_tls_process_rx(ovpn_client_t *cli) {
+    /* Drive DTLS handshake and decrypt any app data buffered from udp recv. */
+    if (!cli || !cli->use_tls) {
+        return;
+    }
+    if (!cli->tls_ready) {
+        while (!cli->tls_ready) {
+            int ret = mbedtls_ssl_handshake_step(&cli->ssl);
+            if (ret == 0) {
+                /* mbedtls version compatibility: check if handshake is complete */
+                /* Use MBEDTLS_PRIVATE macro to be compatible with mbedtls 2.x/3.x */
+                int handshake_done = (cli->ssl.MBEDTLS_PRIVATE(state) == MBEDTLS_SSL_HANDSHAKE_OVER);
+                if (handshake_done) {
+                    cli->tls_ready = 1;
+                    if (cli->debug) {
+                        LLOGI("dtls handshake ok");
+                    }
+                    /* Trigger TLS handshake OK event */
+                    if (cli->event_cb) {
+                        cli->event_cb(OVPN_EVENT_TLS_HANDSHAKE_OK, cli->user_data);
+                    }
+                }
+                continue;
+            }
+            if (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE) {
+                break;
+            }
+            if (cli->debug) {
+                LLOGE("dtls handshake err %d", ret);
+            }
+            /* Trigger TLS handshake FAIL event */
+            if (cli->event_cb) {
+                cli->event_cb(OVPN_EVENT_TLS_HANDSHAKE_FAIL, cli->user_data);
+            }
+            return;
+        }
+    }
+
+    if (!cli->tls_ready) {
+        return;
+    }
+
+    if (!cli->tls_buf) {
+        return;
+    }
+    for (;;) {
+        int rlen = mbedtls_ssl_read(&cli->ssl, cli->tls_buf, 1600);
+        if (rlen > 0) {
+            if ((size_t)rlen < sizeof(struct ovpn_data_hdr)) {
+                cli->stats.drop_malformed++;
+                continue;
+            }
+            struct ovpn_data_hdr hdr;
+            memcpy(&hdr, cli->tls_buf, sizeof(hdr));
+            uint16_t plen = lwip_ntohs(hdr.len);
+            uint32_t seq = lwip_ntohl(hdr.seq);
+            if ((size_t)rlen < sizeof(hdr) + plen) {
+                cli->stats.drop_malformed++;
+                continue;
+            }
+            if (!ovpn_replay_accept(cli, seq)) {
+                cli->stats.drop_replay++;
+                continue;
+            }
+            cli->last_activity_ms = sys_now();
+            if (hdr.flags & OVPN_FLAG_PING) {
+                cli->stats.ping_recv++;
+                ovpn_send_frame(cli, NULL, OVPN_FLAG_PONG);
+                continue;
+            }
+            if (hdr.flags & OVPN_FLAG_PONG) {
+                continue;
+            }
+            if (plen) {
+                struct pbuf *ip = pbuf_alloc(PBUF_IP, plen, PBUF_RAM);
+                if (!ip) {
+                    continue;
+                }
+                memcpy(ip->payload, cli->tls_buf + sizeof(hdr), plen);
+                err_t err = cli->netif.input(ip, &cli->netif);
+                if (err != ERR_OK) {
+                    pbuf_free(ip);
+                } else {
+                    cli->stats.rx_pkts++;
+                    cli->stats.rx_bytes += plen;
+                    /* Trigger data RX event */
+                    if (cli->event_cb) {
+                        cli->event_cb(OVPN_EVENT_DATA_RX, cli->user_data);
+                    }
+                }
+            }
+            if (cli->debug) {
+                LLOGD("rx(tls) seq=%lu len=%u flags=0x%02X", (unsigned long)seq, plen, hdr.flags);
+            }
+            continue;
+        }
+        if (rlen == MBEDTLS_ERR_SSL_WANT_READ || rlen == MBEDTLS_ERR_SSL_WANT_WRITE) {
+            break;
+        }
+        if (rlen == 0) {
+            /* EOF */
+            break;
+        }
+        if (cli->debug) {
+            LLOGE("dtls read err %d", rlen);
+        }
+        break;
+    }
+}
+
+static err_t ovpn_netif_init(struct netif *n) {
+    ovpn_client_t *cli = (ovpn_client_t *)n->state;
+    n->mtu = cli->mtu;
+    n->flags = NETIF_FLAG_POINTTOPOINT | NETIF_FLAG_NOARP | NETIF_FLAG_LINK_UP;
+    n->output = ovpn_netif_output_ip4;
+#if LWIP_IPV6
+    n->output_ip6 = ovpn_netif_output_ip6;
+#endif
+    n->name[0] = 'o';
+    n->name[1] = 'v';
+    return ERR_OK;
+}
+
+void ovpn_client_get_stats(ovpn_client_t *cli, ovpn_client_stats_t *out) {
+    if (!cli || !out) {
+        return;
+    }
+    *out = cli->stats;
+}
+
+void ovpn_client_set_debug(ovpn_client_t *cli, int enable) {
+    if (!cli) {
+        return;
+    }
+    cli->debug = enable ? 1 : 0;
+}

+ 1 - 1
components/network/websocket/luat_lib_websocket.c

@@ -414,7 +414,7 @@ static int l_websocket_send(lua_State *L)
 	}
 	websocket_ctrl->frame_wait ++;
 	ret = luat_websocket_send_frame(websocket_ctrl, &pkg);
-	if (ret < 1) {
+	if (ret < 0) {
 		websocket_ctrl->frame_wait --;// 发送失败
 	}
 	lua_pushboolean(L, ret == 0 ? 1 : 0);

+ 9 - 3
components/ui/sdl2/luat_sdl2.c

@@ -11,14 +11,16 @@ static SDL_Renderer *renderer = NULL;
 static SDL_Texture *framebuffer = NULL;
 static luat_sdl2_conf_t sdl_conf;
 
-static void luat_sdl2_pump_events(void) {
+// 定时调用此函数以保持 SDL2 事件泵活跃,避免窗口无响应
+void luat_sdl2_pump_events(void) {
     SDL_Event e;
+    // 循环处理所有等待的事件
     while (SDL_PollEvent(&e)) {
         if (e.type == SDL_QUIT) {
-            // Graceful exit on window close
+            // 当用户关闭窗口时,优雅退出程序
             exit(0);
         }
-        // Other events are ignored; important is to pump to keep window responsive
+        // 其他事件不做处理,关键作用是让事件泵持续运行,防止界面假死
     }
 }
 
@@ -71,11 +73,15 @@ void luat_sdl2_draw(int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint32_t* da
     SDL_UpdateTexture(framebuffer, &r, data, r.w * 2);
 }
 
+// 刷新渲染器,将framebuffer渲染到窗口,并立即处理SDL事件
 void luat_sdl2_flush(void) {
     if (renderer && framebuffer)
     {
+        // 将framebuffer的内容拷贝到渲染目标(窗口),准备呈现
         SDL_RenderCopy(renderer, framebuffer, NULL, NULL);
+        // 实际执行呈现
         SDL_RenderPresent(renderer);
     }
+    // 画面刷新后立刻处理(pump)SDL事件,保持窗口及触摸输入活跃
     luat_sdl2_pump_events();
 }

+ 1 - 0
components/ui/sdl2/luat_sdl2.h

@@ -16,6 +16,7 @@ int luat_sdl2_deinit(luat_sdl2_conf_t *conf);
 
 void luat_sdl2_draw(int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint32_t* data);
 void luat_sdl2_flush(void);
+void luat_sdl2_pump_events(void);
 
 #endif
 

+ 3 - 0
luat/include/luat_pm.h

@@ -48,6 +48,9 @@ enum
 	LUAT_PM_POWER_LDO_CTL_PIN,
 	LUAT_PM_POWER_WIFI_STA_DTIM,
 	LUAT_PM_POWER_WIFI_AP_DTIM,
+	LUAT_PM_POWER_WIFI,
+
+	LUAT_PM_POWER_QTY,
 };
 
 // 电平类

+ 23 - 0
luat/modules/luat_lib_gpio.c

@@ -52,6 +52,17 @@ static uint32_t default_gpio_pull = Luat_GPIO_DEFAULT;
 // 记录GPIO电平,仅OUTPUT时可用
 static uint8_t gpio_out_levels[(LUAT_GPIO_PIN_MAX + 7) / 8];
 
+static inline int check_wifi_pwr_pin(int pin) {
+    #ifdef LUAT_MODEL_AIR8000
+    extern int luat_airlink_has_wifi(void);
+    if (23 == pin && luat_airlink_has_wifi()) {
+        LLOGW("gpio23 is wifi power pin, operation denied");
+        return 1; // wifi电源管脚禁止操作
+    }
+    #endif
+    return 0;
+}
+
 static uint8_t gpio_bit_get(int pin) {
     if (pin < 0 || pin >= LUAT_GPIO_PIN_MAX)
         return 0;
@@ -247,6 +258,9 @@ static int l_gpio_setup(lua_State *L) {
         LLOGW("id out of range 0 ~ %d, but %d", LUAT_GPIO_PIN_MAX, conf.pin);
         return 0;
     }
+    if (check_wifi_pwr_pin(conf.pin)) {
+        return 0;
+    }
     //conf->mode = luaL_checkinteger(L, 2);
     conf.lua_ref = 0;
     conf.irq = 0;
@@ -456,6 +470,9 @@ static int l_gpio_set(lua_State *L) {
         pin = luaL_checkinteger(L, 1);
         value = luaL_checkinteger(L, 2);
     }
+    if (check_wifi_pwr_pin(pin)) {
+        return 0;
+    }
     #ifdef LUAT_USE_DRV_GPIO
     luat_drv_gpio_set(pin, value);
     #else
@@ -514,6 +531,9 @@ static int l_gpio_close(lua_State *L) {
     int pin = luaL_checkinteger(L, 1);
     if (pin < 0 || pin >= LUAT_GPIO_PIN_MAX)
         return 0;
+    if (check_wifi_pwr_pin(pin)) {
+        return 0;
+    }
     luat_gpio_close(pin);
     if (gpios[pin].lua_ref) {
         luaL_unref(L, LUA_REGISTRYINDEX, gpios[pin].lua_ref);
@@ -575,6 +595,9 @@ static int l_gpio_toggle(lua_State *L) {
         LLOGW("pin id out of range (0-127)");
         return 0;
     }
+    if (check_wifi_pwr_pin(pin)) {
+        return 0;
+    }
     uint8_t value = gpio_bit_get(pin);
     #ifdef LUAT_USE_DRV_GPIO
     luat_drv_gpio_set(pin, value == 0 ? Luat_GPIO_HIGH : Luat_GPIO_LOW);

+ 9 - 1
luat/modules/luat_lib_pm.c

@@ -24,6 +24,7 @@
 #include "luat_base.h"
 #include "luat_pm.h"
 #include "luat_msgbus.h"
+#include "luat_gpio.h"
 #ifdef LUAT_USE_HMETA
 #include "luat_hmeta.h"
 #endif
@@ -352,6 +353,11 @@ static int l_pm_power_ctrl(lua_State *L) {
     {
     	onoff = lua_tointeger(L, 2);
     }
+    if (id == LUAT_PM_POWER_WIFI) {
+        #ifdef LUAT_MODEL_AIR8000
+        luat_gpio_set(23, onoff ? Luat_GPIO_HIGH : Luat_GPIO_LOW);
+        #endif
+    }
     #ifdef LUAT_USE_DRV_PM
     int chip = 0;
     if (lua_isinteger(L, 3)) {
@@ -653,10 +659,12 @@ static const rotable_Reg_t reg_pm[] =
 	//@const IOVOL_CAMD number camera数字电压
     { "IOVOL_CAMD", ROREG_INT(LUAT_PM_LDO_TYPE_CAMD)},
     //@const ID_NATIVE number PM控制的ID, 主芯片, 任意芯片的默认值就是它
-    { "ID_NATIVE",      ROREG_INT(1)},
+    { "ID_NATIVE",      ROREG_INT(0)},
     //@const ID_WIFI number PM控制的ID, WIFI芯片, 仅Air8000可用
     { "ID_WIFI",        ROREG_INT(1)},
 
+    { "WIFI",          ROREG_INT(LUAT_PM_POWER_WIFI)},
+
     //@const WIFI_STA_DTIM number wifi芯片控制STA模式下的DTIM间隔,单位100ms,默认值是1
     { "WIFI_STA_DTIM",  ROREG_INT(LUAT_PM_POWER_WIFI_STA_DTIM)},
     { "WIFI_AP_DTIM",   ROREG_INT(LUAT_PM_POWER_WIFI_AP_DTIM)},

+ 8 - 1
luat/modules/luat_lib_rtos.c

@@ -250,6 +250,8 @@ local luatos_version = rtos.version()
 -- 如果不是数字固件,luatos_version_num 会是0
 -- 如果是不支持的固件, luatos_version_num 会是nil
 local luatos_version, luatos_version_num = rtos.version(true)
+-- 读取底层位数, 32或者64, 2025.12.23 新增
+local luatos_version, luatos_version_num, luatos_bits = rtos.version(true)
 */
 static int l_rtos_version(lua_State *L) {
     lua_pushstring(L, luat_version_str());
@@ -260,7 +262,12 @@ static int l_rtos_version(lua_State *L) {
         #else
         lua_pushinteger(L, 0);
         #endif
-        return 2;
+        #ifdef LUAT_CONF_VM_64bit
+        lua_pushinteger(L, 64);
+        #else
+        lua_pushinteger(L, 32);
+        #endif
+        return 3;
     }
     return 1;
 }

+ 14 - 0
luat/modules/luat_lib_zbuff.c

@@ -144,6 +144,12 @@ static int l_zbuff_create(lua_State *L)
     }
     if (len <= 0) return 0;
 
+    if (len >= 64*1024) {
+        LLOGI("create large size: %d kbyte, trigger force GC", len / 1024);
+        lua_gc(L, LUA_GCCOLLECT, 0);
+        lua_gc(L, LUA_GCCOLLECT, 0);
+    }
+
     luat_zbuff_t *buff = (luat_zbuff_t *)lua_newuserdata(L, sizeof(luat_zbuff_t));
     if (buff == NULL) return 0;
 
@@ -154,6 +160,14 @@ static int l_zbuff_create(lua_State *L)
     }
     buff->addr = (uint8_t *)luat_heap_opt_malloc(buff->type,len);
     if (buff->addr == NULL){
+        // 尝试垃圾回收后再分配一次
+        LLOGI("create size: %d byte but memory not enough, trigger GC", len);
+        lua_gc(L, LUA_GCCOLLECT, 0);
+        lua_gc(L, LUA_GCCOLLECT, 0);
+        buff->addr = (uint8_t *)luat_heap_opt_malloc(buff->type,len);
+    }
+    if (buff->addr == NULL){
+        LLOGW("create size: %d byte but memory not enough!!!", len);
         lua_pushnil(L);
         lua_pushstring(L, "memory not enough");
         return 2;

+ 1 - 1
module/Air780E/demo/errDump/main.lua

@@ -1,6 +1,6 @@
 PROJECT = "errdump_test"
 VERSION = "1.0"
-PRODUCT_KEY = "s1uUnY6KA06ifIjcutm5oNbG3MZf5aUv" --换成自己的
+PRODUCT_KEY = "123" --换成自己的
 -- sys库是标配
 _G.sys = require("sys")
 _G.sysplus = require("sysplus")

+ 1 - 1
module/Air780E/demo/iconv/main.lua

@@ -1,7 +1,7 @@
 -- LuaTools需要PROJECT和VERSION这两个信息
 PROJECT = "my_test"
 VERSION = "1.2"
-PRODUCT_KEY = "s1uUnY6KA06ifIjcutm5oNbG3MZf5aUv" -- 换成自己的
+PRODUCT_KEY = "123" -- 换成自己的
 -- sys库是标配
 _G.sys = require("sys")
 _G.sysplus = require("sysplus")

+ 1 - 1
module/Air780E/demo/socket/EC618/main.lua

@@ -2,7 +2,7 @@
 -- LuaTools需要PROJECT和VERSION这两个信息
 PROJECT = "my_test"
 VERSION = "1.2"
-PRODUCT_KEY = "s1uUnY6KA06ifIjcutm5oNbG3MZf5aUv" --换成自己的
+PRODUCT_KEY = "123" --换成自己的
 -- sys库是标配
 _G.sys = require("sys")
 _G.sysplus = require("sysplus")

+ 1 - 1
module/Air780E/demo/socket/EC618_W5500/main.lua

@@ -2,7 +2,7 @@
 -- LuaTools需要PROJECT和VERSION这两个信息
 PROJECT = "ec618_w5500"
 VERSION = "1.2"
--- PRODUCT_KEY = "s1uUnY6KA06ifIjcutm5oNbG3MZf5aUv" --换成自己的
+-- PRODUCT_KEY = "123" --换成自己的
 
 -- sys库是标配
 _G.sys = require("sys")

+ 1 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1000/exeasyui/hw_drv/hw_default_font_drv.lua

@@ -10,8 +10,7 @@
 2、根据配置的字体、lcd和tp参数,初始化exEasyUI默认使用的字体、硬件显示和触摸;
 3、提供无需外部硬件的字体显示能力;
 
-本文件的对外接口有0个:
-1、require加载后自动执行初始化;
+本文件无对外接口,require加载后自动执行初始化;
 
 @api ui.hw_init(config)
 @summary 初始化exEasyUI硬件系统

+ 1 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1000/exeasyui/hw_drv/hw_gtfont_drv.lua

@@ -10,8 +10,7 @@
 2、根据配置的字体、lcd和tp参数,初始化exEasyUI默认使用的字体、硬件显示和触摸;
 3、提供高质量矢量字体显示能力;
 
-本文件的对外接口有0个:
-1、require加载后自动执行初始化;
+本文件无对外接口,require加载后自动执行初始化;
 
 @api ui.hw_init(config)
 @summary 初始化exEasyUI硬件系统

+ 1 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1000/exeasyui/hw_drv/hw_hzfont_drv.lua

@@ -10,8 +10,7 @@
 2、根据配置的字体、lcd和tp参数,初始化exEasyUI默认使用的字体、硬件显示和触摸;
 3、提供动态字体大小调整和高质量字体显示能力;
 
-本文件的对外接口有0个:
-1、require加载后自动执行初始化;
+本文件无对外接口,require加载后自动执行初始化;
 
 @api ui.hw_init(config)
 @summary 初始化exEasyUI硬件系统

+ 1 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1000/exeasyui/ui/ui_main.lua

@@ -12,8 +12,7 @@
 4、订阅按键事件并分发到按键处理器;
 5、启动UI渲染主循环,维持界面刷新;
 
-本文件的对外接口有0个:
-1、require加载后自动启动UI主任务;
+本文件无对外接口,require加载后自动执行初始化;
 ]]
 
 local home_page = require("home_page")

+ 1 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1010/exeasyui/hw_drv/hw_default_font_drv.lua

@@ -10,8 +10,7 @@
 2、根据配置的字体、lcd和tp参数,初始化exEasyUI默认使用的字体、硬件显示和触摸;
 3、提供无需外部硬件的字体显示能力;
 
-本文件的对外接口有0个:
-1、require加载后自动执行初始化;
+本文件无对外接口,require加载后自动执行初始化;
 
 @api ui.hw_init(config)
 @summary 初始化exEasyUI硬件系统

+ 1 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1010/exeasyui/hw_drv/hw_gtfont_drv.lua

@@ -10,8 +10,7 @@
 2、根据配置的字体、lcd和tp参数,初始化exEasyUI默认使用的字体、硬件显示和触摸;
 3、提供高质量矢量字体显示能力;
 
-本文件的对外接口有0个:
-1、require加载后自动执行初始化;
+本文件无对外接口,require加载后自动执行初始化;
 
 @api ui.hw_init(config)
 @summary 初始化exEasyUI硬件系统

+ 1 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1010/exeasyui/hw_drv/hw_hzfont_drv.lua

@@ -10,8 +10,7 @@
 2、根据配置的字体、lcd和tp参数,初始化exEasyUI默认使用的字体、硬件显示和触摸;
 3、提供动态字体大小调整和高质量字体显示能力;
 
-本文件的对外接口有0个:
-1、require加载后自动执行初始化;
+本文件无对外接口,require加载后自动执行初始化;
 
 @api ui.hw_init(config)
 @summary 初始化exEasyUI硬件系统

+ 1 - 1
module/Air780EHM_Air780EHV_Air780EGH/demo/accessory_board/AirLCD_1010/lcd/ui/home_page.lua

@@ -20,7 +20,7 @@ local home_page = {}
 -- 屏幕尺寸
 local width, height = lcd.getSize()
 
-local center_x = width / 2
+local center_x = math.floor(width / 2)
 
 -- 按钮区域定义
 local buttons = {

+ 1 - 1
module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/Air780EHM_Air780EGH/main.lua

@@ -27,7 +27,7 @@ PROJECT = "extalk"
 VERSION = "001.000.000"
 
 --到 iot.openluat.com 创建项目,获取正确的项目key
-PRODUCT_KEY =  "5544VIDOIHH9Nv8huYVyEIGT4tCvldxI"
+PRODUCT_KEY =  "123"
 
 --在日志中打印项目名和项目版本号
 log.info("main", PROJECT, VERSION)

+ 2 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/Air780EHM_Air780EGH/readme.md

@@ -88,7 +88,7 @@
 3、main.lua 中,修改 PRODUCT_KEY 。
  ``` lua
  --到 iot.openluat.com 创建项目,获取正确的项目key
- PRODUCT_KEY =  "5544VIDOIHH9Nv8huYVyEIGT4tCvldxI"
+ PRODUCT_KEY =  "123"
   ``` 
 
 4、talk.lua 中,修改目标设备终端ID。 
@@ -119,7 +119,7 @@
  I/talk.lua:205 extalk初始化成功
  I/talk.lua:207 对讲系统准备就绪
 ……
- I/extalk.lua:83 MQTT发布 - 主题: ctrl/uplink/866965083769676/0001 内容: {"key":"5544VIDOIHH9Nv8huYVyEIGT4tCvldxI","device_type":1}
+ I/extalk.lua:83 MQTT发布 - 主题: ctrl/uplink/866965083769676/0001 内容: {"key":"123","device_type":1}
  I/extalk.lua:83 MQTT发布 - 主题: ctrl/uplink/866965083769676/0002 内容:  
  I/talk.lua:37 联系人列表更新:
  I/talk.lua:39   1. ID: 861556079986013, 名称: 

+ 1 - 1
module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/Air780EHV/main.lua

@@ -26,7 +26,7 @@ VERSION:项目版本号,ascii string类型
 PROJECT = "extalk"
 VERSION = "001.000.000"
 --到 iot.openluat.com 创建项目,获取正确的项目key
-PRODUCT_KEY =  "5544VIDOIHH9Nv8huYVyEIGT4tCvldxI"
+PRODUCT_KEY =  "123"
 
 --在日志中打印项目名和项目版本号
 log.info("main", PROJECT, VERSION)

+ 2 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/Air780EHV/readme.md

@@ -80,7 +80,7 @@ Air780EHV核心板和AirAudio_1000 配件板的硬件接线方式为:
 3、main.lua 中,修改 PRODUCT_KEY 。
  ``` lua
  --到 iot.openluat.com 创建项目,获取正确的项目key
- PRODUCT_KEY =  "5544VIDOIHH9Nv8huYVyEIGT4tCvldxI"
+ PRODUCT_KEY =  "123"
   ``` 
 
 4、talk.lua 中,修改目标设备终端ID。 
@@ -111,7 +111,7 @@ Air780EHV核心板和AirAudio_1000 配件板的硬件接线方式为:
  I/talk.lua:205 extalk初始化成功
  I/talk.lua:207 对讲系统准备就绪
 ……
- I/extalk.lua:83 MQTT发布 - 主题: ctrl/uplink/866965083769676/0001 内容: {"key":"5544VIDOIHH9Nv8huYVyEIGT4tCvldxI","device_type":1}
+ I/extalk.lua:83 MQTT发布 - 主题: ctrl/uplink/866965083769676/0001 内容: {"key":"123","device_type":1}
  I/extalk.lua:83 MQTT发布 - 主题: ctrl/uplink/866965083769676/0002 内容:  
  I/talk.lua:37 联系人列表更新:
  I/talk.lua:39   1. ID: 861556079986013, 名称: 

+ 1 - 1
module/Air780EHM_Air780EHV_Air780EGH/demo/fota/fota2(使用libfota2扩展库)/iot_server/psm_power_fota.lua

@@ -15,7 +15,7 @@
 
 ]]
 -- 使用合宙iot平台时需要这个参数
-PRODUCT_KEY = "BnYk2BlYO30DiWra7q27wUmEarOiipHO" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
 --加在libfota2扩展库
 libfota2 = require "libfota2"
 

+ 1 - 1
module/Air780EHM_Air780EHV_Air780EGH/demo/fota/fota2(使用libfota2扩展库)/iot_server/update.lua

@@ -13,7 +13,7 @@
 5、在升级结果的回调函数中,根据升级结果进行处理;
 ]]
 -- 使用合宙iot平台时需要这个参数
-PRODUCT_KEY = "X1zBmxSd1H2Gy69DtAyNytmUe7dudGXm" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
 
 libfota2 = require "libfota2"
 

+ 1 - 1
module/Air780EHM_Air780EHV_Air780EGH/demo/fota/fota2(使用libfota2扩展库)/self_server/psm_power_fota.lua

@@ -15,7 +15,7 @@
 
 ]]
 -- 使用合宙iot平台时需要这个参数
-PRODUCT_KEY = "BnYk2BlYO30DiWra7q27wUmEarOiipHO" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+PRODUCT_KEY = "123" -- 到 iot.openluat.com 创建项目,获取正确的项目id
 --加在libfota2扩展库
 libfota2 = require "libfota2"
 

+ 0 - 75
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/combination/readme.md

@@ -1,75 +0,0 @@
-
-## 演示功能概述
-
-使用Air780EGH开发板,本示例主要是利用exgnss库,实现了几种不同的应用场景,
-
-第一种场景是:正常模式,第一步先是通过tcp_client_main文件连接服务器,然后第二步模块会配置GNSS参数,开启GNSS应用,第三步会开启一个60s的定时器,定时器每60s会打开一个60sTIMERRORSUC应用,第四步定位成功之后关闭GNSS,然后获取rmc获取经纬度数据,发送经纬度数据到服务器上。
-
-第二种场景是:低功耗模式,第一步先是通过tcp_client_main文件连接服务器,然后第二步模块会配置GNSS参数,开启GNSS应用,第三步会开启一个60s的定时器,定时器每60s会进入正常模式,打开一个60sTIMERRORSUC应用,第四步定位成功之后关闭GNSS,然后获取rmc获取经纬度数据,发送经纬度数据到服务器上,进入低功耗模式。
-
-第三种场景是:PSM+模式,唤醒之后第一步是配置GNSS参数,开启GNSS应用,第二步定位成功之后关闭GNSS,然后获取rmc获取经纬度数据,拼接唤醒信息和经纬度信息,连接服务器,然后把数据发送数据到服务器上,配置休眠唤醒定时器 ,进入飞行模式,然后进入PSM+模式。
-
-
-## 演示硬件环境
-
-1、Air780EGH开发板一块
-
-2、TYPE-C USB数据线一根
-
-3、gnss天线一根
-
-4、Air780EGH开发板和数据线的硬件接线方式为
-
-- Air780EGH开发板通过TYPE-C USB口供电;(整机开发板的拨钮开关拨到USB供电)
-
-- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
-
-
-## 演示软件环境
-
-1、Luatools下载调试工具
-
-2、[Air780EGH V2012版本固件](https://docs.openluat.com/air780egh/luatos/firmware/version/)
-
-## 演示核心步骤
-
-1、搭建好硬件环境
-
-2、通过Luatools将demo与固件烧录到核心板中
-
-3、烧录好后,板子开机将会在Luatools上看到如下打印:
-
-(1) 第一种场景演示:
-打开GNSS应用
-```lua
-[2025-08-13 17:52:14.073][000000000.420] I/user.全卫星开启
-[2025-08-13 17:52:14.105][000000000.420] I/user.agps开启
-
-```
-连接服务器成功回复:
-```lua
-[2025-08-13 17:52:16.557][000000006.653] I/user.tcp_client_main_task_func libnet.connect success
-```
-
-定位成功发送数据到服务器:
-```lua
-[2025-08-13 17:52:34.288][000000024.791] I/gnss Fixed 344787143 1141919779
-[2025-08-13 17:52:34.350][000000024.854] I/user.exgnss.statInd@1 true 2 normal 60 36 nil function: 0C7EFAA8
-[2025-08-13 17:52:34.367][000000024.855] I/user.exgnss.statInd@2 true 3 libagps 20 2 nil nil
-[2025-08-13 17:52:34.943][000000025.448] I/user.exgnss.timerFnc@1 2 normal 60 36 1
-[2025-08-13 17:52:34.961][000000025.448] I/user.TAGmode1_cb+++++++++ normal
-[2025-08-13 17:52:34.973][000000025.449] I/user.nmea rmc {"variation":0,"lat":3447.8713379,"min":52,"valid":true,"day":13,"lng":11419.1972656,"speed":1.0460000,"year":2025,"month":8,"sec":34,"hour":9,"course":15.3769999}
-[2025-08-13 17:52:34.984][000000025.450] I/user.exgnss.close 2 normal 60 function: 0C7EFAA8
-[2025-08-13 17:52:34.993][000000025.451] I/user.exgnss.timerFnc@2 3 libagps 20 2 nil
-[2025-08-13 17:52:35.010][000000025.452] I/user.DATA gnssnormal {"lat":3447.871338,"lng":11419.197266}
-[2025-08-13 17:52:35.026][000000025.453] I/user.tcp_client_main_task_func libnet.wait true true nil
-[2025-08-13 17:52:35.070][000000025.574] I/user.tcp_client_sender.proc send success
-```
-后续是循环这个操作,每60秒GNSS定位一次,每次定位成功后,通过TCP发送给服务器。
-
-(2) 第二种场景低功耗模式,第三种场景PSM+场景,可以直接用Air9000搭配看功耗分析,配合服务器看接收日志,目前没办法用USB线通过luatools看日志。
-
-
-
-
-

+ 5 - 4
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/combination/lowpower.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/lowpower.lua

@@ -5,7 +5,7 @@
 @date    2025.07.27
 @author  李源龙
 @usage
-使用Air780EGH开发板,外接GPS天线,起一个60s定位一次的定时器,唤醒模块60s一定位,
+使用Air780EGH核心板,外接GPS天线,起一个60s定位一次的定时器,唤醒模块60s一定位,
 然后定位成功获取到经纬度发送到服务器上面,然后进入休眠
 ]]
 
@@ -17,7 +17,8 @@ local function lowpower_cb(tag)
     log.info("nmea", "rmc", json.encode(exgnss.rmc(0)))
     local data=string.format('{"lat":%5f,"lng":%5f}', rmc.lat, rmc.lng)
     sys.publish("SEND_DATA_REQ", "gnsslowpower", data) --发送数据到服务器
-    pm.power(pm.WORK_MODE, 1)--进入低功耗模式
+    -- pm.power(pm.WORK_MODE, 1)--进入低功耗模式
+    -- pm.power(pm.WORK_MODE,1,1)--wifi进入低功耗模式
 end
 
 local function lower_open()
@@ -39,12 +40,12 @@ local function gnss_fnc()
         ----芯片解析星历文件需要10-30s,默认GNSS会开启20s,该逻辑如果不执行,会导致下一次GNSS开启定位是冷启动,
         ----定位速度慢,大概35S左右,所以默认开启,如果可以接受下一次定位是冷启动,可以把auto_open设置成false
         ----需要注意的是热启动在定位成功之后,需要再开启3s左右才能保证本次的星历获取完成,如果对定位速度有要求,建议这么处理
-        auto_open=false 
+        -- auto_open=false 
     }
     exgnss.setup(gnssotps)  --配置GNSS参数
     exgnss.open(exgnss.TIMERORSUC,{tag="lowpower",val=60,cb=lowpower_cb}) --打开一个60s的TIMERORSUC应用,该模式定位成功关闭
     sys.timerLoopStart(lower_open,60000)       --每60s开启一次GNSS
-    -- gpio.close(23)--此脚为gnss备电脚,功能是热启动和保存星历文件,关掉会没有热启动,常开功耗会增高10ua左右
+    -- gpio.close(24)--此脚为gnss备电脚和三轴加速度传感器的供电脚,功能是热启动和保存星历文件,关掉会没有热启动,常开功耗会增高0.5-1MA左右
     -- --关闭USB以后可以降低约150ua左右的功耗,如果不需要USB可以关闭
     pm.power(pm.USB, false)
 end

+ 3 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/combination/main.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/main.lua

@@ -7,7 +7,7 @@
 @author  李源龙
 @usage
 本demo演示的功能为:
-使用Air780EGH开发板,通过exgnss扩展库,开启GNSS定位,展示模块的三种功耗模式:正常模式,低功耗模式,PSM+模式 
+使用Air780EGH核心板,通过exgnss扩展库,开启GNSS定位,展示模块的三种功耗模式:正常模式,低功耗模式,PSM+模式 
 ]]
 
 --[[
@@ -59,7 +59,8 @@ exgnss=require("exgnss")
 require"normal"  --正常模式,搭配GNSS定时开启发送经纬度数据到服务器,不进入任何低功耗模式
 -- require"lowpower"    -- 低功耗模式,搭配GNSS定时开启发送经纬度数据到服务器,定位成功之后关闭GNSS进入低功耗模式
 -- require"psm"     -- PSM+模式,唤醒开启GNSS定位,定位成功之后发送经纬度数据到服务器,然后关闭GNSS进入PSM+模式
--- require"vibration"  -- 振动检测模式,搭配GNSS触发震动检测之后,持续发送经纬度数据到服务器
+
+-- require"vibration"  -- 振动检测模式,搭配GNSS触发震动检测之后,持续发送经纬度数据到服务器,该功能只有780EGG和780EGP支持,780EGH不支持该demo
 
 -- 用户代码已结束---------------------------------------------
 -- 结尾总是这一句

+ 4 - 4
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/combination/normal.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/normal.lua

@@ -5,7 +5,7 @@
 @date    2025.07.27
 @author  李源龙
 @usage
-使用Air780EGH开发板,外接GPS天线,定位然后发送经纬度数据给服务器,
+使用Air8000整机开发板,外接GPS天线,定位然后发送经纬度数据给服务器,
 起一个60s定位一次的定时器,模块60s一定位,然后定位成功获取到经纬度发送到服务器上面
 ]]
 
@@ -26,7 +26,7 @@ local function gnss_fnc()
     log.info("gnss_fnc111")
     local gnssotps={
         gnssmode=1, --1为卫星全定位,2为单北斗
-        -- agps_enable=true,    --是否使用AGPS,开启AGPS后定位速度更快,会访问服务器下载星历,星历时效性为北斗1小时,GPS4小时,默认下载星历的时间为1小时,即一小时内只会下载一次
+        agps_enable=true,    --是否使用AGPS,开启AGPS后定位速度更快,会访问服务器下载星历,星历时效性为北斗1小时,GPS4小时,默认下载星历的时间为1小时,即一小时内只会下载一次
         -- debug=true,    --是否输出调试信息
         -- uart=2,    --使用的串口,780EGH和8000默认串口2
         -- uartbaud=115200,    --串口波特率,780EGH和8000默认115200
@@ -39,8 +39,8 @@ local function gnss_fnc()
         -- auto_open=false 
     }
     exgnss.setup(gnssotps)  --配置GNSS参数
-    exgnss.open(exgnss.TIMERORSUC,{tag="normal",val=60,cb=normal_cb}) --打开一个60s的TIMERORSUC应用,该模式定位成功关闭
-    sys.timerLoopStart(normal_open,60000)       --每60s开启一次GNSS
+    exgnss.open(exgnss.TIMER,{tag="normal",val=30,cb=normal_cb}) --打开一个60s的TIMERORSUC应用,该模式定位成功关闭
+    -- sys.timerLoopStart(normal_open,60000)       --每60s开启一次GNSS
     
 end
 

+ 2 - 2
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/combination/psm.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/psm.lua

@@ -5,7 +5,7 @@
 @date    2025.07.27
 @author  李源龙
 @usage
-使用Air780EGH开发板,外接GPS天线,开启定位,获取到定位发送到服务器上面,然后启动一个60s的定时器唤醒PSM+模式
+使用Air780EGH核心板,外接GPS天线,开启定位,获取到定位发送到服务器上面,然后启动一个60s的定时器唤醒PSM+模式
 模块开启定位,然后定位成功获取到经纬度发送到服务器上面,然后进入PSM+模式,等待唤醒
 ]]
 pm.power(pm.WORK_MODE, 0) 
@@ -95,7 +95,7 @@ local function testTask(ip, port)
     uart.setup(1, 9600) -- 配置uart1,外部唤醒用
     
     -- 配置GPIO以达到最低功耗的目的
-	-- gpio.close(23) --此脚为gnss备电脚,功能是热启动和保存星历文件,关掉会没有热启动,常开功耗会增高10ua左右
+	-- gpio.close(24) --此脚为gnss备电脚和三轴加速度传感器的供电脚,功能是热启动和保存星历文件,关掉会没有热启动,常开功耗会增高0.5-1MA左右
 
     pm.dtimerStart(3, period) -- 启动深度休眠定时器
 

+ 155 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/readme.md

@@ -0,0 +1,155 @@
+
+## 演示功能概述
+
+使用Air780EGH核心板,本示例主要是利用exgnss库,实现了几种不同的应用场景,
+
+第一种场景是:正常模式,第一步先是通过tcp_client_main文件连接服务器,然后第二步模块会配置GNSS参数,开启GNSS应用,第三步会开启一个60s的定时器,定时器每60s会打开一个60sTIMERRORSUC应用,第四步定位成功之后关闭GNSS,然后获取rmc获取经纬度数据,发送经纬度数据到服务器上。
+
+第二种场景是:低功耗模式,第一步先是通过tcp_client_main文件连接服务器,然后第二步模块会配置GNSS参数,开启GNSS应用,第三步会开启一个60s的定时器,定时器每60s会进入正常模式,打开一个60sTIMERRORSUC应用,第四步定位成功之后关闭GNSS,然后获取rmc获取经纬度数据,发送经纬度数据到服务器上,进入低功耗模式。
+
+第三种场景是:PSM+模式,唤醒之后第一步是配置GNSS参数,开启GNSS应用,第二步定位成功之后关闭GNSS,然后获取rmc获取经纬度数据,拼接唤醒信息和经纬度信息,连接服务器,然后把数据发送数据到服务器上,配置休眠唤醒定时器 ,进入飞行模式,然后进入PSM+模式。
+
+第四种场景是:三轴加速度的应用场景,第一步先是通过tcp_client_main文件连接服务器,第二步配置GNSS参数,打开内部加速传感器,设置防抖和中断模式,关于中断触发提供了两种方案,有效震动模式和持续震动检测模式,第三步检测到有效震动或者持续震动后,打开GNSS,每隔5s获取rmc获取经纬度数据,发送经纬度数据到服务器上。
+
+## 演示硬件环境
+
+1、Air780EGH核心板一块
+
+2、TYPE-C USB数据线一根
+
+3、gnss天线一根
+
+4、Air780EGH核心板和数据线的硬件接线方式为
+
+- Air780EGH核心板通过TYPE-C USB口供电;
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air780GH V2018版本固件](https://docs.openluat.com/air780egh/luatos/firmware/version/)
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、通过Luatools将demo与固件烧录到核心板中
+
+3、烧录好后,板子开机将会在Luatools上看到如下打印:
+
+(1) 第一种场景演示:
+打开GNSS应用
+```lua
+[2025-08-13 17:52:14.073][000000000.420] I/user.全卫星开启
+[2025-08-13 17:52:14.105][000000000.420] I/user.agps开启
+
+```
+连接服务器成功回复:
+```lua
+[2025-08-13 17:52:16.557][000000006.653] I/user.tcp_client_main_task_func libnet.connect success
+```
+
+定位成功发送数据到服务器:
+```lua
+[2025-08-13 17:52:34.288][000000024.791] I/gnss Fixed 344787143 1141919779
+[2025-08-13 17:52:34.350][000000024.854] I/user.exgnss.statInd@1 true 2 normal 60 36 nil function: 0C7EFAA8
+[2025-08-13 17:52:34.367][000000024.855] I/user.exgnss.statInd@2 true 3 libagps 20 2 nil nil
+[2025-08-13 17:52:34.943][000000025.448] I/user.exgnss.timerFnc@1 2 normal 60 36 1
+[2025-08-13 17:52:34.961][000000025.448] I/user.TAGmode1_cb+++++++++ normal
+[2025-08-13 17:52:34.973][000000025.449] I/user.nmea rmc {"variation":0,"lat":3447.8713379,"min":52,"valid":true,"day":13,"lng":11419.1972656,"speed":1.0460000,"year":2025,"month":8,"sec":34,"hour":9,"course":15.3769999}
+[2025-08-13 17:52:34.984][000000025.450] I/user.exgnss.close 2 normal 60 function: 0C7EFAA8
+[2025-08-13 17:52:34.993][000000025.451] I/user.exgnss.timerFnc@2 3 libagps 20 2 nil
+[2025-08-13 17:52:35.010][000000025.452] I/user.DATA gnssnormal {"lat":3447.871338,"lng":11419.197266}
+[2025-08-13 17:52:35.026][000000025.453] I/user.tcp_client_main_task_func libnet.wait true true nil
+[2025-08-13 17:52:35.070][000000025.574] I/user.tcp_client_sender.proc send success
+```
+后续是循环这个操作,每60秒GNSS定位一次,每次定位成功后,通过TCP发送给服务器。
+
+(2) 第二种场景低功耗模式,第三种场景PSM+场景,可以直接用Air9000搭配看功耗分析,配合服务器看接收日志,目前没办法用USB线通过luatools看日志。
+
+
+(3)、三轴加速度的应用场景:
+联网成功连接服务器:
+```lua
+[2025-08-13 18:08:50.407][000000006.046] D/mobile NETIF_LINK_ON -> IP_READY
+[2025-08-13 18:08:50.442][000000006.047] I/user.tcp_client_main_task_func recv IP_READY
+[2025-08-13 18:08:50.469][000000006.048] D/socket connect to 112.125.89.8,47855
+[2025-08-13 18:08:50.489][000000006.068] D/mobile TIME_SYNC 0
+[2025-08-13 18:08:50.506][000000006.086] I/user.tcp_client_main_task_func libnet.connect success
+
+```
+有效震动触发场景开启GNSS:
+```lua
+[2025-08-13 18:11:01.380][000000137.275] I/user.int 1
+[2025-08-13 18:11:01.405][000000137.276] I/user.table.remove 0
+[2025-08-13 18:11:01.417][000000137.276] I/user.tick 136 false true
+[2025-08-13 18:11:01.433][000000137.276] I/user.tick2 0 0 0 11 136
+[2025-08-13 18:11:01.866][000000137.758] I/user.int 0
+[2025-08-13 18:11:02.005][000000137.893] I/user.int 1
+[2025-08-13 18:11:02.021][000000137.893] I/user.table.remove 0
+[2025-08-13 18:11:02.031][000000137.894] I/user.tick 137 false true
+[2025-08-13 18:11:02.044][000000137.894] I/user.tick2 0 0 11 136 137
+[2025-08-13 18:11:02.474][000000138.376] I/user.int 0
+[2025-08-13 18:11:02.741][000000138.635] I/user.int 1
+[2025-08-13 18:11:02.755][000000138.635] I/user.table.remove 0
+[2025-08-13 18:11:02.765][000000138.636] I/user.tick 138 false true
+[2025-08-13 18:11:02.780][000000138.636] I/user.tick2 0 11 136 137 138
+[2025-08-13 18:11:03.363][000000139.253] I/user.int 0
+[2025-08-13 18:11:03.537][000000139.438] I/user.int 1
+[2025-08-13 18:11:03.555][000000139.438] I/user.table.remove 0
+[2025-08-13 18:11:03.561][000000139.439] I/user.tick 139 false true
+[2025-08-13 18:11:03.574][000000139.439] I/user.tick2 11 136 137 138 139
+[2025-08-13 18:11:04.020][000000139.921] I/user.int 0
+[2025-08-13 18:11:04.586][000000140.478] I/user.int 0
+[2025-08-13 18:11:05.075][000000140.972] I/user.int 0
+[2025-08-13 18:11:05.202][000000141.093] I/user.tcp_client_main_task_func libnet.wait true false nil
+[2025-08-13 18:11:05.626][000000141.528] I/user.int 0
+[2025-08-13 18:11:06.145][000000142.035] I/user.int 1
+[2025-08-13 18:11:06.171][000000142.035] I/user.table.remove 11
+[2025-08-13 18:11:06.184][000000142.036] I/user.tick 141 true true
+[2025-08-13 18:11:06.192][000000142.036] I/user.tick2 136 137 138 139 141
+[2025-08-13 18:11:06.200][000000142.036] I/user.vib xxx
+[2025-08-13 18:11:06.210][000000142.037] I/user.nmea is_open false
+[2025-08-13 18:11:06.217][000000142.038] I/user.exgnss.open 1 vib nil function: 0C7EEE58
+[2025-08-13 18:11:06.228][000000142.038] Uart_ChangeBR 1338:uart2, 115200 115203 26000000 3611
+[2025-08-13 18:11:06.240][000000142.039] I/user.全卫星开启
+[2025-08-13 18:11:06.249][000000142.039] I/user.debug开启
+[2025-08-13 18:11:06.262][000000142.039] D/gnss Debug ON
+[2025-08-13 18:11:06.277][000000142.040] I/user.agps开启
+```
+触发之后每5s发送一次经纬度数据到服务器:
+```lua
+[2025-08-13 18:13:51.149][000000307.042] I/user.TAGmode1_cb+++++++++ nil
+[2025-08-13 18:13:51.152][000000307.044] I/user.nmea rmc {"variation":0,"lat":3447.8745117,"min":13,"valid":true,"day":13,"lng":11419.1962891,"speed":0.0730000,"year":2025,"month":8,"sec":50,"hour":10,"course":152.6990051}
+[2025-08-13 18:13:51.154][000000307.044] I/user.DATA gnssnormal {"lat":3447.874512,"lng":11419.196289}
+[2025-08-13 18:13:51.157][000000307.046] I/user.tcp_client_main_task_func libnet.wait true true nil
+[2025-08-13 18:13:51.241][000000307.129] I/user.tcp_client_sender.proc send success
+```
+
+持续震动检测模式,触发开始开启GNSS应用:
+```lua
+[2025-08-13 18:25:24.809][000000358.919] I/user.int 1
+[2025-08-13 18:25:24.822][000000358.921] I/user.x 0.35156250000000g y 0.31933593800000g z 1.9990234380000g
+[2025-08-13 18:25:24.833][000000358.922] I/user.nmea is_open false
+[2025-08-13 18:25:24.840][000000358.922] I/user.exgnss.open 1 vib nil function: 0C7EEEE0
+[2025-08-13 18:25:24.850][000000358.923] Uart_ChangeBR 1338:uart2, 115200 115203 26000000 3611
+[2025-08-13 18:25:24.863][000000358.924] I/user.全卫星开启
+[2025-08-13 18:25:24.875][000000358.924] I/user.debug开启
+[2025-08-13 18:25:24.886][000000358.924] D/gnss Debug ON
+[2025-08-13 18:25:24.900][000000358.925] I/user.agps开启
+```
+
+触发之后每5s发送一次经纬度数据到服务器:
+```lua
+[2025-08-13 18:25:32.220][000000366.338] I/user.exgnss.timerFnc@1 1 vib nil nil 1
+[2025-08-13 18:25:32.225][000000366.339] I/user.TAGmode1_cb+++++++++ vib
+[2025-08-13 18:25:32.233][000000366.340] I/user.nmea rmc {"variation":0,"lat":3447.8679199,"min":25,"valid":true,"day":13,"lng":11419.1962891,"speed":0.2490000,"year":2025,"month":8,"sec":32,"hour":10,"course":175.2010040}
+[2025-08-13 18:25:32.239][000000366.341] I/user.DATA gnssnormal {"lat":3447.867920,"lng":11419.196289}
+[2025-08-13 18:25:32.242][000000366.342] I/user.tcp_client_main_task_func libnet.wait true true nil
+[2025-08-13 18:25:32.353][000000366.473] I/user.tcp_client_sender.proc send success
+```
+
+

+ 1 - 1
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/combination/tcp_client_main.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/tcp_client_main.lua

@@ -24,7 +24,7 @@ local tcp_client_sender = require "tcp_client_sender"
 -- 点击 打开TCP 按钮,会创建一个TCP server
 -- 将server的地址和端口赋值给下面这两个变量
 local SERVER_ADDR = "112.125.89.8"
-local SERVER_PORT = 44333
+local SERVER_PORT = 32066
 
 -- tcp_client_main的任务名
 local TASK_NAME = tcp_client_sender.TASK_NAME

+ 0 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/combination/tcp_client_receiver.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/tcp_client_receiver.lua


+ 0 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/combination/tcp_client_sender.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/tcp_client_sender.lua


+ 165 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/combination/vibration.lua

@@ -0,0 +1,165 @@
+--[[
+@module  vibration
+@summary 利用加速度传感器da221实现中断触发gnss定位
+@version 1.0
+@date    2025.08.01
+@author  李源龙
+@usage
+注意:该demo只有780EGG/Air780EGP支持,780EGH不支持,因为没有内置da221加速度传感器
+使用Air780EGG/Air780EGP利用内置的da221加速度传感器实现震动中断触发gnss定位,给了两种方式,
+第一种是加速度传感器震动之后触发,触发之后开始打开gnss,进行定位,定位成功之后每5s上传一次经纬度数据,
+假设10s内没有再触发震动,则关闭gnss,等待下一次震动触发
+
+第二种方式是震动触发之后计算为一次触发,如果10s内触发5次有效震动,则开启后续逻辑,如果10s内没有触发5次,
+则判定为无效震动,等待下一次触发,如果是有效震动就打开gnss,进行定位,
+定位成功之后每5s上传一次经纬度数据到服务器,有效震动触发之后有30分钟的等待时间,在此期间,如果触发有效震动则
+不去进行后续逻辑处理,30分钟时间到了之后,等待下一次有效震动
+具体使用哪种方式可以根据实际需求选择
+]]
+
+exvib=require("exvib")
+tcp_client_main=require("tcp_client_main")
+
+local intPin=gpio.WAKEUP2   --中断检测脚,内部固定wakeup2
+local tid   --获取定时打开的定时器id
+local num=0 --计数器 
+local ticktable={0,0,0,0,0} --存放5次中断的tick值,用于做有效震动对比
+local eff=false --有效震动标志位,用于判断是否触发定位
+
+local function vib_cb(tag)
+    log.info("TAGmode1_cb+++++++++",tag)
+     local  rmc=exgnss.rmc(0)    --获取rmc数据
+    log.info("nmea", "rmc", json.encode(exgnss.rmc(0)))
+    local data=string.format('{"lat":%5f,"lng":%5f}', rmc.lat, rmc.lng)
+    sys.publish("SEND_DATA_REQ", "gnssnormal", data) --发送数据到服务器
+
+end
+--定位成功就5s发送一包数据到服务器
+local function gnss_state(event, ticks)
+    -- event取值有
+    -- "FIXED":string类型 定位成功
+    -- "LOSE": string类型 定位丢失
+    -- "CLOSE": string类型 GNSS关闭,仅配合使用gnss.lua有效
+
+    -- ticks number类型 是事件发生的时间,一般可以忽略
+    log.info("exgnss", "state", event)
+    if event=="FIXED" then
+        log.info("定位成功")
+    end
+end
+
+sys.subscribe("GNSS_STATE",gnss_state)
+
+--有效震动模式
+--tick计数器,每秒+1用于存放5次中断的tick值,用于做有效震动对比
+-- local function tick()
+--     num=num+1
+-- end
+-- --每秒运行一次计时
+-- sys.timerLoopStart(tick,1000)
+
+-- --有效震动判断
+-- local function ind()
+--     log.info("int", gpio.get(intPin))
+--     if gpio.get(intPin) == 1 then
+--         --接收数据如果大于5就删掉第一个
+--         if #ticktable>=5 then
+--             log.info("table.remove",table.remove(ticktable,1))
+--         end
+--         --存入新的tick值
+--         table.insert(ticktable,num)
+--         log.info("tick",num,(ticktable[5]-ticktable[1]<10),ticktable[5]>0)
+--         log.info("tick2",ticktable[1],ticktable[2],ticktable[3],ticktable[4],ticktable[5])
+--         --表长度为5且,第5次中断时间间隔减去第一次间隔小于10s,且第5次值为有效值
+--         if #ticktable>=5 and (ticktable[5]-ticktable[1]<10 and ticktable[1]>0) then
+--             log.info("vib", "xxx")
+--             --是否要去触发有效震动逻辑
+--             if eff==false then
+--                 sys.publish("EFFECTIVE_VIBRATION")
+--             end
+--         end
+--     end
+-- end
+
+-- --设置30s分钟之后再判断是否有效震动函数
+-- local function num_cb()
+--     eff=false
+-- end
+
+-- local function eff_vib()
+--     --触发之后eff设置为true,30分钟之后再触发有效震动
+--     eff=true
+--     --30分钟之后再触发有效震动
+--     sys.timerStart(num_cb,180000)
+--     --判断gnss是否处于打开状态
+--     if exgnss.is_active(exgnss.DEFAULT,{tag="vib"})~=true then
+--         log.info("nmea", "is_open", "false")
+--         exgnss.open(exgnss.DEFAULT,{tag="vib",cb=vib_cb}) 
+--         tid=sys.timerLoopStart(vib_cb, 5000)
+--     else
+--         log.info("nmea", "is_open", "true")
+--     end
+-- end
+
+-- sys.subscribe("EFFECTIVE_VIBRATION",eff_vib)
+
+
+
+--持续震动模式
+--10s没有触发中断就停止
+local function vib_close()
+    exgnss.close(exgnss.DEFAULT,{tag="vib"})
+    sys.timerStop(tid)
+end
+
+--持续震动模式中断函数
+local function ind()
+    log.info("int", gpio.get(intPin))
+    if gpio.get(intPin) == 1 then
+        --10s没有触发中断就停止
+        sys.timerStart(vib_close,10000)
+        local x,y,z =  exvib.read_xyz()      --读取x,y,z轴的数据
+        log.info("x", x..'g', "y", y..'g', "z", z..'g')
+        --判断gnss是否处于打开状态
+        if exgnss.is_active(exgnss.DEFAULT,{tag="vib"})~=true then
+            log.info("nmea", "is_open", "false")
+            exgnss.open(exgnss.DEFAULT,{tag="vib",cb=vib_cb}) 
+            tid=sys.timerLoopStart(vib_cb, 5000)
+        else
+            log.info("nmea", "is_open", "true")
+        end
+    end
+end
+
+
+local function gnss_fnc()
+    log.info("gnss_fnc111")
+    local gnssotps={
+        gnssmode=1, --1为卫星全定位,2为单北斗
+        agps_enable=true,    --是否使用AGPS,开启AGPS后定位速度更快,会访问服务器下载星历,星历时效性为北斗1小时,GPS4小时,默认下载星历的时间为1小时,即一小时内只会下载一次
+        debug=true,    --是否输出调试信息
+        -- uart=2,    --使用的串口,780EGH和8000默认串口2
+        -- uartbaud=115200,    --串口波特率,780EGH和8000默认115200
+        -- bind=1, --绑定uart端口进行GNSS数据读取,是否设置串口转发,指定串口号
+        -- rtc=false    --定位成功后自动设置RTC true开启,flase关闭
+        ----因为GNSS使用辅助定位的逻辑,是模块下载星历文件,然后把数据发送给GNSS芯片,
+        ----芯片解析星历文件需要10-30s,默认GNSS会开启20s,该逻辑如果不执行,会导致下一次GNSS开启定位是冷启动,
+        ----定位速度慢,大概35S左右,所以默认开启,如果可以接受下一次定位是冷启动,可以把auto_open设置成false
+        ----需要注意的是热启动在定位成功之后,需要再开启3s左右才能保证本次的星历获取完成,如果对定位速度有要求,建议这么处理
+        -- auto_open=false 
+    }
+    exgnss.setup(gnssotps)
+    -- 1,微小震动检测,用于检测轻微震动的场景,例如用手敲击桌面;加速度量程2g;
+    -- 2,运动检测,用于电动车或汽车行驶时的检测和人行走和跑步时的检测;加速度量程4g;
+    -- 3,跌倒检测,用于人或物体瞬间跌倒时的检测;加速度量程8g;
+    --打开震动检测功能
+    exvib.open(1)
+    --设置gpio防抖100ms
+    gpio.debounce(intPin, 100)
+    --设置gpio中断触发方式wakeup2唤醒脚默认为双边沿触发
+    gpio.setup(intPin, ind)
+
+end
+
+sys.taskInit(gnss_fnc)
+

+ 0 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/single/gnss.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/single/gnss.lua


+ 1 - 1
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/single/main.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/single/main.lua

@@ -7,7 +7,7 @@
 @author  李源龙
 @usage
 本demo演示的功能为:
-使用Air780EGH开发板,通过exgnss.lua扩展库,开启GNSS定位,展示模块的三种应用状态
+使用Air780EGH核心板,通过exgnss.lua扩展库,开启GNSS定位,展示模块的三种应用状态
 ]]
 
 --[[

+ 5 - 5
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/single/readme.md → module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/exgnss/single/readme.md

@@ -1,7 +1,7 @@
 
 ## 演示功能概述
 
-使用Air780EGH开发板,本示例主要是展示exgnss库的三种应用模式,
+使用Air780EGH核心板,本示例主要是展示exgnss库的三种应用模式,
 
 exgnss.DEFAULT模式
 
@@ -25,15 +25,15 @@ exgnss.TIMER模式
 
 ## 演示硬件环境
 
-1、Air780EGH开发板一块
+1、Air780EGH核心板一块
 
 2、TYPE-C USB数据线一根
 
 3、gnss天线一根
 
-4、Air780EGH开发板和数据线的硬件接线方式为
+4、Air780EGH核心板和数据线的硬件接线方式为
 
-- Air780EGH开发板通过TYPE-C USB口供电;(整机开发板的拨钮开关拨到USB供电)
+- Air780EGH核心板通过TYPE-C USB口供电;
 
 - TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
 
@@ -42,7 +42,7 @@ exgnss.TIMER模式
 
 1、Luatools下载调试工具
 
-2、[Air780EGH V2012版本固件](https://docs.openluat.com/air780egh/luatos/firmware/version/)
+2、[Air780GH V2018版本固件](https://docs.openluat.com/air780egh/luatos/firmware/version/)
 
 ## 演示核心步骤
 

+ 179 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/libgnss/gnss.lua

@@ -0,0 +1,179 @@
+--[[
+@module  gnss
+@summary gnss应用测试功能模块
+@version 1.0
+@date    2025.07.27
+@author  李源龙
+@usage
+主要功能:
+1、开启gnss定位,使用agps辅助定位
+2、定位成功获取经纬度
+
+]]
+local lbsLoc2 = require("lbsLoc2")
+--设置串口2
+local gps_uart_id = 2
+
+--设置gnss的模式为1,1为全卫星开启,2为单北斗开启
+local gnssmode=1
+
+
+local function agps()
+    if libgnss.isFix() then return end
+    local lat, lng
+    
+    -- 判断星历时间和下载星历   
+    while not socket.adapter(socket.dft()) do
+        log.warn("airlbs_multi_cells_wifi_func", "wait IP_READY", socket.dft())
+        -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
+        -- 或者等待1秒超时退出阻塞等待状态;
+        -- 注意:此处的1000毫秒超时不要修改的更长;
+        -- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
+        -- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
+        -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
+        local result=sys.waitUntil("IP_READY", 30000)
+        if result == false then
+            log.warn("gnss_agps", "wait IP_READY timeout")
+            return
+        end
+    end
+    socket.sntp()
+    sys.waitUntil("NTP_UPDATE", 5000)
+    local now = os.time()
+    local agps_time = tonumber(io.readFile("/hxxt_tm") or "0") or 0
+    log.info("os.time",now)
+    log.info("agps_time",agps_time)
+    if now - agps_time > 3600 or io.fileSize("/hxxt.dat") < 1024 then
+        local url 
+
+        if gnssmode and 2 == gnssmode then
+            -- 单北斗
+            url = "http://download.openluat.com/9501-xingli/HXXT_BDS_AGNSS_DATA.dat"
+        else
+            url = "http://download.openluat.com/9501-xingli/HXXT_GPS_BDS_AGNSS_DATA.dat"
+        end
+        local code = http.request("GET", url, nil, nil, {dst="/hxxt.dat"}).wait()
+        if code and code == 200 then
+            log.info("exgnss.opts", "下载星历成功", url)
+            io.writeFile("/hxxt_tm", tostring(now))
+        else
+            log.info("exgnss.opts", "下载星历失败", code)
+        end
+    else
+        log.info("exgnss.opts", "星历不需要更新", now - agps_time)
+    end
+    --进行基站定位,给到gnss芯片一个大概的位置
+
+    lat, lng = lbsLoc2.request(5000)
+    -- local lat, lng, t = lbsLoc2.request(5000, "bs.openluat.com")
+    -- log.info("lbsLoc2", lat, lng)
+    if lat and lng then
+        lat = tonumber(lat)
+        lng = tonumber(lng)
+        log.info("lbsLoc2", lat, lng)
+        -- 转换单位
+        local lat_dd,lat_mm = math.modf(lat)
+        local lng_dd,lng_mm = math.modf(lng)
+        lat = lat_dd * 100 + lat_mm * 60
+        lng = lng_dd * 100 + lng_mm * 60
+    end
+
+    --获取基站定位失败则使用本地之前保存的位置
+    if not lat then
+        -- 获取最后的本地位置
+        local locStr = io.readFile("/hxxtloc")
+        if locStr then
+            local jdata = json.decode(locStr)
+            if jdata and jdata.lat then
+                lat = jdata.lat
+                lng = jdata.lng
+            end
+        end
+    end
+
+    -- 写入星历
+    local agps_data = io.readFile("/hxxt.dat")
+    if agps_data and #agps_data > 1024 then
+        log.info("exgnss.opts", "写入星历数据", "长度", #agps_data)
+        for offset=1,#agps_data,512 do
+            log.info("exgnss", "AGNSS", "write >>>", #agps_data:sub(offset, offset + 511))
+            uart.write(gps_uart_id, agps_data:sub(offset, offset + 511))
+            sys.wait(100) -- 等100ms反而更成功
+        end
+        -- uart.write(gps_uart_id, agps_data)
+    else
+        log.info("exgnss.opts", "没有星历数据")
+        return
+    end
+    -- "lat":23.4068813,"min":27,"valid":true,"day":27,"lng":113.2317505
+    --如果没有经纬度的话,定位时间会变长,大概10-20s左右
+    if not lat or not lng then
+        -- lat, lng = 23.4068813, 113.2317505
+        log.info("exgnss.opts", "没有GPS坐标", lat, lng)
+        return --暂时不写入参考位置
+    else
+        log.info("exgnss.opts", "写入GPS坐标", lat, lng)
+    end
+    --写入时间
+    local date = os.date("!*t")
+    if date.year > 2023 then
+        local str = string.format("$AIDTIME,%d,%d,%d,%d,%d,%d,000", date["year"], date["month"], date["day"],
+            date["hour"], date["min"], date["sec"])
+        log.info("exgnss.opts", "参考时间", str)
+        uart.write(gps_uart_id, str .. "\r\n")
+        sys.wait(20)
+    end
+    -- 写入参考位置
+    local str = string.format("$AIDPOS,%.7f,%s,%.7f,%s,1.0\r\n",
+    lat > 0 and lat or (0 - lat), lat > 0 and 'N' or 'S',
+    lng > 0 and lng or (0 - lng), lng > 0 and 'E' or 'W')
+    log.info("exgnss.opts", "写入AGPS参考位置", str)
+    uart.write(gps_uart_id, str)
+
+end
+
+
+local function gnss_open()
+    log.info("GPS", "start")
+    libgnss.clear() -- 清空数据,兼初始化
+    --设置串口波特率
+    uart.setup(gps_uart_id, 115200)
+    -- 打开GPS
+    pm.power(pm.GPS, true)
+    -- 绑定uart,底层自动处理GNSS数据
+    -- 第二个参数是转发到虚拟UART, 方便上位机分析
+    libgnss.bind(gps_uart_id, uart.VUART_0)
+    sys.wait(200) -- GPNSS芯片启动需要时间
+    -- 调试日志,可选
+    libgnss.debug(true)
+
+    -- 增加显示的语句
+    uart.write(gps_uart_id, "$CFGMSG,0,1,1\r\n") -- GLL
+    sys.wait(20)
+    uart.write(gps_uart_id, "$CFGMSG,0,5,1\r\n") -- VTG
+    sys.wait(20)
+    uart.write(gps_uart_id, "$CFGMSG,0,6,1\r\n") -- ZDA
+    sys.wait(20)
+    sys.taskInit(agps)
+
+end
+
+sys.taskInit(gnss_open)
+
+
+--GNSS定位状态的消息处理函数:
+local function gnss_state(event, ticks)
+    -- event取值有
+    -- "FIXED":string类型 定位成功
+    -- "LOSE": string类型 定位丢失
+
+    -- ticks number类型 是事件发生的时间,一般可以忽略
+    log.info("exgnss", "state", event)
+    if event=="FIXED" then
+        --获取rmc数据
+        --json.encode默认输出"7f"格式保留7位小数,可以根据自己需要的格式调整小数位,本示例保留5位小数
+        log.info("nmea", "rmc0", json.encode(libgnss.getRmc(0),"5f"))
+    end
+end
+
+sys.subscribe("GNSS_STATE",gnss_state)

+ 61 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/libgnss/main.lua

@@ -0,0 +1,61 @@
+
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.07.27
+@author  李源龙
+@usage
+本demo演示的功能为:
+使用Air780EGH核心板,通过libgnss核心库,实现GNSS定位功能
+]]
+
+--[[
+必须定义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 = "gnsstest"
+VERSION = "001.000.000"
+
+--添加硬狗防止程序卡死
+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)
+
+require"gnss"
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!

+ 67 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/libgnss/readme.md

@@ -0,0 +1,67 @@
+
+## 演示功能概述
+
+使用Air780EGH核心板,利用libgnss核心库如何实现定位功能
+
+主要操作为:
+
+打开GNSS,使用AGPS辅助定位
+
+定位成功之后,获取经纬度信息
+
+## 演示硬件环境
+
+1、Air780EGH核心板一块
+
+2、TYPE-C USB数据线一根
+
+3、gnss天线一根
+
+4、Air780EGH核心板和数据线的硬件接线方式为
+
+- Air780EGH核心板通过TYPE-C USB口供电;
+
+- TYPE-C USB数据线直接插到核心板的TYPE-C USB座子,另外一端连接电脑USB口;
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air780GH V2018版本固件](https://docs.openluat.com/air780egh/luatos/firmware/version/)
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、通过Luatools将demo与固件烧录到核心板中
+
+3、烧录好后,板子开机将会在Luatools上看到如下打印:
+
+(1) GNSS开启:
+
+```lua
+[2025-12-09 17:43:15.076][000000000.715] I/user.GPS start
+[2025-12-09 17:43:15.083][000000000.716] Uart_ChangeBR 1338:uart2, 115200 115203 26000000 3611
+[2025-12-09 17:43:15.089][000000000.916] D/gnss Debug ON
+[2025-12-09 17:43:15.099][000000000.924] D/gnss >> #CFGMSG,0,1,1
+$OK*04
+
+[2025-12-09 17:43:15.108][000000000.944] D/gnss >> #CFGMSG,0,5,1
+$OK*04
+
+[2025-12-09 17:43:15.112][000000000.964] D/gnss >> #CFGMSG,0,6,1
+$OK*04
+
+
+```
+
+(2) GNSS定位成功:
+
+```lua
+[2025-12-09 17:43:29.766][000000016.652] I/gnss Fixed 343482711 1135040098
+[2025-12-09 17:43:29.770][000000016.654] I/user.exgnss state FIXED
+[2025-12-09 17:43:29.776][000000016.655] I/user.nmea rmc0 {"variation":0,"lat":3434.82715,"min":43,"valid":true,"day":9,"lng":11350.40137,"speed":0.59900,"year":2025,"month":12,"sec":31,"hour":9,"course":226.62500}
+
+
+```

+ 7 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/gnss/readme.md

@@ -0,0 +1,7 @@
+## exgnss和libgnss的区别
+exgnss:主要利用exgnss库去实现整个功能,主要包含两个目录,single和combination,single是单点定位,展示了exngss的三种模式。combination是融合了各种场景的GNSS定位,主要包括正常模式下的定时开启GNSS,定位成功把经纬度发到服务器的功能,低功耗模式下定时开启GNSS,定位成功把经纬度发到服务器的功能,以及PSM+模式下,定时唤醒模块,定位成功把经纬度发到服务器的功能,还包括了利用8000内置的三轴加速度传感器,通过震动检测来执行GNSS定位的功能。
+
+libngss:主要利用libgnss核心库实现的整个功能,主要功能包括打开GNSS,开启AGPS辅助定位,定位成功获取经纬度。
+
+## 使用推荐
+推荐使用exgnss,因为exgnss在libgnss核心库的基础上,对gnss的开和关做了函数优化,不需要繁琐的步骤进行开和关,同时加入了gnss应用的概念,可以选择常开或者定时开启,支持不同场景的应用方式,让用户在开发的过程中更简单。

+ 0 - 155
module/Air780EHM_Air780EHV_Air780EGH/demo/u8g2/main.lua

@@ -1,155 +0,0 @@
---- 模块功能:u8g2demo
--- @module u8g2
--- @author Dozingfiretruck
--- @release 2021.01.25
--- LuaTools需要PROJECT和VERSION这两个信息
-PROJECT = "u8g2demo"
-VERSION = "1.0.1"
-
-log.info("main", PROJECT, VERSION)
-
--- sys库是标配
-_G.sys = require("sys")
-
---[[接线方式  780EPM开发板----------------------------------SSD1306
-LCD_VCC(LCD那组引脚以sim卡卡槽旁的LCD母排为1的第二个排母孔) ---(VCC)
-I2C1_SCL(CAMERA_SCL)----------------------------------------(SCL)
-I2C1_SDA(CAMERA_SDA)----------------------------------------(SDA)
-GND---------------------------------------------------------(GND)
-]]
-
--- 添加硬狗防止程序卡死
-wdt.init(9000) -- 初始化watchdog设置为9s
-sys.timerLoopStart(wdt.feed, 3000) -- 3s喂一次狗
-
--- gpio.setup(14, nil) -- 关闭GPIO14,防止camera复用关系出问题
--- gpio.setup(15, nil) -- 关闭GPIO15,防止camera复用关系出问题
-
-
-local rtos_bsp = rtos.bsp()
-
--- hw_i2c_id,sw_i2c_scl,sw_i2c_sda,spi_id,spi_res,spi_dc,spi_cs
-function u8g2_pin()
-    if string.find(rtos_bsp, "780EPM") or string.find(rtos_bsp, "718PM") then
-        return 1, 14, 15, 0, 14, 10, 8
-    else
-        log.info("main", "你用的不是780EPM 请更换demo测试")
-        return
-    end
-end
-
-local hw_i2c_id, sw_i2c_scl, sw_i2c_sda, spi_id, spi_res, spi_dc, spi_cs = u8g2_pin()
-
--- 日志TAG, 非必须
-local TAG = "main"
-local chinese =true
--- 主流程
-sys.taskInit(function()
-
-    gpio.setup(2, 1) -- GPIO2打开给camera电源供电
-    gpio.setup(28, 1) -- 1.2版本 GPIO28打开给lcd电源供电
-    gpio.setup(29, 1) -- 1.3硬件版本 GPIO29打开给lcd电源供电
-    sys.wait(2000)
-
-    -- 初始化显示屏
-    log.info(TAG, "init ssd1306")
-
-    -- 初始化硬件i2c的ssd1306
-    log.info("setup SSD1306", u8g2.begin({
-        ic = "ssd1306",
-        direction = 0,
-        mode = "i2c_hw",
-        i2c_id = hw_i2c_id
-    })) -- direction 可选0 90 180 270
-
-    log.info("设置字体模式", u8g2.SetFontMode(1))
-    log.info("清屏", u8g2.ClearBuffer())
-    log.info("设置字体为 oppo字体", u8g2.SetFont(u8g2.font_opposansm8))
-    log.info("在显示屏上展示U8G2+LUATOS", u8g2.DrawUTF8("U8G2+LUATOS", 32, 22))
-
-    if u8g2.font_opposansm12_chinese then
-        u8g2.SetFont(u8g2.font_opposansm12_chinese)
-    elseif u8g2.font_opposansm10_chinese then
-        u8g2.SetFont(u8g2.font_opposansm10_chinese)
-    elseif u8g2.font_sarasa_m12_chinese then
-        u8g2.SetFont(u8g2.font_sarasa_m12_chinese)
-    elseif u8g2.font_sarasa_m10_chinese then
-        u8g2.SetFont(u8g2.font_sarasa_m10_chinese)
-    else
-        print("没有中文字库")
-        chinese = false
-    end
-
-    if chinese then
-    log.info("在显示屏显示中文", u8g2.DrawUTF8("中文测试", 40, 38)) -- 若中文不显示或乱码,代表所刷固件不带这个字号的字体数据
-        
-    end
-    log.info("将存储器帧缓冲区的内容发送到显示器", u8g2.SendBuffer())
-    sys.wait(2000)
-    u8g2.ClearBuffer()
-    if chinese then
-        u8g2.DrawUTF8("屏幕宽度:" .. u8g2.GetDisplayWidth(), 40, 24)
-        u8g2.DrawUTF8("屏幕高度:" .. u8g2.GetDisplayHeight(), 40, 42)
-    else
-        u8g2.DrawUTF8("width:" .. u8g2.GetDisplayWidth(), 40, 24)
-        u8g2.DrawUTF8("height:" .. u8g2.GetDisplayHeight(), 40, 42)
-    end
-    sys.wait(5000)
-    u8g2.SendBuffer()
-
-    u8g2.ClearBuffer()
-    u8g2.DrawUTF8("画线测试:", 30, 24)
-    for i = 0, 128, 8 do
-        u8g2.DrawLine(0, 40, i, 40)
-        u8g2.DrawLine(0, 60, i, 60)
-        u8g2.SendBuffer()
-        sys.wait(100)
-    end
-
-    sys.wait(1000)
-    u8g2.ClearBuffer()
-    u8g2.DrawUTF8("画圆测试:", 30, 24)
-    u8g2.DrawCircle(30, 50, 10, 15)
-    u8g2.DrawDisc(90, 50, 10, 15)
-    u8g2.SendBuffer()
-
-    sys.wait(1000)
-    u8g2.ClearBuffer()
-    u8g2.DrawUTF8("椭圆测试:", 30, 24)
-    u8g2.DrawEllipse(30, 50, 6, 10, 15)
-    u8g2.DrawFilledEllipse(90, 50, 6, 10, 15)
-    u8g2.SendBuffer()
-
-    sys.wait(1000)
-    u8g2.ClearBuffer()
-    u8g2.DrawUTF8("方框测试:", 30, 24)
-    u8g2.DrawBox(30, 40, 30, 24)
-    u8g2.DrawFrame(90, 40, 30, 24)
-    u8g2.SendBuffer()
-
-    sys.wait(1000)
-    u8g2.ClearBuffer()
-    u8g2.DrawUTF8("圆角方框:", 30, 24)
-    u8g2.DrawRBox(30, 40, 30, 24, 8)
-    u8g2.DrawRFrame(90, 40, 30, 24, 8)
-    u8g2.SendBuffer()
-
-    sys.wait(1000)
-    u8g2.ClearBuffer()
-    u8g2.DrawUTF8("三角测试:", 30, 24)
-    u8g2.DrawTriangle(30, 60, 60, 30, 90, 60)
-    u8g2.SendBuffer()
-
-    -- qrcode测试
-    sys.wait(1000)
-    u8g2.ClearBuffer()
-    u8g2.DrawDrcode(4, 4, "https://docs.openluat.com", 30);
-
-    u8g2.SendBuffer()
-
-    -- sys.wait(1000)
-    log.info("main", "u8g2 demo done")
-end)
-
--- 主循环, 必须加
-sys.run()

+ 50 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/ht1621/ht1621_drv.lua

@@ -0,0 +1,50 @@
+--[[
+@module  ht1621_drv
+@summary HT1621段码屏驱动模块 - 仅初始化
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为HT1621段码屏驱动初始化模块,仅包含初始化功能:
+1、初始化ht1621液晶屏
+2、返回seg对象供其他模块使用
+
+本文件的对外接口有:
+1、ht1621_drv.init():初始化HT1621驱动并返回seg对象
+]] 
+
+local ht1621_drv = {}
+
+--[[
+初始化HT1621驱动
+@api ht1621_drv.init()
+@summary 初始化HT1621液晶屏
+@return table seg对象,初始化成功返回seg,失败返回nil
+@usage
+seg = ht1621_drv.init()
+if seg then
+    log.info("HT1621驱动初始化成功")
+end
+]] 
+function ht1621_drv.init()
+    -- 初始化HT1621 (CS=22, DATA=24, WR=1)
+    seg = ht1621.setup(22, 24, 1)
+    
+    if not seg then
+        log.error("ht1621_drv", "HT1621初始化失败")
+        return nil
+    end
+    
+    -- 打开LCD显示
+    ht1621.lcd(seg, true)
+
+    -- 清屏
+    for i = 0, 11 do
+        ht1621.data(seg, i, 0x00)
+    end
+    
+    log.info("ht1621_drv", "HT1621初始化完成")
+    return seg
+end
+
+return ht1621_drv

+ 61 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/ht1621/key_drv.lua

@@ -0,0 +1,61 @@
+--[[
+@module  key_drv
+@summary 按键驱动模块
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为按键驱动功能模块,核心业务逻辑为:
+1、初始化BOOT键和PWR键的GPIO;
+2、配置按键事件的中断处理函数;
+3、实现按键防抖功能,防止误触发;
+4、对外发布按键消息;
+
+本文件没有对外接口,直接在main.lua中require "key_drv"就可以加载运行;
+]]
+
+-- 按键定义
+local key_boot = 0           -- GPIO0按键(BOOT键)
+local key_pwr = gpio.PWR_KEY -- 电源按键
+
+
+-- 按键事件处理函数
+local function handle_boot_key(val)
+    -- print("key_boot", val)
+    if val == 1 then
+        sys.publish("KEY_EVENT", "boot_down")
+    else
+        sys.publish("KEY_EVENT", "boot_up")
+    end
+end
+
+local function handle_pwr_key(val)
+    -- print("key_pwr", val)
+    if val == 1 then
+        sys.publish("KEY_EVENT", "pwr_up")
+    else
+        sys.publish("KEY_EVENT", "pwr_down")
+    end
+end
+
+--[[
+初始化按键GPIO;
+配置BOOT键和PWR键的GPIO中断;
+
+@api init()
+@summary 配置BOOT键和PWR键的GPIO中断
+@return bool 初始化只会返回true
+@usage
+
+]]
+local function init()
+    gpio.setup(key_boot, handle_boot_key, gpio.PULLDOWN, gpio.BOTH)
+    gpio.debounce(key_boot, 50, 0) -- 防抖,防止频繁触发
+
+    gpio.setup(key_pwr, handle_pwr_key, gpio.PULLUP, gpio.BOTH)
+    gpio.debounce(key_pwr, 50, 0) -- 防抖,防止频繁触发
+
+    log.info("key_drv", "按键初始化完成")
+end
+
+init()

+ 83 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/ht1621/main.lua

@@ -0,0 +1,83 @@
+--[[
+@module  main
+@summary HT1621时钟主程序入口
+@version 1.0
+@date    2025.11.28
+@author  江访
+@usage
+本程序是基于Air780EHM/Air780EHV/Air780EGH核心板与HT1621段码屏的时钟显示系统,
+核心功能包括:
+1、系统初始化和硬件驱动加载
+2、看门狗配置(防止程序死循环)
+3、按键驱动和显示主模块的加载
+4、系统任务调度和运行管理
+
+更多说明参考本目录下的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进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+
+-- main.lua - 程序入口文件
+
+-- 定义项目名称和版本号
+PROJECT = "ht1621_clock" -- 项目名称
+VERSION = "001.000.000"    -- 版本号
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+-- 设置日志输出风格为样式2(建议调试时开启)
+-- log.style(2)
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+-- 加载按键驱动模块
+require "key_drv"
+
+-- 加载用户界面系统主模块
+require "ui_main"
+
+-- 用户代码已结束
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!

+ 214 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/ht1621/readme.md

@@ -0,0 +1,214 @@
+# HT1621 段码屏显示时钟
+
+## 一、功能模块介绍
+
+### 1.1 核心主程序模块
+
+1. **main.lua** - 主程序入口,负责系统初始化和任务调度
+2. **ui_main.lua** - 用户界面主控模块,管理时间显示、页面切换和事件处理
+
+### 1.2 驱动模块
+
+1. **ht1621_drv.lua** - HT1621 段码屏驱动模块,负责液晶屏硬件初始化
+2. **key_drv.lua** - 按键驱动模块,管理 BOOT 键和 PWR 键的 GPIO 中断和防抖处理
+
+## 二、按键消息介绍
+
+### 2.1 按键事件消息
+
+1. **"KEY_EVENT"** - 按键事件消息,包含按键类型和状态
+   - boot 键事件:`boot_down`(按下)、`boot_up`(释放)
+   - pwr 键事件:`pwr_down`(按下)、`pwr_up`(释放)
+
+### 2.2 按键功能定义
+
+- **主页(时间显示)**:boot 键(按下)切换显示页面(时间 ↔ 日期)
+- **日期页面**:boot 键(按下)切换回时间页面
+- PWR 键在本演示中未定义特殊功能,仅作为按键状态示例
+
+## 三、显示效果
+
+<table>
+<tr>
+<td>时间-星期<br/></td><td>年月日<br/></td></tr>
+<tr>
+<td rowspan="2"><img src="https://docs.openluat.com/cdn/image/Air780EHM_ht1621显示时间.jpg" width="80" /><br/></td>
+<td><img src="https://docs.openluat.com/cdn/image/Air780EHM_ht1621显示日期.jpg" width="80" /><br/></td></tr>
+</table>
+
+### 4.1 时间显示功能
+
+1. **开机画面** - 显示1秒所有段码,以帮助理解每个段码的显示逻辑,更方便应用到其他场景以及与演示demo不同品牌的ht1621上
+
+### 4.2 时间显示功能
+
+1. **自动时间同步** - 从系统时间获取实时时间
+2. **冒号闪烁** - 每秒钟冒号状态切换,增强时间显示效果
+3. **星期显示** - 显示星期数字(1-7 对应星期一至星期日)
+4. **30 秒自动更新** - 每隔 30 秒自动刷新显示内容
+
+### 4.3日期显示功能
+
+1. **完整日期显示** - 显示年、月、日信息
+2. **格式化显示** - 统一为两位数字格式(如 01、12 等)
+
+### 4.4 页面切换功能
+
+1. **一键切换** - 按 BOOT 键在时间和日期页面间切换
+2. **状态记忆** - 保持当前显示页面状态
+
+### 4.5 系统管理功能
+
+1. **看门狗保护** - 防止程序死循环,自动重启
+
+## 五、演示硬件环境
+
+### 5.1 硬件清单
+
+- Air780EHM/Air780EHV/Air780EGH 核心板 × 1
+- ht1621 液晶屏 × 1 [demo所使用的ht1621屏幕购买链接](https://e.tb.cn/h.72xbNqgE6wdTQzt?tk=xmuJfuxyH4z)
+- 母对母杜邦线 × 6,杜邦线太长的话,会出现 spi 通信不稳定的现象;
+- TYPE-C 数据线 × 1
+- Air780EHM/Air780EHV/Air780EGH 核心板和 ht1621 液晶屏的硬件接线方式为
+
+  - Air780EHM/Air780EHV/Air780EGH 核心板通过 TYPE-C USB 口供电(核心板正面开关拨到 ON 一端),此种供电方式下,VDD_EXT 引脚为 3.3V,可以直接给 ht1621 液晶屏供电;
+  - 为了演示方便,所以 Air780EHM/Air780EHV/Air780EGH 核心板上电后直接通过 VDD_EXT 引脚给 ht1621 液晶屏供电;
+  - 客户在设计实际项目时,一般来说,需要通过一个 GPIO 来控制 LDO 给配件板供电,这样可以灵活地控制配件板的供电,可以使项目的整体功耗降到最低;
+
+### 5.2 接线配置
+
+#### 5.2.1 显示屏接线
+
+<table>
+<tr>
+<td>Air780EHM/Air780EHV/Air780EGH 核心板<br/></td><td>ht1621 液晶屏<br/></td></tr>
+<tr>
+<td>22/GPIO1<br/></td><td>WR<br/></td></tr>
+<tr>
+<td>19/GPIO22<br/></td><td>CS<br/></td></tr>
+<tr>
+<td>20/GPIO24<br/></td><td>DATA<br/></td></tr>
+<tr>
+<td>VDD_EXT<br/></td><td>VCC<br/></td></tr>
+<tr>
+<td>GND<br/></td><td>GND<br/></td></tr>
+</table>
+
+#### 5.2.3 接线图
+![](https://docs.openLuat.com/cdn/image/Air780EHM_ht1621接线图.jpg)
+
+## 六、演示软件环境
+
+### 6.1 开发工具
+
+- [Luatools下载调试工具](https://docs.openluat.com/air780egh/luatos/common/download/) - 固件烧录和代码调试
+
+### 6.2 内核固件
+
+- [点击下载Air780EHM系列最新版本内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/),demo所使用的是LuatOS-SoC_V2018_Air780EHM 1号固件
+  
+- [点击下载Air780EHV系列最新版本内核固件](https://docs.openluat.com/air780ehv/luatos/firmware/version/),demo所使用的是LuatOS-SoC_V2018_Air780EHV 1号固件
+  
+- [点击下载Air780EGH系列最新版本内核固件](https://docs.openluat.com/air780egh/luatos/firmware/version/),demo所使用的是LuatOS-SoC_V2018_Air780EGH 1号固件
+
+### 6.3 脚本文件
+
+1. **main.lua** - 主程序入口
+2. **ui_main.lua** - 用户界面主模块
+3. **ht1621_drv.lua** - HT1621 驱动模块
+4. **key_drv.lua** - 按键驱动模块
+
+## 七、演示核心步骤
+
+### 7.1 硬件准备
+
+1. 按照硬件接线表连接所有设备
+2. 确保电源连接正确,通过TYPE-C USB口供电
+3. 检查所有接线无误,避免短路
+4. 确认核心板上的 BOOT 键和 PWR 键可用
+
+### 7.2 软件配置
+在`main.lua`中加载对应的驱动模块:
+
+```lua
+
+-- 加载用户界面系统主模块
+require "ui_main"
+
+```
+
+### 7.3 软件烧录
+
+1. 使用Luatools选择最新内核固件
+2. 下载本项目所有脚本文件
+3. 将固件和脚本一起烧录到设备
+4. 烧录成功后设备自动重启后开始运行
+
+### 7.4 功能测试
+
+#### 7.4.1 时间显示页面(默认页面)
+
+1. 设备启动后默认显示时间页面
+2. 观察显示效果:小时:分钟 星期(如 12:34 星 5)
+3. 观察冒号每秒闪烁一次
+4. 等待 30 秒,观察时间自动更新
+
+#### 7.4.2 页面切换测试
+
+1. 按下核心板上的 BOOT 键
+2. 观察显示切换到日期页面(格式:年-月-日,如 25 12 11)
+3. 注意:日期页面没有冒号闪烁
+4. 再次按下 BOOT 键,切换回时间页面
+
+#### 7.4.3 日期显示页面
+
+1. 在日期页面,观察显示格式为 YY-MM-DD
+2. 所有数字都显示为两位(如 01、12 等)
+3. 等待 30 秒,观察日期自动更新
+
+#### 7.4.4 按键功能测试
+
+1. **BOOT 键**:按下切换时间和日期页面
+2. **PWR 键**:按下和释放会触发按键事件(本演示中无特殊功能)
+3. 观察日志输出,确认按键事件正常触发
+
+### 7.5 预期效果
+
+- **时间显示页面**:正常显示小时和分钟,冒号每秒闪烁,右下角显示星期数字
+- **日期显示页面**:正常显示年-月-日,所有数字为两位格式
+- **页面切换**:按 BOOT 键在时间和日期页面间流畅切换
+- **自动更新**:每 30 秒自动更新显示内容
+- **冒号闪烁**:仅时间页面有冒号闪烁,每秒切换状态
+- **按键响应**:BOOT 键切换页面,PWR 键触发按键事件
+
+### 7.6 故障排除
+
+1. **显示异常或无显示**:
+
+   - 检查 HT1621 接线是否正确(CS、DATA、WR、VCC、GND)
+   - 确认 GPIO 引脚配置正确
+   - 检查电源电压是否稳定(3.3V)
+2. **按键无响应**:
+
+   - 检查 BOOT 键和 PWR 键是否正常
+   - 确认 key_drv.lua 中按键 GPIO 配置正确
+   - 查看日志确认按键驱动初始化成功
+3. **冒号不闪烁**:
+
+   - 确认当前显示的是时间页面(日期页面无冒号)
+   - 检查冒号闪烁定时器是否正常启动
+   - 查看 ui_main.lua 中的冒号处理逻辑
+4. **时间显示错误**:
+
+   - 确认系统时间设置正确
+   - 检查 os.date()函数返回值
+   - 确认时间格式化逻辑正确
+5. **系统运行不稳定**:
+
+   - 检查内存使用情况,适当调整定时器频率
+   - 确认看门狗功能正常启用
+   - 查看错误日志分析具体问题
+
+### 7.7 扩展功能建议
+
+ht1621更多接口的使用可以查看[ht1621核心库说明](https://docs.openluat.com/osapi/core/ht1621/)

+ 339 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/ht1621/ui_main.lua

@@ -0,0 +1,339 @@
+--[[
+@module  ui_main
+@summary 显示主模块
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为显示主模块,核心业务逻辑为:
+1、初始化ht1621液晶屏
+2、显示时间或日期页面
+3、处理按键切换页面
+4、定时更新显示内容
+5、冒号闪烁控制
+
+本文件对外接口:无
+]]
+
+-- 引入驱动模块
+local ht1621_drv = require "ht1621_drv"
+local key_drv = require "key_drv"
+
+-- 段码屏对象
+local seg = nil
+-- 当前显示页面:time或date
+local current_page = "time"
+-- 冒号状态:true为亮,false为灭
+local colon_state = true
+-- 冒号闪烁定时器ID
+local colon_timer = nil
+-- 定时更新定时器ID
+local update_timer = nil
+
+-- 数字段码表 (0-9)
+local digit_map = {
+    [0] = 0xEB, -- 0
+    [1] = 0x0A, -- 1
+    [2] = 0xAD, -- 2
+    [3] = 0x8F, -- 3
+    [4] = 0x4E, -- 4
+    [5] = 0xC7, -- 5
+    [6] = 0xE7, -- 6
+    [7] = 0x8A, -- 7
+    [8] = 0xEF, -- 8
+    [9] = 0xCF  -- 9
+}
+
+-- 星期数字映射 (1-7)
+local week_map = {
+    ["Monday"] = 1,
+    ["Tuesday"] = 2,
+    ["Wednesday"] = 3,
+    ["Thursday"] = 4,
+    ["Friday"] = 5,
+    ["Saturday"] = 6,
+    ["Sunday"] = 7
+}
+
+--[[
+清屏函数
+@summary 清除所有显示
+]]
+local function clear_display()
+    if not seg then return end
+    for i = 0, 11 do
+        ht1621.data(seg, i, 0x00)
+    end
+end
+
+--[[
+显示单个数字到指定位置
+@param position number 显示位置 (0,2,4,6,8,10)
+@param num number 要显示的数字 (0-9)
+@param show_dp boolean 是否显示该位置的小数点
+]]
+local function show_digit(position, num, show_dp)
+    if not seg then return end
+    if num < 0 or num > 9 then return end
+
+    local value = digit_map[num]
+    if show_dp then
+        value = value | 0x10 -- 添加小数点
+    end
+
+    ht1621.data(seg, position, value)
+end
+
+--[[
+显示时间页面
+@summary 显示时间和星期
+]]
+local function show_time()
+    if not seg then return end
+
+    -- 清屏
+    clear_display()
+
+    -- 获取当前时间
+    local now = os.date("*t")
+    local hour_str = string.format("%02d", now.hour)
+    local min_str = string.format("%02d", now.min)
+
+    -- 获取星期数字 (1-7)
+    local week_name = os.date("%A")
+    local week_num = week_map[week_name] or 1
+
+    -- 显示时间
+    -- 位置1: 小时的十位 (位置0)
+    show_digit(0, tonumber(string.sub(hour_str, 1, 1)), false)
+
+    -- 位置2: 小时的个位 (位置2,带冒号)
+    show_digit(2, tonumber(string.sub(hour_str, 2, 2)), colon_state)
+
+    -- 位置3: 分钟的十位 (位置4)
+    show_digit(4, tonumber(string.sub(min_str, 1, 1)), false)
+
+    -- 位置4: 分钟的个位 (位置6)
+    show_digit(6, tonumber(string.sub(min_str, 2, 2)), false)
+
+    -- 位置6: 星期 (位置10,显示1-7)
+    show_digit(10, week_num, false)
+
+    -- 设置当前页面
+    current_page = "time"
+
+    log.info("ui_main", string.format("显示时间: %s:%s 星期%d", hour_str, min_str, week_num))
+end
+
+--[[
+显示日期页面
+@summary 显示年月日
+]]
+local function show_date()
+    if not seg then return end
+
+    -- 清屏
+    clear_display()
+
+    -- 获取当前日期
+    local now = os.date("*t")
+    local year_str = string.sub(tostring(now.year), 3, 4) -- 最后两位
+    local month_str = string.format("%02d", now.month)
+    local day_str = string.format("%02d", now.day)
+
+    -- 显示日期
+    -- 位置1: 年份的十位 (位置0)
+    show_digit(0, tonumber(string.sub(year_str, 1, 1)), false)
+
+    -- 位置2: 年份的个位 (位置2)
+    show_digit(2, tonumber(string.sub(year_str, 2, 2)), false)
+
+    -- 位置3: 月份的十位 (位置4)
+    show_digit(4, tonumber(string.sub(month_str, 1, 1)), false)
+
+    -- 位置4: 月份的个位 (位置6)
+    show_digit(6, tonumber(string.sub(month_str, 2, 2)), false)
+
+    -- 位置5: 日期的十位 (位置8)
+    show_digit(8, tonumber(string.sub(day_str, 1, 1)), false)
+
+    -- 位置6: 日期的个位 (位置10)
+    show_digit(10, tonumber(string.sub(day_str, 2, 2)), false)
+
+    -- 设置当前页面
+    current_page = "date"
+
+    log.info("ui_main", string.format("显示日期: %s-%s-%s", year_str, month_str, day_str))
+end
+
+--[[
+设置冒号状态
+@param state boolean 冒号状态,true为亮,false为灭
+]]
+local function set_colon_state(state)
+    colon_state = state
+    -- 如果当前显示的是时间页面,更新冒号显示
+    if current_page == "time" then
+        local now = os.date("*t")
+        local hour_str = string.format("%02d", now.hour)
+        show_digit(2, tonumber(string.sub(hour_str, 2, 2)), colon_state)
+    end
+end
+
+--[[
+冒号闪烁处理函数
+@summary 每秒钟切换冒号的亮灭状态
+]]
+local function colon_blink_callback()
+    local current_state = colon_state
+    set_colon_state(not current_state)
+end
+
+--[[
+启动冒号闪烁
+@summary 创建冒号闪烁定时器
+]]
+local function start_colon_blink()
+    if colon_timer then
+        sys.timerStop(colon_timer)
+    end
+    colon_timer = sys.timerLoopStart(colon_blink_callback, 1000) -- 每1秒执行一次
+end
+
+--[[
+停止冒号闪烁
+@summary 停止冒号闪烁定时器
+]]
+local function stop_colon_blink()
+    if colon_timer then
+        sys.timerStop(colon_timer)
+        colon_timer = nil
+    end
+end
+
+--[[
+定时更新显示内容
+@summary 每30秒检查并更新显示内容
+]]
+local function periodic_update_callback()
+    if current_page == "time" then
+        show_time()
+    else
+        show_date()
+    end
+    log.info("ui_main", "定时更新显示内容")
+end
+
+--[[
+启动定时更新
+@summary 创建定时更新定时器
+]]
+local function start_periodic_update()
+    if update_timer then
+        sys.timerStop(update_timer)
+    end
+    update_timer = sys.timerLoopStart(periodic_update_callback, 30000) -- 每30秒执行一次
+end
+
+--[[
+停止定时更新
+@summary 停止定时更新定时器
+]]
+local function stop_periodic_update()
+    if update_timer then
+        sys.timerStop(update_timer)
+        update_timer = nil
+    end
+end
+
+--[[
+切换显示页面
+@summary 在时间和日期页面之间切换
+]]
+local function toggle_page()
+    if current_page == "time" then
+        show_date()
+        -- 停止冒号闪烁
+        stop_colon_blink()
+    else
+        show_time()
+        -- 启动冒号闪烁
+        start_colon_blink()
+    end
+end
+
+--[[
+按键事件处理回调
+@param key_event string 按键事件类型
+]]
+local function key_event_callback(key_event)
+    if key_event == "boot_down" then
+        -- 切换显示页面
+        toggle_page()
+        log.info("ui_main", "按键切换页面")
+    end
+end
+
+--[[
+按键事件处理函数
+@summary 处理按键事件,切换显示页面
+]]
+local function handle_key_event()
+    sys.subscribe("KEY_EVENT", key_event_callback)
+end
+
+
+--[[
+开机显示函数
+@summary 显示全8图案和所有特殊符号,持续1秒
+]]
+local function show_boot_screen()
+    if not seg then return end
+
+    -- 显示全8图案和所有特殊符号,不同ht1621地址可能不同,此接口可用于测试
+    ht1621.data(seg, 0, 0xEF | 0x10)  -- 显示第一个8和电池图标
+    ht1621.data(seg, 2, 0xEF | 0x10)  -- 显示第二个8和冒号
+    ht1621.data(seg, 4, 0xEF | 0x10)  -- 显示第三个8和温度圆圈
+    ht1621.data(seg, 6, 0xEF | 0x10)  -- 显示第四个8和右下小数点
+    ht1621.data(seg, 8, 0xEF | 0x10)  -- 显示第五个8和右下小数点
+    ht1621.data(seg, 10, 0xEF | 0x10) -- 显示第六个8和右下小数点
+
+    log.info("ui_main", "显示开机画面")
+end
+
+--[[
+主函数
+@summary 初始化所有组件并启动主逻辑
+]]
+local function main()
+    -- 初始化HT1621驱动,获取seg对象
+    seg = ht1621_drv.init()
+    if not seg then
+        log.error("ui_main", "HT1621驱动初始化失败")
+        return
+    end
+
+    -- 显示开机画面
+    show_boot_screen()
+    sys.wait(1000)
+
+    -- 注册按键事件处理
+    handle_key_event()
+
+    -- 初始显示时间页面
+    show_time()
+
+    -- 启动冒号闪烁
+    start_colon_blink()
+
+    -- 启动定时更新
+    start_periodic_update()
+
+    -- 启动内存监控(可选,调试时开启)
+    -- sys.timerLoopStart(monitor_memory, 3000)
+
+    log.info("ui_main", "显示主模块初始化完成")
+end
+
+-- 运行主函数
+sys.taskInit(main)

+ 75 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/hw_drv/hw_default_font_drv.lua

@@ -0,0 +1,75 @@
+--[[
+@module  hw_default_font_drv
+@summary LCD初始化和内置点阵字体驱动模块
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为U8G2内置字体硬件驱动模块,核心业务逻辑为:
+1、初始化ST7567单色点阵屏(128x64分辨率);
+2、配置SPI通信参数和显示参数;
+3、设置内置字体显示模式;
+4、显示开机信息并开启背光;
+
+本文件无对外接口,模块加载时自动执行初始化;
+]]
+
+-- ST7567 SPI引脚配置
+local spi_id, spi_res, spi_dc, spi_cs = 1, 22, 14, 12
+
+local function init()
+    -- 初始化U8G2显示屏 - ST7567, 128x64
+    local result = u8g2.begin(
+        {
+            ic = "custom",        -- 使用自定义IC
+            direction = 0,        -- 显示方向
+            mode = "spi_hw_4pin", -- SPI硬件4线模式
+            spi_id = spi_id,      -- SPI端口号
+            spi_res = spi_res,    -- 复位引脚
+            spi_dc = spi_dc,      -- 数据/命令选择引脚
+            spi_cs = spi_cs       -- 片选引脚
+        },
+        {
+            width = 128, -- 分辨率宽度,128像素
+            height = 64, -- 分辨率高度,64像素
+
+            -- 初始化命令表,根据ST7567芯片手册配置
+            initcmd = {
+                0xE2,        -- 系统复位
+                0x82,        -- 设置偏压比
+                0x2F,        -- 电源控制(开启内部电荷泵)
+                0x26,        -- 电阻比率设置
+                0xF8, 0x00,  -- 设置显示偏移(垂直偏移量为0)
+                0x81, 0x09,  -- 设置对比度(0x09为对比度值)
+                0x40,        -- 设置显示起始行(第0行)
+                0xC8,        -- COM扫描方向(反向)
+                0xA4,        -- 正常显示模式
+                0xAF,        -- 开启显示
+            },
+            sleepcmd = 0xAE, -- 休眠命令
+            wakecmd = 0xAF,  -- 唤醒命令
+        }
+    )
+
+    if result == 1 then
+        -- SPI接口屏幕才能获取初始化成功后屏幕的长宽,I2C接口无法获取的屏幕初始化成功后的长宽
+        local width = u8g2.GetDisplayWidth()
+        local height = u8g2.GetDisplayHeight()
+        log.info("u8g2", "ST7567初始化成功" .. width .. "x" .. height)
+
+        -- 设置字体显示模式为透明
+        u8g2.SetFontMode(1)
+
+        -- 显示开机信息
+        u8g2.ClearBuffer()
+        u8g2.SetFont(u8g2.font_opposansm12_chinese)
+        u8g2.DrawUTF8("内置字体进入", 30, 30)
+        u8g2.SendBuffer()
+
+        -- 打开背光,若采用GPIO控制
+    else
+        log.error("u8g2", "初始化失败,错误码:", result)
+    end
+end
+
+init()

+ 90 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/hw_drv/hw_gtfont_drv.lua

@@ -0,0 +1,90 @@
+--[[
+@module  hw_gtfont_drv
+@summary LCD初始化和外置GTFont驱动模块
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为LCD初始化和GTFont外置字库驱动模块,核心业务逻辑为:
+1、初始化ST7567单色点阵屏(128x64分辨率);
+2、初始化SPI设备连接AirFONTS_1000字库芯片;
+3、初始化矢量字库功能;
+4、显示开机信息并开启背光;
+
+本文件无对外接口,模块加载时自动执行初始化;
+]]
+
+-- ST7567 SPI引脚配置
+local spi_id, spi_res, spi_dc, spi_cs = 1, 22, 14, 12
+
+-- GTFont SPI引脚配置
+local gtfspi_id, gtfspi_cs = 0, 8
+
+-- 初始化U8G2显示屏 - ST7567, 128x64
+local function init()
+    local result = u8g2.begin(
+        {
+            ic = "custom",        -- 使用自定义IC
+            direction = 0,        -- 显示方向
+            mode = "spi_hw_4pin", -- SPI硬件4线模式
+            spi_id = spi_id,      -- SPI端口号
+            spi_res = spi_res,    -- 复位引脚
+            spi_dc = spi_dc,      -- 数据/命令选择引脚
+            spi_cs = spi_cs       -- 片选引脚
+        },
+        {
+            width = 128, -- 分辨率宽度,128像素
+            height = 64, -- 分辨率高度,64像素
+
+            -- 初始化命令表,根据ST7567芯片手册配置
+            initcmd = {
+                0xE2,        -- 系统复位
+                0x82,        -- 设置偏压比
+                0x2F,        -- 电源控制(开启内部电荷泵)
+                0x26,        -- 电阻比率设置
+                0xF8, 0x00,  -- 设置显示偏移(垂直偏移量为0)
+                0x81, 0x09,  -- 设置对比度
+                0x40,        -- 设置显示起始行(第0行)
+                0xC8,        -- COM扫描方向(反向)
+                0xA4,        -- 正常显示模式
+                0xAF,        -- 开启显示
+            },
+            sleepcmd = 0xAE, -- 休眠命令
+            wakecmd = 0xAF,  -- 唤醒命令
+        }
+    )
+
+    if result == 1 then
+        -- SPI接口屏幕才能获取初始化成功后屏幕的长宽,I2C接口无法获取的屏幕初始化成功后的长宽
+        local width = u8g2.GetDisplayWidth()
+        local height = u8g2.GetDisplayHeight()
+        log.info("u8g2", "ST7567初始化成功" .. width .. "x" .. height)
+
+        --创建一个SPI设备对象,赋值给全局变量名
+        gtfont_spi = spi.deviceSetup(gtfspi_id, gtfspi_cs, 0, 0, 8, 20 * 1000 * 1000, spi.MSB, 1, 0)
+        log.info("AirFONTS_1000.init", "spi.deviceSetup", type(gtfont_spi))
+        --检查SPI设备对象是否创建成功
+        if type(gtfont_spi) == "userdata" then
+            local result = gtfont.init(gtfont_spi)
+            --初始化矢量字库
+            if result == false then
+                log.error("gtfont_drv.init", "gtfont_drv.init error")
+                spi.close(gtfont_spi)
+            end
+        end
+
+        -- 设置字体显示模式为透明
+        u8g2.SetFontMode(1)
+
+        -- 显示启动信息
+        u8g2.ClearBuffer()
+        u8g2.drawGtfontUtf8("GTFont进入", 16, 0, 30)
+        u8g2.SendBuffer()
+
+        -- 打开背光,若采用GPIO控制
+    else
+        log.error("u8g2", "初始化失败,错误码:", result)
+    end
+end
+
+init()

+ 66 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/hw_drv/key_drv.lua

@@ -0,0 +1,66 @@
+--[[
+@module  key_drv
+@summary 按键驱动模块
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为按键驱动功能模块,核心业务逻辑为:
+1、初始化BOOT键和PWR键的GPIO;
+2、配置按键事件的中断处理函数;
+3、实现按键防抖功能,防止误触发;
+4、对外发布按键消息;
+
+本文件没有对外接口,直接在main.lua中require "key_drv"就可以加载运行;
+]]
+
+-- 按键定义
+local key_boot = 0           -- GPIO0按键(BOOT键)
+local key_pwr = gpio.PWR_KEY -- 电源按键
+
+
+-- 按键事件处理函数
+local function handle_boot_key(val)
+    -- print("key_boot", val)
+    if val == 1 then
+        sys.publish("KEY_EVENT", "boot_down")
+    else
+        sys.publish("KEY_EVENT", "boot_up")
+    end
+end
+
+local function handle_pwr_key(val)
+    -- print("key_pwr", val)
+    if val == 1 then
+        sys.publish("KEY_EVENT", "pwr_up")
+    else
+        sys.publish("KEY_EVENT", "pwr_down")
+    end
+end
+
+--[[
+初始化按键GPIO;
+配置BOOT键和PWR键的GPIO中断;
+
+@api init()
+@summary 配置BOOT键和PWR键的GPIO中断
+@return bool 初始化只会返回true
+
+@usage
+local result = key_drv.init()
+if result then
+    log.info("按键驱动初始化成功")
+end
+]]
+local function init()
+    gpio.setup(key_boot, handle_boot_key, gpio.PULLDOWN, gpio.BOTH)
+    gpio.debounce(key_boot, 50, 0) -- 防抖,防止频繁触发
+
+    gpio.setup(key_pwr, handle_pwr_key, gpio.PULLUP, gpio.BOTH)
+    gpio.debounce(key_pwr, 50, 0) -- 防抖,防止频繁触发
+
+    log.info("key_drv", "按键初始化完成")
+end
+
+init()
+

+ 99 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/main.lua

@@ -0,0 +1,99 @@
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本demo演示的核心功能为:
+1、使用U8G2图形库驱动ST7567单色点阵屏(128x64);
+2、提供三种显示模式选择:组件演示、内置字体显示、GTFont外置字库显示;
+3、通过BOOT键和PWR键进行菜单导航和确认操作;
+4、支持按键防抖和看门狗机制,确保系统稳定运行;
+
+本文件作为程序入口,主要完成以下初始化工作:
+1、定义项目名称和版本号;
+2、初始化看门狗定时器;
+3、加载硬件显示驱动(支持内置12号中文点阵字体和外置GTFont两种模式);
+4、加载按键驱动模块;
+5、启动UI主模块;
+
+更多说明参考本目录下的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进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+
+-- main.lua - 程序入口文件
+
+
+-- 项目名称和版本定义
+PROJECT = "u8g2_demo" -- 项目名称,用于标识当前工程
+VERSION = "001.000.000"         -- 项目版本号
+
+-- 在日志中打印项目名和项目版本号
+log.info("u8g2_demo", PROJECT, VERSION)
+
+-- 设置日志输出风格为样式2(建议调试时开启)
+-- log.style(2)
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+
+
+-- 加载显示和字体驱动模块,有以下两种:
+-- 1. hw_default_font_drv - LCD显示驱动和内置字体驱动模块,hw_default_font_drv和hw_gtfont_drv二选一使用
+-- 2. hw_gtfont_drv - LCD显示驱动和GTFont外置字体驱动模块,hw_default_font_drv和hw_gtfont_drv二选一使用
+require("hw_default_font_drv")  -- 使用内置12号中文点阵字体
+-- require("hw_gtfont_drv")     -- 使用GTFont外置矢量字库,在屏幕上表现
+
+-- 加载按键驱动
+require("key_drv")
+
+-- 加载UI主模块
+require("ui_main")
+
+-- 用户代码已结束
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 248 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/readme.md

@@ -0,0 +1,248 @@
+# U8G2显示屏与按键演示系统
+
+## 一、功能模块介绍
+
+### 1.1 核心主程序模块
+
+1. **main.lua** - 主程序入口,负责系统初始化和任务调度
+2. **ui_main.lua** - 用户界面主控模块,管理页面切换和事件分发
+
+### 1.2 显示页面模块
+
+1. **home_page.lua** - 主页模块,提供应用入口和导航功能
+2. **component_page.lua** - 组件演示模块,展示进度条和基本图形
+3. **default_font_page.lua** - 内置字体演示模块,展示U8G2内置字体效果
+4. **gtfont_page.lua** - GTFont矢量字体演示模块,展示外置字库效果
+
+### 1.3 驱动模块
+
+1. **hw_default_font_drv.lua** - LCD初始化和内置字体驱动模块,hw_default_font_drv和hw_gtfont_drv二选一使用
+2. **hw_gtfont_drv.lua** - LCD初始化和GTFont外置字体驱动模块,hw_default_font_drv和hw_gtfont_drv二选一使用
+3. **key_drv.lua** - 按键驱动模块,管理BOOT键和PWR键
+
+
+## 二、按键消息介绍
+
+1. **"KEY_EVENT"** - 按键事件消息,包含按键类型和状态
+   - boot 键事件:`boot_down`(按下)、`boot_up`(释放)
+   - pwr 键事件:`pwr_down`(按下)、`pwr_up`(释放)
+   - 按键功能定义:
+     - 主页:boot 键(释放)选择/切换选项,pwr 键(释放)确认
+     - 组件演示页面:boot 键(释放)切换选项,pwr 键(释放)确认(返回或进度 +10%)
+     - 内置字体页面:boot 键(释放)切换选项(只有一个返回按钮,无实际效果),pwr 键(释放)返回
+     - GTFont 页面:boot 键(释放)切换选项(返回或切换字体大小),pwr 键(释放)确认
+
+注意:当前代码中只处理按键的释放事件(boot_up 和 pwr_up),按下事件被忽略。
+
+## 三、显示效果
+
+<table>
+<tr>
+<td>主页<br/></td><td>组件演示页<br/></td><td>内置中文字体页面<br/></td><td>GTFont页面<br/></td></tr>
+<tr>
+<td rowspan="2"><img src="https://docs.openluat.com/cdn/image/Air780EHM_st7567_homepage.jpg" width="80" /><br/></td><td rowspan="2"><img src="https://docs.openluat.com/cdn/image/Air780EHM_ST7567_component_page.jpg" width="80" /><br/></td><td><img src="https://docs.openluat.com/cdn/image/Air780EHM_st7567_default_font_page.jpg" width="80" /><br/></td>
+<td><img src="https://docs.openluat.com/cdn/image/Air780EHM_st7567_gtfont_page.jpg" width="80" /><br/></td></tr>
+</table>
+
+## 四、功能详细说明
+
+### 4.1 组件演示页面
+
+1. **进度条显示** - 展示进度条,可通过"+10%"按钮增加进度(最大 100%)
+2. **基本图形绘制** - 展示圆形、实心圆、矩形、实心矩形、三角形
+3. **按钮交互** - 支持返回首页和调整进度两种功能
+
+### 4.2 内置字体演示页面
+
+1. **内置字体显示** - 展示 U8G2 内置中文字体效果
+2. **时间显示** - 显示当前系统时间,支持实时更新
+3. **简洁界面** - 单按钮设计,便于快速返回
+
+### 4.3 GTFont 矢量字体演示页面
+
+1. **矢量字体显示** - 使用 GTFont 矢量字库显示文字(需外置字库支持)
+2. **字体大小切换** - 支持 12、14、16、18、20 号字体大小循环切换
+3. **兼容性设计** - 无外置字库时自动使用内置字体显示
+
+### 4.4 按键交互功能
+
+1. **页面导航** - 支持多页面之间的流畅切换
+2. **防抖处理** - 按键驱动内置 50ms 防抖,防止误触发
+3. **事件分发** - 统一的事件分发机制,便于扩展
+
+## 五、演示硬件环境
+
+### 5.1 硬件清单
+
+- Air780EHM/Air780EHV/Air780EGH 核心板 × 1
+- st7657 显示屏 × 1 [本demo演示使用的屏幕购买链接]( https://e.tb.cn/h.72oQitvwK2AJtDC?tk=ymJ3fuxC8L4)
+- GTFont 矢量字库,使用的是 AirFONTS_1000 配件板 × 1
+- 母对母杜邦线 × 14,杜邦线太长的话,会出现 spi 通信不稳定的现象;
+- TYPE-C 数据线 × 1
+- Air780EHM/Air780EHV/Air780EGH 核心板和 ST7567单色点阵屏以及AirFONTS_1000 配件板的硬件接线方式为
+
+  - Air780EHM/Air780EHV/Air780EGH 核心板通过 TYPE-C USB 口供电(核心板正面开关拨到 ON 一端),此种供电方式下,VDD_EXT 引脚为 3.3V,可以直接给 ST7567单色点阵屏和AirFONTS_1000 配件板供电;
+  - 为了演示方便,所以 Air780EHM/Air780EHV/Air780EGH 核心板上电后直接通过 VBAT 引脚给 ST7567单色点阵屏,VDD-EXT引脚给AirFONTS_1000 配件板供电;
+  - 客户在设计实际项目时,一般来说,需要通过一个 GPIO 来控制 LDO 给配件板供电,这样可以灵活地控制配件板的供电,可以使项目的整体功耗降到最低;
+
+### 5.2 接线配置
+
+#### 5.2.1 LCD 显示屏接线
+
+<table>
+<tr>
+<td>Air780EHM/Air780EHV/Air780EGH 核心板<br/></td><td>st7567<br/></td></tr>
+<tr>
+<td>57/U3TXD<br/></td><td>SCL<br/></td></tr>
+<tr>
+<td>28/U2RXD<br/></td><td>CS<br/></td></tr>
+<tr>
+<td>19/GPIO22<br/></td><td>RST<br/></td></tr>
+<tr>
+<td>29/U2TXD<br/></td><td>SDA<br/></td></tr>
+<tr>
+<td>58/U3RXD<br/></td><td>DC<br/></td></tr>
+<tr>
+<td>VBAT<br/></td><td>BL<br/></td></tr>
+<tr>
+<td>VBAT<br/></td><td>VCC<br/></td></tr>
+<tr>
+<td>GND<br/></td><td>GND<br/></td></tr>
+</table>
+
+#### 5.2.2 GTFont 字库接线
+
+<table>
+<tr>
+<td>Air780EHM/Air780EHV/Air780EGH 核心板<br/></td><td>AirFONTS_1000配件板<br/></td></tr>
+<tr>
+<td>83/SPI0_CS<br/></td><td>CS<br/></td></tr>
+<tr>
+<td>84/SPI0_MISO<br/></td><td>MISO<br/></td></tr>
+<tr>
+<td>85/SPI0_MOSI<br/></td><td>MOSI<br/></td></tr>
+<tr>
+<td>86/SPI0_CLK<br/></td><td>CLK<br/></td></tr>
+<tr>
+<td>24/VDD_EXT<br/></td><td>VCC<br/></td></tr>
+<tr>
+<td>GND<br/></td><td>GND<br/></td></tr>
+</table>
+
+#### 5.2.3 接线图
+![](https://docs.openLuat.com/cdn/image/Air780EHM_st7567接线图.jpg)
+
+## 六、演示软件环境
+
+### 6.1 开发工具
+
+- [Luatools下载调试工具](https://docs.openluat.com/air780egh/luatos/common/download/) - 固件烧录和代码调试
+
+### 6.2 内核固件
+
+- [点击下载Air780EHM系列最新版本内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/),demo所使用的是LuatOS-SoC_V2014_Air780EHM 1号固件
+  
+- [点击下载Air780EHV系列最新版本内核固件](https://docs.openluat.com/air780ehv/luatos/firmware/version/),demo所使用的是LuatOS-SoC_V2014_Air780EHV 1号固件
+  
+- [点击下载Air780EGH系列最新版本内核固件](https://docs.openluat.com/air780egh/luatos/firmware/version/),demo所使用的是LuatOS-SoC_V2014_Air780EGH 1号固件
+
+## 七、演示核心步骤
+
+### 7.1 硬件准备
+1. 按照硬件接线表连接所有设备
+2. 确保电源连接正确,通过TYPE-C USB口供电
+3. 检查所有接线无误,避免短路
+
+### 7.2 软件配置
+在`main.lua`中选择加载对应的驱动模块:
+
+```lua
+-- 加载显示和字体驱动模块,有以下两种:
+-- 1. hw_default_font_drv - LCD显示驱动和内置字体驱动模块,hw_default_font_drv和hw_gtfont_drv二选一使用
+-- 2. hw_gtfont_drv - LCD显示驱动和GTFont外置字体驱动模块,hw_default_font_drv和hw_gtfont_drv二选一使用
+require("hw_default_font_drv")  -- 使用内置12号中文点阵字体
+-- require("hw_gtfont_drv")     -- 使用GTFont外置矢量字库,在屏幕上表现
+
+-- 加载按键驱动
+require("key_drv")
+
+-- 加载UI主模块
+require("ui_main")
+```
+
+### 7.3 软件烧录
+1. 使用Luatools烧录最新内核固件
+2. 下载并烧录本项目所有脚本文件
+3. 烧录成功后设备自动重启后开始运行
+
+### 7.4 功能测试
+
+#### 7.4.1 主页面操作
+
+1. 设备启动后显示主页面,包含三个功能选项
+2. 使用 boot 键(释放)切换选择不同的菜单项
+3. 使用 pwr 键(释放)进入选中的演示页面
+
+#### 7.4.2 组件演示页面
+
+1. 查看进度条显示(初始 30%)
+2. 查看基本图形绘制效果
+3. 使用 boot 键切换按钮(返回、+10%)
+4. 使用 pwr 键执行当前选中按钮的功能
+5. 按 pwr 键(当返回按钮选中时)返回主页
+
+#### 7.4.3 内置字体演示页面
+
+1. 查看内置字体显示效果
+2. 查看当前时间显示(每 300ms 更新一次)
+3. 使用 boot 键切换按钮(只有一个返回按钮)
+4. 按 pwr 键返回主页
+
+#### 7.4.4 GTFont 演示页面
+
+1. 查看字体大小显示(如果使用 GTFont 驱动,则显示 GTFont 字体,否则显示内置字体)
+2. 使用 boot 键切换按钮(返回、切换字体大小)
+3. 使用 pwr 键执行当前选中按钮的功能
+4. 按 pwr 键(当返回按钮选中时)返回主页
+
+### 7.5 预期效果
+
+- **系统启动**:显示开机信息(内置字体进入/GTFont 进入),然后进入主页面
+- **主页面**:正常显示三个菜单项,boot 键切换选项,pwr 键确认
+- **组件演示页面**:进度条和图形显示正常,按键功能正常
+- **内置字体页面**:字体显示正常,时间更新正常,pwr 键返回
+- **GTFont 页面**:字体显示正常,字体大小切换正常,pwr 键返回
+- **按键响应**:所有按键操作响应及时准确,页面切换流畅
+
+### 7.6 故障排除
+
+1. **显示屏不亮**
+
+   - 检查电源接线是否正确
+   - 确认 SPI 通信速率是否合适
+
+2. **显示内容异常**
+
+   - 检查初始化参数和命令是否正确
+   - 确认显示屏分辨率设置是否与自己的屏幕相同
+
+3. **按键无响应**
+
+   - 检查按键 GPIO 引脚配置
+   - 确认按键中断处理函数是否正确注册
+   - 检查防抖参数是否合适
+
+4. **GTFont 功能异常**
+
+   - 检查 GTFont 字库接线是否正确
+   - 确认 SPI 通信速率是否合适(20MHz)
+   - 检查字库初始化是否成功
+
+5. **系统卡顿或重启**
+
+   - 确认内存使用情况
+   - 适当调整屏幕刷新频率
+
+### 7.7 扩展建议
+  
+  本demo所演示的接口都可以在[u8g2核心库](https://docs.openluat.com/osapi/core/u8g2)中找到,更丰富的使用方式可以参考u8g2核心库进行进一步开发。

+ 147 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/ui/component_page.lua

@@ -0,0 +1,147 @@
+--[[
+@module  component_page
+@summary U8G2组件演示页面模块 - 128x64屏幕
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为组件演示页面模块,核心业务逻辑为:
+1、展示U8G2图形组件的绘制能力;
+2、显示进度条和基本图形(圆形、矩形、三角形等);
+3、提供进度调整功能和返回首页功能;
+4、支持BOOT键和PWR键的导航操作;
+
+本文件的对外接口有4个:
+1、component_page.draw():绘制页面内容;
+2、component_page.handle_key(key_type):处理按键事件;
+3、component_page.on_enter():页面进入回调;
+4、component_page.on_leave():页面离开回调;
+]]
+
+local component_page = {}
+
+-- 进度条当前值(0-100)
+local progress_value = 30
+
+-- 当前选中按钮的索引(1:返回, 2:+10%)
+local selected_index = 1
+
+--[[
+@api draw()
+@summary 绘制组件演示页面内容
+@return 无返回值
+@usage
+-- 在UI主循环中调用
+component_page.draw()
+]]
+function component_page.draw()
+    -- 标题
+    u8g2.SetFont(u8g2.font_6x10)
+    u8g2.DrawUTF8("组件页", 35, 10)
+    
+    -- 进度条区域
+    u8g2.DrawUTF8("进度条:", 5, 25)
+    
+    -- 进度条背景(更大)
+    u8g2.DrawFrame(42, 15, 50, 12)
+    
+    -- 进度条前景
+    local fill_width = math.floor(50 * progress_value / 100)
+    u8g2.DrawBox(41, 15, fill_width, 12)
+    
+    -- 进度文本
+    u8g2.DrawUTF8(progress_value .. "%", 95,25)
+    
+    -- 图形演示区域
+    u8g2.DrawUTF8("图形:", 5, 40)
+    
+    -- 绘制基本图形(增加间距)
+    u8g2.DrawCircle(40, 36, 5, u8g2.DRAW_ALL)
+    u8g2.DrawDisc(60, 36, 5, u8g2.DRAW_ALL)
+    u8g2.DrawFrame(75, 31, 10, 10)
+    u8g2.DrawBox(90, 31, 10, 10)
+    u8g2.DrawTriangle(105, 40, 110, 30, 115, 40)
+    
+    -- 按钮区域(布局更宽松)
+    if selected_index == 1 then
+        -- 返回按钮:选中状态
+        u8g2.DrawButtonUTF8("返回", 10, 58, u8g2.BTN_INV + u8g2.BTN_BW1, 0, 2, 0)
+    else
+        -- 返回按钮:未选中状态
+        u8g2.DrawButtonUTF8("返回", 10, 58, u8g2.BTN_BW1, 0, 2, 0)
+    end
+    
+    if selected_index == 2 then
+        -- +10%按钮:选中状态
+        u8g2.DrawButtonUTF8("+10%", 70, 58, u8g2.BTN_INV + u8g2.BTN_BW1, 0, 2, 0)
+    else
+        -- +10%按钮:未选中状态
+        u8g2.DrawButtonUTF8("+10%", 70, 58, u8g2.BTN_BW1, 0, 2, 0)
+    end
+end
+
+--[[
+@api handle_key(key_type)
+@summary 处理按键事件,实现进度调整和页面导航
+@param string key_type 按键类型,可选值:
+  - "confirm":确认键,执行当前选中按钮的功能
+  - "next":切换到下一个按钮
+  - "prev":切换到上一个按钮
+@return bool 是否已处理该按键,true表示已处理
+@usage
+-- 在UI主循环中调用
+local handled = component_page.handle_key("confirm")
+]]
+function component_page.handle_key(key_type)
+    log.info("component_page.handle_key", "key_type:", key_type)
+    
+    if key_type == "confirm" then
+        -- 确认键:执行当前选中按钮的功能
+        if selected_index == 1 then
+            -- 返回按钮:返回首页
+            switch_page("home")
+        elseif selected_index == 2 then
+            -- +10%按钮:增加进度值,最大不超过100%
+            progress_value = math.min(100, progress_value + 10)
+        end
+        return true
+    elseif key_type == "next" then
+        -- 切换到下一个按钮
+        selected_index = selected_index % 2 + 1
+        return true
+    elseif key_type == "prev" then
+        -- 切换到上一个按钮
+        selected_index = (selected_index - 2) % 2 + 1
+        return true
+    end
+    
+    return false
+end
+
+--[[
+@api on_enter()
+@summary 页面进入时的初始化操作,重置选中项和进度值
+@return 无返回值
+@usage
+-- 在页面切换时自动调用
+component_page.on_enter()
+]]
+function component_page.on_enter()
+    -- 页面进入时初始化
+    selected_index = 1  -- 默认选中返回按钮
+end
+
+--[[
+@api on_leave()
+@summary 页面离开时的清理操作
+@return 无返回值
+@usage
+-- 在页面切换时自动调用
+component_page.on_leave()
+]]
+function component_page.on_leave()
+    -- 页面离开时的清理操作
+    -- 当前无需特殊清理
+end
+
+return component_page

+ 110 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/ui/default_font_page.lua

@@ -0,0 +1,110 @@
+--[[
+@module  default_font_page
+@summary U8G2内置字体演示页面模块 - 128x64屏幕
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为默认字体演示页面模块,核心业务逻辑为:
+1、展示U8G2内置字体的显示效果;
+2、显示固定文本内容和当前系统时间;
+3、提供返回首页的功能;
+4、支持BOOT键和PWR键的导航操作;
+
+本文件的对外接口有4个:
+1、default_font_page.draw():绘制页面内容;
+2、default_font_page.handle_key(key_type):处理按键事件;
+3、default_font_page.on_enter():页面进入回调;
+4、default_font_page.on_leave():页面离开回调;
+]]
+
+local default_font_page = {}
+
+-- 当前选中项的索引(仅有一个返回按钮)
+local selected_index = 1
+
+--[[
+@api draw()
+@summary 绘制默认字体演示页面内容
+@return 无返回值
+@usage
+-- 在UI主循环中调用
+default_font_page.draw()
+]]
+function default_font_page.draw()
+    -- 标题
+    u8g2.DrawUTF8("内置字体页", 35, 10)
+
+    -- 字体演示(居中显示)
+    u8g2.DrawUTF8("合宙LuatOS", 25, 27)
+    u8g2.DrawUTF8("U8G2演示程序", 20, 42)
+    u8g2.DrawUTF8(os.date("%Y-%m-%d %H:%M:%S"), 0, 58)
+
+    -- 按钮区域
+    if selected_index == 1 then
+        -- 返回按钮:选中状态
+        u8g2.DrawButtonUTF8("返回", 5, 10, u8g2.BTN_INV + u8g2.BTN_BW1, 0, 2, 0)
+    else
+        -- 返回按钮:未选中状态
+        u8g2.DrawButtonUTF8("返回", 5, 10, u8g2.BTN_BW1, 0, 2, 0)
+    end
+end
+
+--[[
+@api handle_key(key_type)
+@summary 处理按键事件,实现页面导航
+@param string key_type 按键类型,可选值:
+  - "confirm":确认键,返回首页
+  - "next":切换选中状态(仅有一个按钮,无实际效果)
+  - "prev":切换选中状态(仅有一个按钮,无实际效果)
+@return bool 是否已处理该按键,true表示已处理
+@usage
+-- 在UI主循环中调用
+local handled = default_font_page.handle_key("confirm")
+]]
+function default_font_page.handle_key(key_type)
+    log.info("default_font_page.handle_key", "key_type:", key_type)
+
+    if key_type == "confirm" then
+        -- 确认键:返回首页
+        switch_page("home")
+        return true
+    elseif key_type == "next" or key_type == "prev" then
+        -- 切换选中项(只有一个按钮,所以切换无实际效果)
+        -- 但为了保持接口一致性,仍然返回true表示已处理
+        return true
+    end
+
+    return false
+end
+
+--[[
+@api on_enter()
+@summary 页面进入时的初始化操作,重置选中项和设置帧更新时间
+@return 无返回值
+@usage
+-- 在页面切换时自动调用
+default_font_page.on_enter()
+]]
+function default_font_page.on_enter()
+    -- 页面进入时初始化
+    selected_index = 1
+    -- 设置较短的帧更新时间,使时间显示能够实时更新
+    frame_time = 300  -- 300ms
+end
+
+--[[
+@api on_leave()
+@summary 页面离开时的清理操作,恢复默认帧更新时间
+@return 无返回值
+@usage
+-- 在页面切换时自动调用
+default_font_page.on_leave()
+]]
+function default_font_page.on_leave()
+    -- 页面离开时的清理操作
+    -- 恢复默认的帧更新时间(60秒)
+    frame_time = 60 * 1000
+end
+
+return default_font_page

+ 148 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/ui/gtfont_page.lua

@@ -0,0 +1,148 @@
+--[[
+@module  gtfont_page
+@summary U8G2 GTFont演示页面模块 - 128x64屏幕
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为GTFont演示页面模块,核心业务逻辑为:
+1、展示GTFont外置字库的字体大小切换功能;
+2、显示当前选中的字体大小;
+3、提供返回按钮和字体大小切换按钮;
+4、支持BOOT键和PWR键的导航操作;
+
+本文件的对外接口有4个:
+1、gtfont_page.draw():绘制页面内容;
+2、gtfont_page.handle_key(key_type):处理按键事件;
+3、gtfont_page.on_enter():页面进入回调;
+4、gtfont_page.on_leave():页面离开回调;
+]]
+
+local gtfont_page = {}
+
+-- 当前选中按钮的索引(1:返回, 2:切换)
+local selected_index = 1
+
+-- 字体大小选项数组
+local size_options = { 12, 14, 16, 18, 20 }
+
+-- 当前字体大小在数组中的索引
+local size_index = 1
+
+--[[
+@function get_current_size
+@summary 获取当前选中的字体大小
+@return number 当前字体大小
+]]
+local function get_current_size()
+    return size_options[size_index]
+end
+
+--[[
+@api draw()
+@summary 绘制GTFont演示页面内容
+@return 无返回值
+@usage
+-- 在UI主循环中调用
+gtfont_page.draw()
+]]
+function gtfont_page.draw()
+    -- 标题
+    u8g2.SetFont(u8g2.font_6x10)
+    u8g2.DrawUTF8("GTFont页", 34, 10)
+
+    -- 字体大小显示
+    u8g2.DrawUTF8("字体:", 5, 28)
+
+    local current_size = get_current_size()
+
+    -- 使用GTFont绘制字体大小
+    u8g2.drawGtfontUtf8(tostring(current_size) .. "号", current_size, 40, 16)
+
+    -- 示例文本区域
+    u8g2.DrawUTF8("示例文本:需外置字库", 5, 60)
+
+    -- 按钮区域(垂直排列)
+    if selected_index == 1 then
+        -- 返回按钮:选中状态
+        u8g2.DrawButtonUTF8("返回", 5, 10, u8g2.BTN_INV + u8g2.BTN_BW1, 0, 0, 0)
+    else
+        -- 返回按钮:未选中状态
+        u8g2.DrawButtonUTF8("返回", 5, 10, u8g2.BTN_BW1, 0, 0, 0)
+    end
+
+    if selected_index == 2 then
+        -- 切换按钮:选中状态
+        u8g2.DrawButtonUTF8("切换", 99, 10, u8g2.BTN_INV + u8g2.BTN_BW1, 0, 0, 0)
+    else
+        -- 切换按钮:未选中状态
+        u8g2.DrawButtonUTF8("切换", 99, 10, u8g2.BTN_BW1, 0, 0, 0)
+    end
+end
+
+--[[
+@api handle_key(key_type)
+@summary 处理按键事件,实现字体大小切换和页面导航
+@param string key_type 按键类型,可选值:
+  - "confirm":确认键,执行当前选中按钮的功能
+  - "next":切换到下一个按钮
+  - "prev":切换到上一个按钮
+@return bool 是否已处理该按键,true表示已处理
+@usage
+-- 在UI主循环中调用
+local handled = gtfont_page.handle_key("confirm")
+]]
+function gtfont_page.handle_key(key_type)
+    log.info("gtfont_page.handle_key", "key_type:", key_type)
+
+    if key_type == "confirm" then
+        -- 确认键:执行当前选中按钮的功能
+        if selected_index == 1 then
+            -- 返回按钮:返回首页
+            switch_page("home")
+        elseif selected_index == 2 then
+            -- 切换按钮:切换到下一个字体大小
+            size_index = size_index % #size_options + 1
+        end
+        return true
+    elseif key_type == "next" then
+        -- 切换到下一个按钮
+        selected_index = selected_index % 2 + 1
+        return true
+    elseif key_type == "prev" then
+        -- 切换到上一个按钮
+        selected_index = (selected_index - 2) % 2 + 1
+        return true
+    end
+
+    return false
+end
+
+--[[
+@api on_enter()
+@summary 页面进入时的初始化操作,重置选中项和字体大小索引
+@return 无返回值
+@usage
+-- 在页面切换时自动调用
+gtfont_page.on_enter()
+]]
+function gtfont_page.on_enter()
+    -- 页面进入时初始化
+    selected_index = 1 -- 默认选中返回按钮
+    size_index = 1     -- 默认使用第一个字体大小
+end
+
+--[[
+@api on_leave()
+@summary 页面离开时的清理操作
+@return 无返回值
+@usage
+-- 在页面切换时自动调用
+gtfont_page.on_leave()
+]]
+function gtfont_page.on_leave()
+    -- 页面离开时的清理操作
+    -- 当前无需特殊清理
+end
+
+return gtfont_page

+ 111 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/ui/home_page.lua

@@ -0,0 +1,111 @@
+--[[
+@module  home_page
+@summary U8G2主页模块 - 128x64屏幕
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为主页显示模块,核心业务逻辑为:
+1、显示主菜单,包含三个选项:组件演示、内置字体、GTFont演示;
+2、处理BOOT键和PWR键的导航和确认操作;
+3、管理当前选中项状态;
+
+本文件的对外接口有4个:
+1、home_page.draw():绘制页面内容;
+2、home_page.handle_key(key_type):处理按键事件;
+3、home_page.on_enter():页面进入回调;
+4、home_page.on_leave():页面离开回调;
+]]
+
+local home_page = {}
+
+-- 菜单项
+local menu_items = {
+    {name = "component", text = "1.组件演示", x = 15, y = 22},
+    {name = "default_font", text = "2.内置字体", x = 15, y = 40},
+    {name = "gtfont", text = "3.GTFont演示", x = 15, y = 58}
+}
+
+local selected_index = 1
+
+--[[
+@api draw()
+@summary 绘制主页内容
+@return 无返回值
+@usage
+-- 在UI主循环中调用
+home_page.draw()
+]]
+function home_page.draw()
+
+    -- 绘制按键提示
+    u8g2.DrawUTF8("BOOT:选择", 5, 10)
+    u8g2.DrawUTF8("PWR:确认", 70, 10)
+    
+    -- 绘制菜单项
+    for i, item in ipairs(menu_items) do
+        if i == selected_index then
+            -- 选中状态
+            u8g2.DrawButtonUTF8(item.text, item.x, item.y, 
+                u8g2.BTN_INV + u8g2.BTN_BW1, 100, 2, 0)
+        else
+            -- 未选中状态
+            u8g2.DrawButtonUTF8(item.text, item.x, item.y, 
+                u8g2.BTN_BW1, 100, 2, 0)
+        end
+    end
+end
+
+--[[
+@api handle_key(key_type)
+@summary 处理按键事件
+@param string key_type 按键类型,可选值:"confirm"、"next"、"prev"
+@return bool 是否已处理该按键
+@usage
+-- 在UI主循环中调用
+home_page.handle_key("confirm")
+]]
+function home_page.handle_key(key_type)
+    log.info("home_page.handle_key", "key_type:", key_type, "selected_index:", selected_index)
+    
+    if key_type == "confirm" then
+        -- 确认键:切换到选中的页面
+        local item = menu_items[selected_index]
+        switch_page(item.name)
+        return true
+    elseif key_type == "next" then
+        -- 向下选择
+        selected_index = selected_index % #menu_items + 1
+        return true
+    elseif key_type == "prev" then
+        -- 向上选择
+        selected_index = (selected_index - 2) % #menu_items + 1
+        return true
+    end
+    
+    return false
+end
+--[[@api on_enter()
+@summary 页面进入时的初始化操作
+@return 无返回值
+@usage
+-- 在页面切换时自动调用
+home_page.on_enter()
+]]
+function home_page.on_enter()
+    selected_index = 1  -- 重置选中项
+end
+
+--[[
+@api on_leave()
+@summary 页面离开时的清理操作
+@return 无返回值
+@usage
+-- 在页面切换时自动调用
+home_page.on_leave()
+]]
+function home_page.on_leave()
+    -- 页面离开时的清理操作
+end
+
+return home_page

+ 160 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/ui/u8g2/ui/ui_main.lua

@@ -0,0 +1,160 @@
+--[[
+@module  ui_main
+@summary U8G2主程序模块 - 128x64屏幕
+@version 1.0
+@date    2025.12.11
+@author  江访
+@usage
+本文件为U8G2图形界面的主控模块,核心业务逻辑为:
+1、管理四个页面:主页、组件演示页、内置字体页、GTFont演示页;
+2、处理按键事件并分发给当前页面;
+3、控制页面切换逻辑,调用页面的进入/离开回调函数;
+4、实现主渲染循环,定期更新屏幕显示;
+
+本文件的对外接口有2个:
+1、switch_page(new_page):页面切换接口;
+2、handle_key_event(key_event):按键事件处理入口;
+]]
+
+-- 加载页面
+local home_page = require("home_page")
+local component_page = require("component_page")
+local default_font_page = require("default_font_page")
+local gtfont_page = require("gtfont_page")
+
+-- 超时更新画面时间,默认60秒
+frame_time = 60 * 1000
+
+-- 页面管理
+local PAGE_NAMES = {
+    HOME = "home",
+    COMPONENT = "component",
+    DEFAULT_FONT = "default_font",
+    GTFONT = "gtfont"
+}
+
+-- 当前页面
+local current_page = PAGE_NAMES.HOME
+
+--[[
+@api handle_key_event(key_event)
+@summary 处理按键事件
+@param string key_event 按键事件类型,可选值:"boot_up"、"boot_down"、"pwr_up"、"pwr_down"
+@return bool 是否已处理该按键事件
+@usage
+-- 在主循环中调用
+handle_key_event("boot_up")
+]]
+-- 按键处理函数
+local function handle_key_event(key_event)
+    log.info("按键事件", "event:", key_event, "当前页面:", current_page)
+
+    -- 按键抬起生效
+    if key_event == "boot_up" then
+        -- BOOT键抬起:切换到下一个选项
+        if current_page == PAGE_NAMES.HOME then
+            return home_page.handle_key("next")
+        elseif current_page == PAGE_NAMES.COMPONENT then
+            return component_page.handle_key("next")
+        elseif current_page == PAGE_NAMES.DEFAULT_FONT then
+            return default_font_page.handle_key("next")
+        elseif current_page == PAGE_NAMES.GTFONT then
+            return gtfont_page.handle_key("next")
+        end
+        return false
+    elseif key_event == "pwr_up" then
+        -- PWR键抬起:确认/返回
+        if current_page == PAGE_NAMES.HOME then
+            return home_page.handle_key("confirm")
+        elseif current_page == PAGE_NAMES.COMPONENT then
+            return component_page.handle_key("confirm")
+        elseif current_page == PAGE_NAMES.DEFAULT_FONT then
+            return default_font_page.handle_key("confirm")
+        elseif current_page == PAGE_NAMES.GTFONT then
+            return gtfont_page.handle_key("confirm")
+        end
+    end
+    return false
+end
+
+--[[
+@api switch_page(new_page)
+@summary 切换当前显示的页面
+@param string new_page 要切换到的页面名称,可选值:"home"、"component"、"default_font"、"gtfont"
+@return 无返回值
+@usage
+-- 在页面处理函数中调用
+switch_page("home")
+]]
+-- 页面切换函数(供其他页面调用)
+function switch_page(new_page)
+    log.info("switch_page", "从", current_page, "切换到", new_page)
+
+    -- 调用旧页面的离开函数
+    if current_page == PAGE_NAMES.HOME and home_page.on_leave then
+        home_page.on_leave()
+    elseif current_page == PAGE_NAMES.COMPONENT and component_page.on_leave then
+        component_page.on_leave()
+    elseif current_page == PAGE_NAMES.DEFAULT_FONT and default_font_page.on_leave then
+        default_font_page.on_leave()
+    elseif current_page == PAGE_NAMES.GTFONT and gtfont_page.on_leave then
+        gtfont_page.on_leave()
+    end
+
+    current_page = new_page
+
+    -- 调用新页面的进入函数
+    if new_page == PAGE_NAMES.HOME and home_page.on_enter then
+        home_page.on_enter()
+    elseif new_page == PAGE_NAMES.COMPONENT and component_page.on_enter then
+        component_page.on_enter()
+    elseif new_page == PAGE_NAMES.DEFAULT_FONT and default_font_page.on_enter then
+        default_font_page.on_enter()
+    elseif new_page == PAGE_NAMES.GTFONT and gtfont_page.on_enter then
+        gtfont_page.on_enter()
+    end
+
+    log.info("ui_main", "已切换到页面:", current_page)
+end
+
+-- 主UI任务
+local function ui_main()
+
+    -- 预留1S给开机信息显示
+    sys.wait(1000)
+    log.info("ui_main", "启动UI主循环")
+
+    -- 初始化主页
+    home_page.on_enter()
+
+    -- 主渲染循环
+    while true do
+        -- 设置默认字体
+        u8g2.SetFont(u8g2.font_opposansm12_chinese)
+        -- 清空缓冲区
+        u8g2.ClearBuffer()
+
+        -- 根据当前页面绘制内容
+        if current_page == PAGE_NAMES.HOME then
+            home_page.draw()
+        elseif current_page == PAGE_NAMES.COMPONENT then
+            component_page.draw()
+        elseif current_page == PAGE_NAMES.DEFAULT_FONT then
+            default_font_page.draw()
+        elseif current_page == PAGE_NAMES.GTFONT then
+            gtfont_page.draw()
+        end
+
+        -- 刷新显示
+        u8g2.SendBuffer()
+
+        -- 等待按键事件
+        local result, key_event = sys.waitUntil("KEY_EVENT", frame_time)
+        if result then
+            handle_key_event(key_event)
+        end
+    end
+end
+
+-- 启动UI任务
+sys.taskInit(ui_main)

+ 0 - 1
module/Air780EPM/demo/WebSocket/websocket_sender.lua

@@ -115,7 +115,6 @@ local function send_item(ws_client)
                 log.info("wbs_sender", "发送成功", "长度", #item.data)
             end
             
-            -- 由于sent事件可能不会触发,我们直接认为发送成功
             if item.cb and item.cb.func then
                 item.cb.func(true, item.cb.para)
             end

+ 1 - 2
module/Air780EPM/demo/accessory_board/AirLCD_1000/exeasyui/hw_drv/hw_default_font_drv.lua

@@ -9,8 +9,7 @@
 1、根据配置的lcd和tp参数,初始化exEasyUI默认使用12号英文点阵字体、硬件显示和触摸;
 2、提供无需外部硬件的字体显示能力;
 
-本文件的对外接口有0个:
-1、require加载后自动执行初始化;
+本文件无对外接口,require加载后自动执行初始化;
 
 @api ui.hw_init(config)
 @summary 初始化exEasyUI硬件系统

+ 1 - 2
module/Air780EPM/demo/accessory_board/AirLCD_1000/exeasyui/ui/ui_main.lua

@@ -12,8 +12,7 @@
 4、订阅按键事件并分发到按键处理器;
 5、启动UI渲染主循环,维持界面刷新;
 
-本文件的对外接口有0个:
-1、require加载后自动启动UI主任务;
+本文件无对外接口,require加载后自动执行初始化;
 ]]
 
 local home_page = require("home_page")

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini