Преглед на файлове

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

mw преди 6 месеца
родител
ревизия
96a6d20c26
променени са 99 файла, в които са добавени 9034 реда и са изтрити 2139 реда
  1. 23 15
      components/airlink/src/exec/luat_airlink_cmd_exec_info.c
  2. 6 2
      components/airlink/src/task/luat_airlink_spi_master_task.c
  3. 8 1
      components/camera/luat_lib_camera.c
  4. 2 8
      components/eink/luat_lib_eink.c
  5. 1 1
      components/ethernet/common/dhcp_client.c
  6. 40 0
      components/lcd/luat_lib_lcd.c
  7. 0 2
      components/minmea/luat_lib_libgnss.c
  8. 40 1
      components/mobile/luat_lib_mobile.c
  9. 9 0
      components/mobile/luat_mobile.h
  10. 10 8
      components/network/adapter/luat_network_adapter.c
  11. 5 8
      components/network/adapter_lwip2/net_lwip2.c
  12. 24 11
      components/network/httpsrv/src/luat_httpsrv_lwip.c
  13. 3 1
      components/network/iperf/binding/luat_lib_iperf.c
  14. 4 1
      components/network/libemqtt/luat_lib_mqtt.c
  15. 2 2
      components/network/libhttp/luat_http_client.c
  16. 83 10
      components/network/netdrv/binding/luat_lib_netdrv.c
  17. 4 0
      components/network/netdrv/include/luat_netdrv.h
  18. 2 4
      components/network/netdrv/include/luat_netdrv_ch390h.h
  19. 9 2
      components/network/netdrv/include/luat_netdrv_event.h
  20. 1 3
      components/network/netdrv/include/luat_netdrv_whale.h
  21. 12 27
      components/network/netdrv/src/ch390h_task.c
  22. 37 6
      components/network/netdrv/src/luat_netdrv.c
  23. 37 37
      components/network/netdrv/src/luat_netdrv_ch390h.c
  24. 149 8
      components/network/netdrv/src/luat_netdrv_event.c
  25. 22 119
      components/network/netdrv/src/luat_netdrv_whale.c
  26. 32 10
      components/network/ulwip/src/ulwip_dhcp_client.c
  27. 8 0
      components/network/websocket/luat_lib_websocket.c
  28. 9 2
      components/network/websocket/luat_websocket.c
  29. 2 2
      components/sms/binding/luat_lib_sms.c
  30. 1 0
      components/tp/luat_lib_tp.c
  31. 4 3
      components/tp/luat_tp.c
  32. 15 12
      components/tp/luat_tp_cst9220.c
  33. 4 2
      components/u8g2/u8g2.h
  34. 87 2
      components/u8g2/u8g2_font.c
  35. 1839 0
      luat/demo/airui/demo_2.json
  36. 36 4
      luat/demo/airui/main.lua
  37. 13 0
      luat/demo/mobile/main.lua
  38. 3 0
      luat/include/luat_uart.h
  39. 2 1
      luat/modules/luat_lib_gpio.c
  40. 1 0
      luat/modules/luat_lib_mcu.c
  41. 8 37
      luat/modules/luat_lib_pm.c
  42. 18 11
      luat/modules/luat_lib_spi.c
  43. 20 1
      luat/modules/luat_lib_uart.c
  44. 22 6
      luat/modules/luat_lib_zbuff.c
  45. 0 81
      module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/airaudio.lua
  46. 541 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/exaudio.lua
  47. 561 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/extalk.lua
  48. 199 46
      module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/main.lua
  49. 0 128
      module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/talk.lua
  50. BIN
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/10.amr
  51. 541 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/exaudio.lua
  52. 62 192
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/main.lua
  53. 101 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/play_file.lua
  54. 108 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/play_stream.lua
  55. 104 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/play_tts.lua
  56. 106 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/readme.md
  57. 84 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/record_file.lua
  58. 55 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/record_stream.lua
  59. BIN
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/test.pcm
  60. BIN
      module/Air780EHM_Air780EHV_Air780EGH/demo/audio/音频硬件框架.png
  61. 119 0
      module/Air780EPM/demo/accessory_board/AirSHT30_1000/AirSHT30_1000.lua
  62. 63 0
      module/Air780EPM/demo/accessory_board/AirSHT30_1000/main.lua
  63. 55 0
      module/Air780EPM/demo/accessory_board/AirSHT30_1000/readme.md
  64. 34 0
      module/Air780EPM/demo/accessory_board/AirSHT30_1000/sht30_app.lua
  65. 2 2
      module/Air780EPM/demo/network_routing/4g_out_ethernet_in/main.lua
  66. 0 0
      module/Air780EPM/demo/network_routing/4g_out_ethernet_in/netif_app.lua
  67. 3 7
      module/Air780EPM/demo/network_routing/4g_out_ethernet_in/readme.md
  68. 199 0
      module/Air8000/demo/accessory_board/AIRSPINAND_1000/AIRSPINAND_1000.lua
  69. 53 0
      module/Air8000/demo/accessory_board/AIRSPINAND_1000/main.lua
  70. 109 0
      module/Air8000/demo/accessory_board/AIRSPINAND_1000/readme.md
  71. 141 0
      module/Air8000/demo/accessory_board/AirETH_1000/http/http_app.lua
  72. 88 0
      module/Air8000/demo/accessory_board/AirETH_1000/http/netdrv/netdrv_eth_spi.lua
  73. 94 0
      module/Air8000/demo/accessory_board/AirETH_1000/http/netdrv/netdrv_multiple.lua
  74. 27 0
      module/Air8000/demo/accessory_board/AirETH_1000/http/netdrv_device.lua
  75. 0 79
      module/Air8000/demo/accessory_board/AirETH_1000/lan.lua
  76. 11 8
      module/Air8000/demo/accessory_board/AirETH_1000/main.lua
  77. 0 49
      module/Air8000/demo/accessory_board/AirETH_1000/netif_app.lua
  78. 78 0
      module/Air8000/demo/accessory_board/AirETH_1000/network_routing/4g_out_ethernet_in_wifi_in/4g-eth-wifi.lua
  79. 55 0
      module/Air8000/demo/accessory_board/AirETH_1000/network_routing/wifi_out_ethernet_in_wifi_in/wifi-eth-wifi.lua
  80. 20 64
      module/Air8000/demo/accessory_board/AirETH_1000/readme.md
  81. 0 60
      module/Air8000/demo/accessory_board/AirETH_1000/wan.lua
  82. 0 339
      module/Air8000/demo/airtalk/airtalk_dev_ctrl.lua
  83. 0 37
      module/Air8000/demo/airtalk/audio_config.lua
  84. 0 27
      module/Air8000/demo/airtalk/demo_define.lua
  85. 541 0
      module/Air8000/demo/airtalk/exaudio.lua
  86. 561 0
      module/Air8000/demo/airtalk/extalk.lua
  87. 52 62
      module/Air8000/demo/airtalk/main.lua
  88. 112 0
      module/Air8000/demo/airtalk/readme.md
  89. 196 0
      module/Air8000/demo/airtalk/talk.lua
  90. 1 1
      module/Air8000/demo/audio/exaudio.lua
  91. 9 55
      module/Air8000/project/整机开发板出厂工程/user/airaudio.lua
  92. 541 0
      module/Air8000/project/整机开发板出厂工程/user/exaudio.lua
  93. 561 0
      module/Air8000/project/整机开发板出厂工程/user/extalk.lua
  94. 5 3
      module/Air8000/project/整机开发板出厂工程/user/main.lua
  95. 61 439
      module/Air8000/project/整机开发板出厂工程/user/talk.lua
  96. 2 2
      module/Air8101/demo/tf_card/tfcard_app.lua
  97. 52 0
      module/spec/合宙IOT通用报文协议AirCloud_1.0.md
  98. 60 88
      script/libs/exfotawifi.lua
  99. 10 0
      script/libs/httpdns.lua

+ 23 - 15
components/airlink/src/exec/luat_airlink_cmd_exec_info.c

@@ -16,6 +16,7 @@
 #include "lwip/pbuf.h"
 #include "lwip/ip_addr.h"
 #include "luat_netdrv_whale.h"
+#include "luat_netdrv_event.h"
 
 #define LUAT_LOG_TAG "airlink"
 #include "luat_log.h"
@@ -61,18 +62,21 @@ __AIRLINK_CODE_IN_RAM__ int luat_airlink_cmd_exec_dev_info(luat_airlink_cmd_t* c
                 drv->netif->hwaddr_len = 6;
 
                 // STA网络状态对吗?
+                // LLOGD("sta station %d  netif link is %d", dev->wifi.sta_state, netif_is_link_up(drv->netif));
                 if (dev->wifi.sta_state == 0) {
-                    if (netif_is_up(drv->netif)) {
+                    if (netif_is_link_up(drv->netif)) {
                         // 网卡掉线了哦
                         LLOGD("wifi sta掉线了");
-                        luat_netdrv_whale_ipevent(drv, 0);
+                        // luat_netdrv_whale_ipevent(drv, 0);
+                        luat_netdrv_set_link_updown(drv, 0);
                     }
                 }
                 else {
-                    if (netif_is_up(drv->netif) == 0) {
+                    if (netif_is_link_up(drv->netif) == 0) {
                         // 网卡上线了哦
                         LLOGD("wifi sta上线了");
-                        luat_netdrv_whale_ipevent(drv, 1);
+                        // luat_netdrv_whale_ipevent(drv, 1);
+                        luat_netdrv_set_link_updown(drv, 1);
                     }
                 }
                 break;
@@ -96,18 +100,20 @@ __AIRLINK_CODE_IN_RAM__ int luat_airlink_cmd_exec_dev_info(luat_airlink_cmd_t* c
                 }
                 // AP网络状态对吗?
                 if (dev->wifi.ap_state == 0) {
-                    if (netif_is_up(drv->netif)) {
+                    if (netif_is_link_up(drv->netif)) {
                         // 网卡掉线了哦
                         LLOGD("wifi ap已关闭");
-                        luat_netdrv_whale_ipevent(drv, 0);
+                        // luat_netdrv_whale_ipevent(drv, 0);
+                        luat_netdrv_set_link_updown(drv, 0);
                     }
                 }
                 else {
-                    if (netif_is_up(drv->netif) == 0) {
+                    if (netif_is_link_up(drv->netif) == 0) {
                         // 网卡上线了哦
                         ipaddr_ntoa_r(&drv->netif->ip_addr, buff, 32);
-                        LLOGD("wifi ap已开启 %s", buff);
-                        luat_netdrv_whale_ipevent(drv, 1);
+                        LLOGD("wifi ap已开启 %s %p", buff, drv->netif);
+                        // luat_netdrv_whale_ipevent(drv, 1);
+                        luat_netdrv_set_link_updown(drv, 1);
                     }
                 }
                 break;
@@ -126,18 +132,20 @@ __AIRLINK_CODE_IN_RAM__ int luat_airlink_cmd_exec_dev_info(luat_airlink_cmd_t* c
         // 1是已注册, 5是漫游且已注册
         if (dev->cat1.cat_state != 1 && dev->cat1.cat_state != 5) {
             // 掉线了
-            if (netif_is_up(drv->netif)) {
+            if (netif_is_link_up(drv->netif)) {
                 // 网卡掉线了哦
-                LLOGD("4G网卡掉线了");
-                luat_netdrv_whale_ipevent(drv, 0);
+                LLOGD("4G代理网卡掉线了");
+                // luat_netdrv_whale_ipevent(drv, 0);
+                luat_netdrv_set_link_updown(drv, 0);
             }
         }
         else {
             // 上线了
-            if (netif_is_up(drv->netif) == 0) {
+            if (netif_is_link_up(drv->netif) == 0) {
                 // 网卡上线了哦
-                LLOGD("4G网卡上线了");
-                luat_netdrv_whale_ipevent(drv, 1);
+                LLOGD("4G代理网卡上线了");
+                // luat_netdrv_whale_ipevent(drv, 1);
+                luat_netdrv_set_link_updown(drv, 1);
             }
         }
     }

+ 6 - 2
components/airlink/src/task/luat_airlink_spi_master_task.c

@@ -208,15 +208,19 @@ static uint8_t slave_is_irq_ready = 0;
 
 __AIRLINK_CODE_IN_RAM__ void airlink_transfer_and_exec(uint8_t *txbuff, uint8_t *rxbuff)
 {
-    // 拉低片选, 准备发送数据
-    luat_gpio_set(AIRLINK_SPI_CS_PIN, 0);
     // 清除link
     memset(&s_link, 0, sizeof(airlink_link_data_t));
     airlink_link_data_t *link = NULL;
 
     g_airlink_statistic.tx_pkg.total++;
+    //luat_spi_lock(MASTER_SPI_ID);
+    // 拉低片选, 准备发送数据
+    luat_gpio_set(AIRLINK_SPI_CS_PIN, 0);
+    // 发送数据
     luat_spi_transfer(MASTER_SPI_ID, (const char *)txbuff, TEST_BUFF_SIZE, (char *)rxbuff, TEST_BUFF_SIZE);
+    // 拉高片选, 结束发送
     luat_gpio_set(AIRLINK_SPI_CS_PIN, 1);
+    //luat_spi_unlock(MASTER_SPI_ID);
     // luat_airlink_print_buff("RX", rxbuff, 32);
     // 对接收到的数据进行解析
     link = luat_airlink_data_unpack(rxbuff, TEST_BUFF_SIZE);

+ 8 - 1
components/camera/luat_lib_camera.c

@@ -248,7 +248,14 @@ static int l_camera_init(lua_State *L){
         lua_pop(L, 1);
         result = luat_camera_init(&conf);
         if (result < 0) {
-            lua_pushboolean(L, 0);
+            if (conf.async) {
+                camera_idp = luat_pushcwait(L);
+                lua_pushboolean(L, 0);
+                luat_pushcwait_error(L,1);
+            }
+            else {
+                lua_pushboolean(L, 0);
+            }
         } else {
             if (conf.async) {
                 camera_idp = luat_pushcwait(L);

+ 2 - 8
components/eink/luat_lib_eink.c

@@ -919,14 +919,6 @@ static int l_eink_bat(lua_State *L)
     return 0;
 }
 
-/**
-缓冲区绘制天气图标
-@api eink.weather_icon(x, y, code)
-@int x坐标
-@int y坐标
-@int 天气代号
-@return nil 无返回值
-*/
 static int l_eink_weather_icon(lua_State *L)
 {
     size_t len;
@@ -1278,7 +1270,9 @@ static const rotable_Reg_t reg_eink[] =
 
     { "qrcode",         ROREG_FUNC(l_eink_qrcode)},
     { "bat",            ROREG_FUNC(l_eink_bat)},
+    #ifndef LUAT_USE_EINK_LITE
     { "weather_icon",   ROREG_FUNC(l_eink_weather_icon)},
+    #endif
 
     { "model",          ROREG_FUNC(l_eink_model)},
     { "drawXbm",        ROREG_FUNC(l_eink_drawXbm)},

+ 1 - 1
components/ethernet/common/dhcp_client.c

@@ -267,7 +267,7 @@ int ip4_dhcp_run(dhcp_client_info_t *dhcp, Buffer_Struct *in, Buffer_Struct *out
 	*remote_ip = 0xffffffff;
 	int result = 0;
 	uint64_t tnow = luat_mcu_tick64_ms();
-	LLOGD("dhcp state %d %lld %lld %lld", dhcp->state, tnow, dhcp->lease_p1_time, dhcp->lease_p2_time);
+	LLOGD("dhcp state %d tnow %lld p1 %lld p2 %lld", dhcp->state, tnow, dhcp->lease_p1_time, dhcp->lease_p2_time);
 	if (in)
 	{
 		result = analyze_ip4_dhcp(dhcp, in);

+ 40 - 0
components/lcd/luat_lib_lcd.c

@@ -6,6 +6,12 @@
 @date    2021.06.16
 @demo lcd
 @tag LUAT_USE_LCD
+@usage
+--提醒:
+-- 1. 本模块需要硬件支持, 请确认你的设备有lcd屏幕
+-- 2. 本功能支持 SPI QSPI RGB等多种接口的lcd屏幕, 取决于具体的模组硬件
+-- 3. 对于Air780EHM/Air8000 系列, 默认开启JPG硬件解码, JPG图片的长宽都需要是16的倍数, 否则会出现画面拉伸的现象
+-- 4. 大部分API都只能在lcd.init()成功后使用
 */
 #include "luat_base.h"
 #include "luat_lcd.h"
@@ -1109,6 +1115,39 @@ static int l_lcd_set_font(lua_State *L) {
     return 1;
 }
 
+/*
+设置使用文件系统中的字体文件
+@api lcd.setFontfile(font, indentation)
+@string filename 字体文件
+@int indentation, 等宽字体ascii右侧缩进0~127个pixel,等宽字体的ascii字符可能在右侧有大片空白,用户可以选择删除部分。留空或者超过127则直接删除右半边, 非等宽字体无效
+@usage
+-- 设置为字体,对之后的drawStr有效,调用lcd.drawStr前一定要先设置
+
+-- 若提示 "only font pointer is allow" , 则代表当前固件不含对应字体, 可使用云编译服务免费定制
+-- 云编译文档: https://wiki.luatos.com/develop/compile/Cloud_compilation.html
+
+lcd.setFontfile("/sd/u8g2_font_opposansm12.bin")
+lcd.drawStr(40,10,"drawStr")
+sys.wait(2000)
+*/
+static int l_lcd_set_fontfile(lua_State *L) {
+    if (lcd_dft_conf == NULL) {
+        LLOGE("lcd not init");
+        return 0;
+    }
+    size_t sz;
+    const uint8_t* font_filename = (const uint8_t*)luaL_checklstring(L, 1, &sz);
+    lcd_dft_conf->luat_lcd_u8g2.font_file = luat_fs_fopen(font_filename, "rb");
+    luat_u8g2_set_ascii_indentation(0xff);
+    u8g2_SetFont(&(lcd_dft_conf->luat_lcd_u8g2), NULL);
+    if (lua_isinteger(L, 2)) {
+        int indentation = luaL_checkinteger(L, 2);
+        luat_u8g2_set_ascii_indentation(indentation);
+    }
+    lua_pushboolean(L, 1);
+    return 1;
+}
+
 /*
 显示字符串
 @api lcd.drawStr(x,y,str,fg_color)
@@ -1960,6 +1999,7 @@ static const rotable_Reg_t reg_lcd[] =
     { "setupBuff",  ROREG_FUNC(l_lcd_setup_buff)},
     { "autoFlush",  ROREG_FUNC(l_lcd_auto_flush)},
     { "setFont",    ROREG_FUNC(l_lcd_set_font)},
+    { "setFontfile",    ROREG_FUNC(l_lcd_set_fontfile)},
     { "setDefault", ROREG_FUNC(l_lcd_set_default)},
     { "getDefault", ROREG_FUNC(l_lcd_get_default)},
     { "getSize",    ROREG_FUNC(l_lcd_get_size)},

+ 0 - 2
components/minmea/luat_lib_libgnss.c

@@ -67,8 +67,6 @@ static int gnss_txt_cb = 0;
 // static int gnss_rmc_cb = 0;
 static int gnss_other_cb = 0;
 
-void luat_uart_set_app_recv(int id, luat_uart_recv_callback_t cb);
-
 static inline void push_gnss_value(lua_State *L, struct minmea_float *f, int mode) {
     if (f->value == 0) {
         lua_pushinteger(L, 0);

+ 40 - 1
components/mobile/luat_lib_mobile.c

@@ -1260,7 +1260,7 @@ static int l_mobile_event_handle(lua_State* L, void* ptr) {
     uint8_t index = 0;
     uint8_t status = 0;
     int ret = 0;
-
+    int16_t sIntraSearchP, sNonIntraSearchP, sIntraSearchQ, sNonIntraSearchQ;
 
     rtos_msg_t* msg = (rtos_msg_t*)lua_topointer(L, -1);
     event = msg->arg1;
@@ -1561,6 +1561,45 @@ end)
             break;
         }
         break;
+	case LUAT_MOBILE_EVENT_RRC:
+		// LLOGD("LUAT_MOBILE_EVENT_RRC status %d",status);
+/*
+@sys_pub mobile
+RRC部分信息上报,2025/9/15启用
+RRC_IND
+@usage
+sys.subscribe("RRC_IND", function(event, value, ...)
+	log.info("rrc status", event, value, ...)
+end)
+event目前有
+1、"DRX",DRX周期值,后续跟1个参数为具体的DRX周期值,单位ms,目前只有320,640,1280,2560
+2、"IDLE_MEAS_THRESHOLD",RRC IDLE下邻区测量阈值,后续跟4个参数为具体的测量阈值,单位dbm
+4个参数分别为sIntraSearchP, sNonIntraSearchP, sIntraSearchQ, sNonIntraSearchQ
+当rsrp <= sIntraSearchP,启动同频邻区测量,低功耗下功耗有所升高
+当rsrp <= sNonIntraSearchP,启动异频邻区测量,低功耗下功耗显著升高
+如果sIntraSearchQ不为0,当rsrq <= sIntraSearchQ,启动同频邻区测量,低功耗下功耗有所升高
+如果sNonIntraSearchQ不为0,当rsrq <= sNonIntraSearchQ,启动异频邻区测量,低功耗下功耗显著升高
+*/
+		switch(status)
+		{
+		case LUAT_MOBILE_RRC_DRX_CYCLE_UPDATED:
+            lua_pushstring(L, "RRC_IND");
+            lua_pushstring(L, "DRX");
+            lua_pushinteger(L, index * 320);
+            lua_call(L, 3, 0);
+			break;
+		case LUAT_MOBILE_RRC_IDLE_MEAS_THRESHOLD:
+            lua_pushstring(L, "RRC_IND");
+            lua_pushstring(L, "IDLE_MEAS_THRESHOLD");
+            luat_mobile_rrc_get_idle_meas_threshold(&sIntraSearchP, &sNonIntraSearchP, &sIntraSearchQ, &sNonIntraSearchQ);
+            lua_pushinteger(L, sIntraSearchP);
+            lua_pushinteger(L, sNonIntraSearchP);
+            lua_pushinteger(L, sIntraSearchQ);
+            lua_pushinteger(L, sNonIntraSearchQ);
+            lua_call(L, 6, 0);
+			break;
+		}
+		break;
 	default:
 		break;
 	}

+ 9 - 0
components/mobile/luat_mobile.h

@@ -532,6 +532,7 @@ typedef enum LUAT_MOBILE_EVENT
 	LUAT_MOBILE_EVENT_IMS_REGISTER_STATUS, /**< IMS注册状态,volte必须在注册成功情况下使用*/
 	LUAT_MOBILE_EVENT_CC,	/**< 通话相关消息*/
 	LUAT_MOBILE_EVENT_USB_ETH_ON,
+	LUAT_MOBILE_EVENT_RRC,
 	LUAT_MOBILE_EVENT_FATAL_ERROR = 0xff,/**< 网络遇到严重故障*/
 }LUAT_MOBILE_EVENT_E;
 
@@ -648,6 +649,12 @@ typedef enum LUAT_MOBILE_CC_PLAY_IND
 	LUAT_MOBILE_CC_PLAY_CALL_INCOMINGCALL_RINGING, /**< 播放来电铃声*/
 }LUAT_MOBILE_CC_PLAY_IND_E;
 
+typedef enum LUAT_MOBILE_RRC_IND
+{
+	LUAT_MOBILE_RRC_DRX_CYCLE_UPDATED,
+	LUAT_MOBILE_RRC_IDLE_MEAS_THRESHOLD,
+	LUAT_MOBILE_RRC_IDLE_MEAS_ACTION,
+}LUAT_MOBILE_RRC_IND_E;
 /**
  * @brief 获取当前SIM卡状态
  *
@@ -735,6 +742,8 @@ void luat_mobile_rrc_auto_release_pause(uint8_t onoff);
  */
 void luat_mobile_rrc_release_once(void);
 
+void luat_mobile_rrc_get_idle_meas_threshold(int16_t *sIntraSearchP, int16_t *sNonIntraSearchP, int16_t *sIntraSearchQ, int16_t *sNonIntraSearchQ);
+
 /**
  * @brief 获取当前与基站的RRC状态,等于AT+CSCON
  *

+ 10 - 8
components/network/adapter/luat_network_adapter.c

@@ -18,6 +18,10 @@
 #include "luat_netdrv_event.h"
 #endif
 
+#ifdef LUAT_USE_NETDRV
+extern void luat_netdrv_fire_socket_event_netctrl(uint32_t event_id, network_ctrl_t* ctrl, uint8_t proto);
+#endif
+
 typedef struct
 {
 #ifdef LUAT_USE_LWIP
@@ -811,6 +815,7 @@ static int network_state_shakehand(network_ctrl_t *ctrl, OS_EVENT *event, networ
 				#else
     			DBG_ERR("0x%x, %d", -result, ctrl->ssl->state);
 				#endif
+				DBG_Printf("TLS handshake failed %d !!\n", -result);
     			ctrl->need_close = 1;
     			return -1;
     		}
@@ -1095,9 +1100,9 @@ static int32_t network_default_socket_callback(void *data, void *param)
 	network_ctrl_t *ctrl = (network_ctrl_t *)event->Param3;
 
 	// 插入几个事件回调
-	if (event->ID != 0 && ctrl) {
+	if (event->ID != 0 && event->ID != EV_NW_DNS_RESULT && ctrl) {
 		#ifdef LUAT_USE_NETDRV
-		luat_netdrv_fire_socket_event_netctrl(event->ID, ctrl);
+		luat_netdrv_fire_socket_event_netctrl(event->ID, ctrl, 0);
 		#endif
 	}
 
@@ -1398,8 +1403,7 @@ network_ctrl_t *network_alloc_ctrl(uint8_t adapter_index)
 	if (i >= adapter->opt->max_socket_num) {DBG_ERR("adapter no more ctrl!");}
 	#ifdef LUAT_USE_NETDRV
 	if (ctrl) {
-		extern void luat_netdrv_fire_socket_event_netctrl(uint32_t event_id, network_ctrl_t* ctrl);
-		luat_netdrv_fire_socket_event_netctrl(0x81 + EV_NW_RESET, ctrl);
+		luat_netdrv_fire_socket_event_netctrl(0x81 + EV_NW_RESET, ctrl, 0);
 	}
 	#endif // LUAT_USE_NETDRV
 	return ctrl;
@@ -1419,8 +1423,7 @@ void network_release_ctrl(network_ctrl_t *ctrl)
 		if (&adapter->ctrl_table[i] == ctrl)
 		{
 			#ifdef LUAT_USE_NETDRV
-			extern void luat_netdrv_fire_socket_event_netctrl(uint32_t event_id, network_ctrl_t* ctrl);
-			luat_netdrv_fire_socket_event_netctrl(0x82 + EV_NW_RESET, ctrl);
+			luat_netdrv_fire_socket_event_netctrl(0x82 + EV_NW_RESET, ctrl, 0);
 			#endif
 			network_deinit_tls(ctrl);
 			if (ctrl->timer)
@@ -1602,8 +1605,7 @@ int network_socket_connect(network_ctrl_t *ctrl, luat_ip_addr_t *remote_ip)
 		DBG("network %d local port auto select %u",offset, local_port);
 	}
 	#ifdef LUAT_USE_NETDRV
-	extern void luat_netdrv_fire_socket_event_netctrl(uint32_t event_id, network_ctrl_t* ctrl);
-	luat_netdrv_fire_socket_event_netctrl(0x83 + EV_NW_RESET, ctrl);
+	luat_netdrv_fire_socket_event_netctrl(0x83 + EV_NW_RESET, ctrl, 0);
 	#endif
 	return adapter->opt->socket_connect(ctrl->socket_id, ctrl->tag, local_port, remote_ip, ctrl->remote_port, adapter->user_data);
 }

+ 5 - 8
components/network/adapter_lwip2/net_lwip2.c

@@ -759,7 +759,7 @@ static void net_lwip2_task(void *param)
 		}
 		p_ip = (ip_addr_t *)event.Param2;
 		ipaddr_ntoa_r(p_ip, ip_string, 64);
-		LLOGD("connect %s:%d %s", ip_string, prvlwip.socket[socket_id].remote_port, prvlwip.socket[socket_id].is_tcp ? "TCP" : "UDP");
+		LLOGD("adapter %d connect %s:%d %s", adapter_index, ip_string, prvlwip.socket[socket_id].remote_port, prvlwip.socket[socket_id].is_tcp ? "TCP" : "UDP");
 		local_ip = NULL;
 		#if LWIP_IPV6
 		if (p_ip->type == IPADDR_TYPE_V4)
@@ -925,13 +925,10 @@ static void net_lwip2_task(void *param)
 			luat_heap_free(ips);
 			break;
 		}
-		// ipaddr_ntoa_r(&ips[0], ip_string, 64);
-		// LLOGI("设置IP啊 %s", ip_string);
-		// ipaddr_ntoa_r(&ips[1], ip_string, 64);
-		// LLOGI("设置MARK啊 %s", ip_string);
-		// ipaddr_ntoa_r(&ips[2], ip_string, 64);
-		// LLOGI("设置GW啊 %s", ip_string);
-		netif_set_addr(prvlwip.lwip_netif[adapter_index], &ips[0], &ips[1], &ips[2]);
+		ip4_addr_t ip4 = {.addr=ip_addr_get_ip4_u32(&ips[0])};
+		ip4_addr_t netmask4 = {.addr=ip_addr_get_ip4_u32(&ips[1])};
+		ip4_addr_t gw4 = {.addr=ip_addr_get_ip4_u32(&ips[2])};
+		netif_set_addr(prvlwip.lwip_netif[adapter_index], &ip4, &netmask4, &gw4);
 		luat_heap_free(ips);
 		net_lwip2_check_network_ready(adapter_index);
 		break;

+ 24 - 11
components/network/httpsrv/src/luat_httpsrv_lwip.c

@@ -35,6 +35,7 @@ typedef struct client_socket_ctx
     uint32_t recv_done;
     char *buff;
     size_t buff_offset;
+    size_t buff_size;
     struct tcp_pcb* pcb;
     size_t send_size;
     size_t sent_size;
@@ -282,22 +283,33 @@ static err_t client_recv_cb(void *arg, struct tcp_pcb *tpcb,
         tcp_abort(tpcb);
         return ERR_ABRT;
     }
-    // LLOGD("tpcb %p p %p len %d err %d", tpcb, p, p->len, err);
+    LLOGD("tpcb %p p %p len %d err %d", tpcb, p, p->tot_len, err);
     client_socket_ctx_t* ctx = (client_socket_ctx_t*)arg;
     if (ctx->buff == NULL) {
-        ctx->buff = luat_heap_malloc(4096);
+        ctx->buff = luat_heap_malloc(p->tot_len);
         if (ctx->buff == NULL) {
-            LLOGD("out of memory when malloc client buff");
+            LLOGW("out of memory when malloc client buff %d", p->tot_len);
             // pbuf_free(p); // 需要吗?
             tcp_abort(tpcb);
             return ERR_ABRT;
         }
         ctx->buff_offset = 0;
+        ctx->buff_size = p->tot_len;
     }
-    memcpy(ctx->buff + ctx->buff_offset, p->payload, p->len);
-    ctx->buff_offset += p->len;
+    if (ctx->buff_offset + p->tot_len > ctx->buff_size) {
+        char* ptr = luat_heap_realloc(ctx->buff, ctx->buff_offset + p->tot_len);
+        if (ptr == NULL) {
+            LLOGW("out of memory when realloc client buff %d", ctx->buff_offset + p->tot_len);
+            tcp_abort(tpcb);
+            return ERR_ABRT;
+        }
+        ctx->buff = ptr;
+        ctx->buff_size = ctx->buff_offset + p->tot_len;
+    }
+    pbuf_copy_partial(p, ctx->buff + ctx->buff_offset, p->tot_len, 0);
+    ctx->buff_offset += p->tot_len;
     //LLOGD("request %.*s", p->len, p->payload);
-    tcp_recved(tpcb, p->len);
+    tcp_recved(tpcb, p->tot_len);
     pbuf_free(p);
 
     ctx->parser.data = ctx;
@@ -629,7 +641,7 @@ static int my_on_message_complete(http_parser* parser) {
 static int my_on_url(http_parser* parser, const char *at, size_t length) {
     LLOGD("on_header_url %p %d", at, length);
     if (length > 1024) {
-        LLOGW("request URL is too long!!!");
+        LLOGW("request URL is too long!!! %d", length);
         return HPE_INVALID_URL;
     }
     client_socket_ctx_t* client = (client_socket_ctx_t*)parser->data;
@@ -675,15 +687,16 @@ static int my_on_body(http_parser* parser, const char *at, size_t length) {
         }
     }
     else {
-        char* tmp = luat_heap_realloc(client->body, client->body_size + length);
+        // TODO 做成链表
+        char* tmp = luat_heap_realloc(client->body, length);
         if (tmp == NULL) {
-            LLOGE("realloc body FAIL!!!");
+            LLOGE("realloc body FAIL!!! %d", length);
             return HPE_INVALID_STATUS;
         }
         client->body = tmp;
     }
-    memcpy(client->body + client->body_size, at, length);
-    client->body_size += length;
+    memcpy(client->body, at, length);
+    client->body_size = length;
     return 0;
 }
 

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

@@ -114,7 +114,8 @@ static int start_gogogo(iperf_start_ctx_t* ctx) {
 /*
 启动server模式
 @api iperf.server(id, port)
-@int 网络适配器的id, 必须填, 例如 socket.LWIP_ETH0
+@int 网络适配器的id, 必须填, 例如 socket.LWIP_ETH
+@int 监听的端口, 可选, 默认5001
 @return boolean 成功返回true, 失败返回false
 @usage
 -- 启动server模式, 监听5001端口
@@ -149,6 +150,7 @@ static int l_iperf_server(lua_State *L) {
 @api iperf.client(id, ip, port)
 @int 网络适配器的id, 必须填, 例如 socket.LWIP_ETH0
 @string 远程服务器的ip, 只能是ipv4地址,不支持域名!!! 必须填值
+@int 远程服务器的端口, 可选, 默认5001
 @return boolean 成功返回true, 失败返回false
 @usage
 -- 启动client模式, 连接服务器的5001端口

+ 4 - 1
components/network/libemqtt/luat_lib_mqtt.c

@@ -89,7 +89,7 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
 		case MQTT_MSG_CONN_TIMEOUT: {
 			LLOGW("connect timeout %s %d!! expect conack in %ds", mqtt_ctrl->host, mqtt_ctrl->remote_port, mqtt_ctrl->conn_timeout);
 			#ifdef LUAT_USE_NETDRV
-			luat_netdrv_fire_socket_event_netctrl(EV_NW_TIMEOUT, mqtt_ctrl->netc);
+			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);
@@ -272,6 +272,9 @@ int32_t luatos_mqtt_callback(lua_State *L, void* ptr){
 					lua_call(L, 4, 0);
 				}
             }
+			#ifdef LUAT_USE_NETDRV
+			luat_netdrv_fire_socket_event_netctrl(EV_NW_SOCKET_ERROR, mqtt_ctrl->netc, 4);
+			#endif
 			break;
 		default : {
 			LLOGD("l_mqtt_callback error arg1:%d",msg->arg1);

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

@@ -187,7 +187,7 @@ static void http_resp_error(luat_http_ctrl_t *http_ctrl, int error_code) {
 	LLOGD("http_resp_error headers_complete:%d re_request_count:%d",http_ctrl->headers_complete,http_ctrl->re_request_count);
 	if (http_ctrl->close_state == 0 && http_ctrl->headers_complete==1 && http_ctrl->re_request_count < http_ctrl->retry_cnt_max){
 		#ifdef LUAT_USE_NETDRV
-		luat_netdrv_fire_socket_event_netctrl(EV_NW_TIMEOUT, http_ctrl->netc);
+		luat_netdrv_fire_socket_event_netctrl(EV_NW_TIMEOUT, http_ctrl->netc, 3);
 		#endif
 		http_ctrl->re_request_count++;
 		network_close(http_ctrl->netc, 0);
@@ -198,7 +198,7 @@ static void http_resp_error(luat_http_ctrl_t *http_ctrl, int error_code) {
 		}
 	}else if (http_ctrl->close_state==0){
 		#ifdef LUAT_USE_NETDRV
-		luat_netdrv_fire_socket_event_netctrl(EV_NW_TIMEOUT, http_ctrl->netc);
+		luat_netdrv_fire_socket_event_netctrl(EV_NW_TIMEOUT, http_ctrl->netc, 3);
 		#endif
 error:
 		http_ctrl->close_state=1;

+ 83 - 10
components/network/netdrv/binding/luat_lib_netdrv.c

@@ -48,6 +48,7 @@ netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=0,cs=8,irq=20})
 */
 static int l_netdrv_setup(lua_State *L) {
     luat_netdrv_conf_t conf = {0};
+    size_t len = 0;
     conf.id = luaL_checkinteger(L, 1);
     conf.impl = luaL_optinteger(L, 2, 0);
     conf.irqpin = 255; // 默认无效
@@ -75,6 +76,44 @@ static int l_netdrv_setup(lua_State *L) {
             conf.flags = luaL_checkinteger(L, -1);
         };
         lua_pop(L, 1);
+
+        #ifdef LUAT_USE_NETDRV_WG
+        // WG的配置参数比较多, 放在这里面传递
+        // 需要的参数有, private_key, public_key, endpoint, port, address, dns, mtu
+        if (lua_getfield(L, 3, "wg_private_key") == LUA_TSTRING) {
+            conf.wg_private_key = luaL_checklstring(L, -1, &len);
+        };
+        lua_pop(L, 1);
+        // 本地端口
+        if (lua_getfield(L, 3, "wg_listen_port") == LUA_TNUMBER) {
+            conf.wg_listen_port = luaL_checkinteger(L, -1);
+        };
+        lua_pop(L, 1);
+        // keepalive时长
+        if (lua_getfield(L, 3, "wg_keepalive") == LUA_TNUMBER) {
+            conf.wg_keepalive = luaL_checkinteger(L, -1);
+        };
+        lua_pop(L, 1);
+        // 预分享密钥
+        if (lua_getfield(L, 3, "wg_preshared_key") == LUA_TSTRING) {
+            conf.wg_preshared_key = luaL_checklstring(L, -1, &len);
+        };
+        lua_pop(L, 1);
+
+        // 对端信息, 公钥, IP地址, 端口
+        if (lua_getfield(L, 3, "wg_endpoint_key") == LUA_TSTRING) {
+            conf.wg_endpoint_key = luaL_checklstring(L, -1, &len);
+        };
+        lua_pop(L, 1);
+        if (lua_getfield(L, 3, "wg_endpoint_ip") == LUA_TSTRING) {
+            conf.wg_endpoint_ip = luaL_checklstring(L, -1, &len);
+        };
+        lua_pop(L, 1);
+        if (lua_getfield(L, 3, "wg_endpoint_port") == LUA_TNUMBER) {
+            conf.wg_endpoint_port = luaL_checkinteger(L, -1);
+        };
+        lua_pop(L, 1);
+        #endif
     }
     luat_netdrv_t* ret = luat_netdrv_setup(&conf);
     lua_pushboolean(L, ret != NULL);
@@ -424,15 +463,46 @@ static int l_socket_evt_cb(lua_State *L, void* ptr) {
         break;
     }
     lua_newtable(L);
-    // 填充参数表, 远端ip, 远端端口, 本地ip, 本地端口
+    // 填充参数表 远端ip, 远端端口, 本地ip, 本地端口
     char buff[32] = {0};
-    char* p = ipaddr_ntoa_r(&evt->remote_ip, buff, 32);
-    lua_pushstring(L, p);
-    lua_setfield(L, -2, "remote_ip");
+    if (!ip_addr_isany(&evt->remote_ip)) {
+        ipaddr_ntoa_r(&evt->remote_ip, buff, 32);
+        lua_pushstring(L, buff);
+        lua_setfield(L, -2, "remote_ip");
+    }
+
+    if (!ip_addr_isany(&evt->online_ip)) {
+        ipaddr_ntoa_r(&evt->online_ip, buff, 32);
+        lua_pushstring(L, buff);
+        lua_setfield(L, -2, "online_ip");
+    }
 
     lua_pushinteger(L, evt->remote_port);
     lua_setfield(L, -2, "remote_port");
 
+    switch (evt->proto)
+    {
+    case 1:
+        lua_pushstring(L, "tcp");
+        break;
+    case 2:
+        lua_pushstring(L, "udp");
+        break;
+    case 3:
+        lua_pushstring(L, "http");
+        break;
+    case 4:
+        lua_pushstring(L, "mqtt");
+        break;
+    case 5:
+        lua_pushstring(L, "websocket");
+        break;
+    default:
+        lua_pushstring(L, "unknown");
+        break;
+    }
+    lua_setfield(L, -2, "proto");
+
     // p = ipaddr_ntoa_r(&evt->local_ip, buff, 32);
     // lua_pushstring(L, p);
     // lua_setfield(L, -2, "local_ip");
@@ -479,16 +549,16 @@ netdrv.on(socket.LWIP_ETH, netdrv.EVT_SOCKET, function(id, event, params)
     -- event是事件id, 字符串类型, 
         - create 创建socket对象
         - release 释放socket对象
-        - connecting 正在连接
-        - connected 连接成功
+        - connecting 正在连接, 域名解析成功后出现
+        - connected 连接成功, TCP三次握手成功后出现
         - closed 连接关闭
-        - remote_close 远程关闭
+        - remote_close 远程关闭, 网络中断,或者服务器主动断开
         - timeout dns解析超时,或者tcp连接超时
         - error 错误,包括一切异常错误
-        - dns_result dns解析结果, 如果remote_ip为0.0.0.0,表示解析失败
     -- params是参数表
-        - remote_ip 远端ip地址
-        - remote_port 远端端口
+        - remote_ip 远端ip地址,未必存在
+        - remote_port 远端端口,未必存在
+        - online_ip 实际连接的ip地址,未必存在
         - domain_name 远端域名,如果是通过域名连接的话, release时没有这个值, create时也没有
     log.info("netdrv", "socket event", id, event, json.encode(params or {}))
     if params then
@@ -558,6 +628,9 @@ static const rotable_Reg_t reg_netdrv[] =
     //@const CH390 number 南京沁恒CH390系列,支持CH390D/CH390H, SPI通信
     { "CH390",          ROREG_INT(1)},
     { "UART",           ROREG_INT(16)}, // UART形式的网卡, 不带MAC, 直接IP包
+    #ifdef LUAT_USE_NETDRV_WG
+    { "WG",             ROREG_INT(32)}, // Wireguard VPN网卡
+    #endif
     //@const WHALE number 虚拟网卡
     { "WHALE",          ROREG_INT(64)}, // 通用WHALE设备
 

+ 4 - 0
components/network/netdrv/include/luat_netdrv.h

@@ -2,6 +2,7 @@
 #define LUAT_NETDRV_H
 
 #include "lwip/pbuf.h"
+#include "luat_ulwip.h"
 
 struct luat_netdrv;
 
@@ -68,6 +69,7 @@ typedef struct luat_netdrv_statics
 typedef struct luat_netdrv {
     int32_t id;
     struct netif* netif;
+    ulwip_ctx_t* ulwip;
     luat_netdrv_dataout_cb dataout;
     luat_netdrv_bootup_cb boot;
     luat_netdrv_ready_cb ready;
@@ -125,6 +127,8 @@ void luat_netdrv_netif_set_down(struct netif* netif);
 
 void luat_netdrv_netif_set_link_down(struct netif* netif);
 
+int luat_netdrv_dhcp_opt(luat_netdrv_t* drv, void* userdata, int enable);
+
 extern uint32_t g_netdrv_debug_enable;
 
 #ifndef __NETDRV_CODE_IN_RAM__

+ 2 - 4
components/network/netdrv/include/luat_netdrv_ch390h.h

@@ -21,10 +21,8 @@ typedef struct ch390h
     uint8_t intpin;
     uint8_t adapter_id;
     uint8_t status;
-    uint8_t dhcp;
-    uint8_t hwaddr[6];
-    struct netif* netif;
-    ulwip_ctx_t ulwip;
+    uint8_t init_done;
+    uint8_t init_step;
     luat_netdrv_t* netdrv;
     uint8_t rxbuff[1600];
     uint8_t txbuff[1600];

+ 9 - 2
components/network/netdrv/include/luat_netdrv_event.h

@@ -3,6 +3,7 @@
 
 #include "lwip/pbuf.h"
 #include "lwip/ip_addr.h"
+#include "luat_ulwip.h"
 
 // 事件, 用户可订阅
 enum {
@@ -16,9 +17,11 @@ enum {
 typedef struct netdrv_tcp_evt {
     uint8_t id; // 网络适配器ID
     uint8_t flags; // 事件标志, 标识
-    uint16_t re; // 保留字段, 目前未使用
+    uint8_t proto; // 协议类型, 1=TCP, 2=UDP, 3=HTTP, 4=MQTT, 5=WEBSOCKET, 6=FTP
+    uint8_t re; // 保留字段, 目前未使用
     ip_addr_t local_ip; // 本地IP地址
     ip_addr_t remote_ip; // 远程IP地址
+    ip_addr_t online_ip; // 连接上的IP地址, DNS事件无效
     uint16_t local_port; // 本地端口
     uint16_t remote_port; // 远程端口
     char domain_name[256]; // 解析的域名, DNS事件有效
@@ -37,6 +40,10 @@ typedef struct netdrv_tcpevt_reg {
 
 void luat_netdrv_register_socket_event_cb(uint8_t id, uint32_t flags, luat_netdrv_tcp_evt_cb cb, void* userdata);
 
-void luat_netdrv_fire_socket_event_netctrl(uint32_t event_id, network_ctrl_t* ctrl);
+void luat_netdrv_fire_socket_event_netctrl(uint32_t event_id, network_ctrl_t* ctrl, uint8_t proto);
+
+void luat_netdrv_send_ip_event(luat_netdrv_t* drv, uint8_t ready);
+
+void luat_netdrv_set_link_updown(luat_netdrv_t* drv, uint8_t updown);
 
 #endif

+ 1 - 3
components/network/netdrv/include/luat_netdrv_whale.h

@@ -8,12 +8,10 @@
 
 typedef struct luat_netdrv_whale {
     uint8_t id;
-    void* userdata;
     uint8_t flags;
     uint16_t mtu;
+    void* userdata;
     uint8_t mac[6];
-    uint8_t dhcp;
-    ulwip_ctx_t ulwip;
 }luat_netdrv_whale_t;
 
 

+ 12 - 27
components/network/netdrv/src/ch390h_task.c

@@ -18,6 +18,7 @@
 #include "luat_wdt.h"
 
 #include "luat_rtos.h"
+#include "luat_netdrv_event.h"
 
 #define LUAT_LOG_TAG "netdrv.ch390x"
 #include "luat_log.h"
@@ -63,7 +64,7 @@ static int ch390h_irq_cb(void *data, void *args) {
 }
 
 static int ch390h_bootup(ch390h_t* ch) {
-    if (ch->ulwip.netif != NULL) {
+    if (ch->init_done) {
         return 0;
     }
     // 初始化SPI设备, 由外部代码初始化, 因为不同bsp的速度不一样, 就不走固定值了
@@ -93,9 +94,7 @@ static int ch390h_bootup(ch390h_t* ch) {
         // LLOGI("enable pull mode, use pool mode");
     }
 
-    // 初始化dhcp相关资源
-    ch->ulwip.netif = ch->netif;
-    ch->ulwip.adapter_index = ch->adapter_id;
+    ch->init_done = 1;
     return 0;
 }
 
@@ -180,7 +179,7 @@ err_t ch390_netif_output(struct netif *netif, struct pbuf *p) {
         if (ch == NULL) {
             continue;
         }
-        if (ch->netif != netif) {
+        if (ch->netdrv->netif != netif) {
             continue;
         }
         ch390h_dataout_pbuf(ch, p);
@@ -265,11 +264,9 @@ static int task_loop_one(ch390h_t* ch, luat_ch390h_cstring_t* cs) {
         
         LLOGD("初始化MAC %02X%02X%02X%02X%02X%02X", buff[0], buff[1], buff[2], buff[3], buff[4], buff[5]);
         // TODO 判断mac是否合法
-        memcpy(ch->hwaddr, buff, 6);
-        memcpy(ch->netif->hwaddr, buff, 6);
+        memcpy(ch->netdrv->netif->hwaddr, buff, 6);
         ch->status = 2;
         ch->netdrv->dataout = ch390h_dataout;
-        netif_set_up(ch->netif);
         luat_ch390h_basic_config(ch);
         luat_ch390h_set_phy(ch, 1);
         luat_ch390h_set_rx(ch, 1);
@@ -306,28 +303,16 @@ static int task_loop_one(ch390h_t* ch, luat_ch390h_cstring_t* cs) {
         // LLOGD("PHY状态 %02X", buff[0]);
         luat_ch390h_set_phy(ch, 1);
         luat_ch390h_set_rx(ch, 1);
-        if (netif_is_link_up(ch->netif)) {
-            LLOGI("link is down %d %d", ch->spiid, ch->cspin);
-            luat_netdrv_netif_set_link_down(ch->netif);
-            ulwip_netif_ip_event(&ch->ulwip);
-            if (ch->dhcp) {
-                // 停止dhcp定时器
-                ulwip_dhcp_client_stop(&ch->ulwip);
-            }
+        if (netif_is_link_up(ch->netdrv->netif)) {
+            LLOGI("link is down %d %d %p", ch->spiid, ch->cspin, ch->netdrv->netif);
+            luat_netdrv_set_link_updown(ch->netdrv, 0);
         }
         return 0; // 网络断了, 没那么快恢复的, 等吧
     }
 
-    if (!netif_is_link_up(ch->netif)) {
+    if (!netif_is_link_up(ch->netdrv->netif)) {
         LLOGI("link is up %d %d %s", ch->spiid, ch->cspin, (NSR & (1<<7)) ? "10M" : "100M");
-        netif_set_link_up(ch->netif);
-        luat_rtos_task_sleep(20); // 等待50ms
-        ulwip_netif_ip_event(&ch->ulwip);
-        if (ch->dhcp) {
-            luat_rtos_task_sleep(30); // 等待30ms再启动dhcp
-            // 启动dhcp定时器
-            ulwip_dhcp_client_start(&ch->ulwip);
-        }
+        luat_netdrv_set_link_updown(ch->netdrv, 1);
     }
 
     if (cs) {
@@ -366,7 +351,7 @@ static int task_loop_one(ch390h_t* ch, luat_ch390h_cstring_t* cs) {
             }
             else {
                 // 如果返回值是0, 那就是继续处理, 输入到netif
-                ret = luat_netdrv_netif_input_proxy(ch->netif, ch->rxbuff, len - 4);
+                ret = luat_netdrv_netif_input_proxy(ch->netdrv->netif, ch->rxbuff, len - 4);
                 if (ret) {
                     LLOGE("luat_netdrv_netif_input_proxy 返回错误!!! ret %d", ret);
                     return 1;
@@ -396,7 +381,7 @@ static int task_loop(ch390h_t *ch, luat_ch390h_cstring_t* cs) {
     int ret = 0;
     for (size_t i = 0; i < MAX_CH390H_NUM; i++)
     {
-        if (ch390h_drvs[i] != NULL && ch390h_drvs[i]->netif != NULL) {
+        if (ch390h_drvs[i] != NULL && ch390h_drvs[i]->init_step) {
             ret += task_loop_one(ch390h_drvs[i], ch == ch390h_drvs[i] ? cs : NULL);
         }
     }

+ 37 - 6
components/network/netdrv/src/luat_netdrv.c

@@ -18,25 +18,33 @@ 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) {
-    if (conf->id < 0 || conf->id >= NW_ADAPTER_QTY) {
+    int id = conf->id;
+    if (id < 0 || id >= NW_ADAPTER_QTY) {
         return NULL;
     }
     int ret = 0;
-    if (drvs[conf->id] == NULL) {
+    if (drvs[id] == NULL) {
         // 注册新的设备?
         #ifdef __LUATOS__
         #ifdef LUAT_USE_NETDRV_CH390H
         if (conf->impl == 1) { // CH390H
-            drvs[conf->id] = luat_netdrv_ch390h_setup(conf);
-            return drvs[conf->id];
+            drvs[id] = luat_netdrv_ch390h_setup(conf);
+            return drvs[id];
         }
         #endif
         #ifdef LUAT_USE_AIRLINK
         if (conf->impl == 64) { // WHALE
-            drvs[conf->id] = luat_netdrv_whale_setup(conf);
-            return drvs[conf->id];
+            drvs[id] = luat_netdrv_whale_setup(conf);
+            return drvs[id];
+        }
+        #endif
+        #ifdef LUAT_USE_NETDRV_WG
+        if (conf->impl == 32) { // WG
+            drvs[id] = luat_netdrv_wg_setup(conf);
+            return drvs[id];
         }
         #endif
         #endif
@@ -318,3 +326,26 @@ void luat_netdrv_netif_set_link_down(struct netif* netif) {
     nd6_cleanup_netif(netif);
     #endif /* LWIP_IPV6 */
 }
+
+// DHCP操作
+
+int luat_netdrv_dhcp_opt(luat_netdrv_t* drv, void* userdata, int enable) {
+    if (drv->ulwip == NULL) {
+        return -1;
+    }
+    if (drv->ulwip->dhcp_enable == enable) {
+        return 0;
+    }
+    // cfg->dhcp = (uint8_t)enable;
+    drv->ulwip->dhcp_enable = enable;
+    if (drv->ulwip->netif == NULL) {
+        return 0;
+    }
+    if (enable) {
+        ulwip_dhcp_client_start(drv->ulwip);
+    }
+    else {
+        ulwip_dhcp_client_stop(drv->ulwip);
+    }
+    return 0;
+}

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

@@ -28,19 +28,6 @@ extern err_t luat_netdrv_etharp_output(struct netif *netif, struct pbuf *q, cons
 
 extern err_t ch390_netif_output(struct netif *netif, struct pbuf *p);
 
-static int ch390h_dhcp(luat_netdrv_t* drv, void* userdata, int enable) {
-    ch390h_t* ch = (ch390h_t*)userdata;
-    ch->dhcp = (uint8_t)enable;
-    ch->ulwip.dhcp_enable = enable;
-    if (enable && ch->ulwip.netif) {
-        ulwip_dhcp_client_start(&ch->ulwip);
-    }
-    else {
-        ulwip_dhcp_client_stop(&ch->ulwip);
-    }
-    return 0;
-}
-
 static int ch390h_ctrl(luat_netdrv_t* drv, void* userdata, int cmd, void* buff, size_t len) {
     ch390h_t* ch = (ch390h_t*)userdata;
     if (ch == NULL) {
@@ -76,75 +63,88 @@ static err_t ch390_netif_init(struct netif *netif) {
     #endif
     netif->mtu        = 1460;
     netif->flags      = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET | NETIF_FLAG_IGMP | NETIF_FLAG_MLD6;
-    memcpy(netif->hwaddr, ch->hwaddr, ETH_HWADDR_LEN);
     netif->hwaddr_len = ETH_HWADDR_LEN;
-    net_lwip2_set_netif(ch->adapter_id, ch->netif);
+    netif_set_up(ch->netdrv->netif);
+    net_lwip2_set_netif(ch->adapter_id, ch->netdrv->netif);
     net_lwip2_register_adapter(ch->adapter_id);
     ch->status = 0;
-    LLOGD("netif init ok %d", ch->adapter_id);
+    LLOGD("adapter %d netif init ok", ch->adapter_id);
     return 0;
 }
 
 static void ch390_lwip_init(void* args) {
     ch390h_t* ch = (ch390h_t*)args;
-    netif_add(ch->netif, IP4_ADDR_ANY4, IP4_ADDR_ANY4, IP4_ADDR_ANY4, ch, ch390_netif_init, luat_netdrv_netif_input_main);
+    netif_add(ch->netdrv->netif, IP4_ADDR_ANY4, IP4_ADDR_ANY4, IP4_ADDR_ANY4, ch, ch390_netif_init, luat_netdrv_netif_input_main);
 }
 
 luat_netdrv_t* luat_netdrv_ch390h_setup(luat_netdrv_conf_t *cfg) {
-    
-    LLOGD("注册CH390H设备 SPI id %d cs %d irq %d", cfg->spiid, cfg->cspin, cfg->irqpin);
+
+    LLOGD("注册CH390H设备(%d) SPI id %d cs %d irq %d", cfg->id, cfg->spiid, cfg->cspin, cfg->irqpin);
     ch390h_t* ch = luat_heap_malloc(sizeof(ch390h_t));
-    if (ch == NULL) {
+    struct netif* netif = luat_heap_malloc(sizeof(struct netif));
+    luat_netdrv_t* drv = luat_heap_malloc(sizeof(luat_netdrv_t));
+    ulwip_ctx_t* ulwip = luat_heap_malloc(sizeof(ulwip_ctx_t));
+    if (ch == NULL || netif == NULL || drv == NULL || ulwip == NULL) {
         LLOGD("分配CH390H内存失败!!!");
-        return NULL;
+        goto clean;
     }
+    
     memset(ch, 0, sizeof(ch390h_t));
+    memset(netif, 0, sizeof(struct netif));
+    memset(drv, 0, sizeof(luat_netdrv_t));
+    memset(ulwip, 0, sizeof(ulwip_ctx_t));
+
     ch->adapter_id = cfg->id;
     ch->cspin = cfg->cspin;
     ch->spiid = cfg->spiid;
     ch->intpin = cfg->irqpin;
+    // ch->dhcp = 1;
+    ulwip->dhcp_enable = 1;
+    ulwip->adapter_index = cfg->id;
+    ulwip->netif = netif;
+
+    drv->ulwip = ulwip;
+
     for (size_t i = 0; i < MAX_CH390H_NUM; i++)
     {
         if (ch390h_drvs[i] != NULL) {
             if (ch390h_drvs[i]->adapter_id == ch->adapter_id) {
                 LLOGE("已经注册过相同的adapter_id %d", ch->adapter_id);
-                luat_heap_free(ch);
-                return NULL;
+                goto clean;
             }
             if (ch390h_drvs[i]->spiid == ch->spiid  && ch390h_drvs[i]->cspin == ch->cspin) {
                 LLOGE("已经注册过相同的spi+cs %d %d",ch->spiid, ch->cspin);
-                luat_heap_free(ch);
-                return NULL;
+                goto clean;
             }
             if (ch390h_drvs[i]->intpin != 255 && ch390h_drvs[i]->intpin == ch->intpin) {
                 LLOGE("已经注册过相同的int脚 %d", ch->intpin);
-                luat_heap_free(ch);
-                return NULL;
+                goto clean;
             }
             continue;
         }
         ch390h_drvs[i] = ch;
-        if (ch390h_drvs[i]->netif == NULL) {
-            ch390h_drvs[i]->netif = luat_heap_malloc(sizeof(struct netif));
-            memset(ch390h_drvs[i]->netif, 0, sizeof(struct netif));
-        }
-        luat_netdrv_t* drv = luat_heap_malloc(sizeof(luat_netdrv_t));
-        memset(drv, 0, sizeof(luat_netdrv_t));
-        drv->netif = ch390h_drvs[i]->netif;
+        // ch390h_drvs[i]->netif = netif;
+        
+        drv->netif = netif;
         drv->userdata = ch390h_drvs[i];
         drv->id = ch->adapter_id;
         drv->dataout = NULL;
         drv->boot = NULL;
-        drv->dhcp = ch390h_dhcp;
+        drv->dhcp = luat_netdrv_dhcp_opt;
         drv->ctrl = ch390h_ctrl;
         ch->netdrv = drv;
         tcpip_callback_with_block((tcpip_callback_fn)ch390_lwip_init, ch, 1);
+        ch->init_step = 1; // 已经完成基础初始化
         extern void luat_ch390h_task_start(void);
         luat_ch390h_task_start();
-        LLOGD("ch390注册完成");
+        LLOGD("注册完成 adapter %d spi %d cs %d irq %d", ch->adapter_id, ch->spiid, ch->cspin, ch->intpin);
         return drv;
     }
     LLOGE("已经没有CH390H空位了!!!");
-    luat_heap_free(ch);
+clean:
+    if (ch) luat_heap_free(ch);
+    if (netif) luat_heap_free(netif);
+    if (drv) luat_heap_free(drv);
+    if (ulwip) luat_heap_free(ulwip);
     return NULL;
 }

+ 149 - 8
components/network/netdrv/src/luat_netdrv_event.c

@@ -3,6 +3,7 @@
 #include "luat_network_adapter.h"
 #include "luat_mem.h"
 #include "luat_mcu.h"
+#include "luat_ulwip.h"
 
 #include "lwip/ip_addr.h"
 #include "lwip/netif.h"
@@ -30,20 +31,23 @@ void luat_netdrv_register_socket_event_cb(uint8_t id, uint32_t evt_flags, luat_n
     // LLOGD("socket event cb adapter %d, flags=0x%02X userdata=%p", id, evt_flags, userdata);
 }
 
-__NETDRV_CODE_IN_RAM__ void luat_netdrv_fire_socket_event_netctrl(uint32_t event_id, network_ctrl_t* ctrl) {
+__NETDRV_CODE_IN_RAM__ void luat_netdrv_fire_socket_event_netctrl(uint32_t event_id, network_ctrl_t* ctrl, uint8_t proto) {
+    if (event_id < EV_NW_RESET || event_id == EV_NW_SOCKET_TX_OK || event_id == EV_NW_SOCKET_RX_NEW || event_id == EV_NW_STATE) {
+        return; // 其他事件无视
+    }
+    if (ctrl == NULL) {
+        return;
+    }
     uint8_t adapter_id = ctrl->adapter_index;
     //LLOGD("fire tcp event %08X for adapter %d", event_id, adapter_id);
-    if (event_id < EV_NW_RESET || event_id == EV_NW_SOCKET_TX_OK || event_id == EV_NW_SOCKET_RX_NEW) {
-        return; // 其他事件无视
+    if (adapter_id >= NW_ADAPTER_QTY || adapter_id <= 0) {
+        LLOGW("ctrl %p adapter %d is invalid, but socket event id is %08x", ctrl, adapter_id, event_id);
+        return;
     }
     if (s_tcpevt_regs[adapter_id].cb == NULL) {
         //LLOGD("TCP事件网络适配器ID无效 %d", adapter_id);
         return;
     }
-    if (ctrl == NULL || adapter_id >= NW_ADAPTER_QTY || adapter_id <= 0) {
-        LLOGW("TCP事件网络适配器ID无效 %d", adapter_id);
-        return;
-    }
     event_id -= EV_NW_RESET;
     if ((s_tcpevt_regs[adapter_id].flags & event_id) == 0) {
         //LLOGD("TCP事件网络适配器ID无效 %d", adapter_id);
@@ -52,6 +56,12 @@ __NETDRV_CODE_IN_RAM__ void luat_netdrv_fire_socket_event_netctrl(uint32_t event
     netdrv_tcp_evt_t evt = {0};
     evt.id = adapter_id;
     evt.flags = event_id;
+    if (proto == 0 && event_id != (0x81)) {
+        evt.proto = ctrl->is_tcp ? 1 : 2;
+    }
+    else {
+        evt.proto = proto;
+    }
     if (ctrl->domain_name && ctrl->domain_name_len > 0) {
         strncpy(evt.domain_name, ctrl->domain_name, sizeof(evt.domain_name) - 1);
         evt.domain_name[sizeof(evt.domain_name) - 1] = 0;
@@ -61,9 +71,140 @@ __NETDRV_CODE_IN_RAM__ void luat_netdrv_fire_socket_event_netctrl(uint32_t event
     // }
     // else {
     evt.remote_ip = ctrl->remote_ip;
+    evt.online_ip = ctrl->online_ip;
     // }
     evt.local_port = ctrl->local_port;
     evt.remote_port = ctrl->remote_port;
     evt.userdata = ctrl->user_data;
     s_tcpevt_regs[adapter_id].cb(&evt, s_tcpevt_regs[adapter_id].userdata);
-}
+}
+
+// IP 事件, 分成 IP_READY 和 IP_LOSE
+#ifdef __LUATOS__
+static int netif_ip_event_cb(lua_State *L, void* ptr) {
+    rtos_msg_t* msg = (rtos_msg_t*)lua_topointer(L, -1);
+    char buff[32] = {0};
+    luat_netdrv_t* netdrv = luat_netdrv_get(msg->arg1);
+    if (netdrv == NULL) {
+        return 0;
+    }
+    lua_getglobal(L, "sys_pub");
+    if (lua_isfunction(L, -1)) {
+        if (msg->arg2 == 0) {
+            LLOGD("IP_LOSE %d", netdrv->id);
+            lua_pushstring(L, "IP_LOSE");
+            lua_pushinteger(L, netdrv->id);
+            lua_call(L, 2, 0);
+        }
+        else {
+            ipaddr_ntoa_r(&netdrv->netif->ip_addr, buff,  32);
+            LLOGD("IP_READY %d %s", netdrv->id, buff);
+            lua_pushstring(L, "IP_READY");
+            lua_pushstring(L, buff);
+            lua_pushinteger(L, netdrv->id);
+            lua_call(L, 3, 0);
+        }
+    }
+    return 0;
+}
+
+void luat_netdrv_send_ip_event(luat_netdrv_t* drv, uint8_t ready) {
+    rtos_msg_t msg = {0};
+    if (drv == NULL || drv->netif == NULL) {
+        return;
+    }
+    msg.arg1 = drv->id;
+    msg.arg2 = ready;
+    msg.ptr = NULL;
+    msg.handler = netif_ip_event_cb;
+    luat_msgbus_put(&msg, 0);
+}
+
+#else
+// TODO 改成weak实现?
+void luat_netdrv_send_ip_event(luat_netdrv_t* drv, uint8_t updown) {
+    // nop
+}
+#endif
+
+typedef struct tmpptr {
+    luat_netdrv_t* drv;
+    uint8_t updown;
+}tmpptr_t;
+
+static void delay_dhcp_start(void* args) {
+    ulwip_ctx_t *ctx = (ulwip_ctx_t *)args;
+    if (ctx && ctx->dhcp_enable) {
+        ulwip_dhcp_client_start(ctx);
+    }
+}
+
+static void link_updown(void* args) {
+    tmpptr_t* ptr = (tmpptr_t*)args;
+    luat_netdrv_t* drv = ptr->drv;
+    uint8_t updown = ptr->updown;
+    void* userdata = NULL;
+    if (drv == NULL || drv->netif == NULL) {
+        return;
+    }
+    struct netif *netif = drv->netif;
+    ulwip_ctx_t *ulwip = drv->ulwip;
+    // LLOGI("netif %d link prev %d set %s %p", drv->id, netif_is_link_up(netif), updown ? "UP" : "DOWN", netif);
+    if (updown && netif_is_link_up(netif) == 0) {
+        LLOGD("网卡(%d)设置为UP", drv->id);
+        netif_set_link_up(netif);
+        net_lwip2_set_link_state(drv->id, 1);
+        if (ulwip) {
+            if (ulwip->netif == NULL) {
+                ulwip->netif = netif;
+            }
+            if (ulwip->dhcp_enable) {
+                ulwip_dhcp_client_stop(ulwip);
+                // 延时50ms, 避免netif_set_up和dhcp冲突
+                sys_timeout(50, delay_dhcp_start, ulwip);
+            }
+            else if (!ip_addr_isany(&netif->ip_addr)) {
+                // 静态IP, 那就发布IP_READY事件
+                luat_netdrv_send_ip_event(drv, 1);
+            }
+        }
+        else {
+            if (!ip_addr_isany(&netif->ip_addr)) {
+                // 静态IP, 那就发布IP_READY事件
+                luat_netdrv_send_ip_event(drv, 1);
+            }
+        }
+        return;
+    }
+    if (updown == 0 && netif_is_link_up(netif)) {
+        LLOGD("网卡(%d)设置为DOWN", drv->id);
+        luat_netdrv_netif_set_link_down(netif);
+        if (ulwip && ulwip->dhcp_enable) {
+            ulwip_dhcp_client_stop(ulwip);
+        }
+        net_lwip2_set_link_state(drv->id, 0);
+        luat_netdrv_send_ip_event(drv, 0);
+    }
+
+    #if 0
+    network_adapter_info* info = network_adapter_fetch(drv->id, &userdata);
+    if (info == NULL || info->check_ready == NULL) {
+        // LLOGI("网络适配器(%d)不存在, 或者没有check_ready函数", adapter_index);
+        return;
+    }
+    int ready = info->check_ready(userdata);
+    net_lwip2_set_link_state(drv->id, ready);
+    luat_netdrv_send_ip_event(drv, ready);
+    #endif
+}
+
+void luat_netdrv_set_link_updown(luat_netdrv_t* drv, uint8_t updown) {
+    if (drv == NULL || drv->netif == NULL) {
+        return;
+    }
+    tmpptr_t ptr = {
+        .drv = drv,
+        .updown = updown
+    };
+    tcpip_callback_with_block(link_updown, &ptr, 1);
+}

+ 22 - 119
components/network/netdrv/src/luat_netdrv_whale.c

@@ -14,6 +14,7 @@
 #include "luat_airlink.h"
 #include "luat_mem.h"
 #include "luat_netdrv_whale.h"
+#include "luat_netdrv_event.h"
 #include "luat_ulwip.h"
 
 #define LUAT_LOG_TAG "netdrv.whale"
@@ -32,7 +33,6 @@ extern luat_airlink_dev_info_t g_airlink_self_dev_info;
 static err_t luat_netif_init(struct netif *netif);
 static err_t netif_output(struct netif *netif, struct pbuf *p);
 static int netif_ip_event_cb(lua_State *L, void* ptr);
-static int whale_dhcp(luat_netdrv_t* drv, void* userdata, int enable);
 
 void luat_netdrv_whale_dataout(luat_netdrv_t* drv, void* userdata, uint8_t* buff, uint16_t len) {
     // TODO 发送到spi slave task
@@ -87,8 +87,8 @@ void luat_netdrv_whale_boot(luat_netdrv_t* drv, void* userdata) {
         memset(netdrv->netif, 0, sizeof(struct netif));
     }
     luat_netdrv_whale_t* cfg = (luat_netdrv_whale_t*)userdata;
-    cfg->ulwip.netif = netdrv->netif;
-    cfg->ulwip.adapter_index = cfg->id;
+    drv->ulwip->netif = netdrv->netif;
+    drv->ulwip->adapter_index = cfg->id;
 
     netif_add(netdrv->netif, IP4_ADDR_ANY, IP4_ADDR_ANY, IP4_ADDR_ANY, netdrv, luat_netif_init, luat_netdrv_netif_input_main);
 
@@ -97,20 +97,15 @@ void luat_netdrv_whale_boot(luat_netdrv_t* drv, void* userdata) {
         // 默认是down的就行
     }
     else if (netdrv->id == NW_ADAPTER_INDEX_LWIP_GP_GW) {
-        // 这里要分情况, 如果本身带4G模块, 那么就up, 否则就是down
-        // 通过devinfo等途径, 通知对端netif的开启与关闭
-        #if defined(LUAT_USE_MOBILE) && !defined(LUAT_USE_DRV_MOBILE)
-        netif_set_up(netdrv->netif);
-        #endif
     }
     else {
-        netif_set_up(netdrv->netif);
+        // 其他的设备, 直接设置成up和link up
+        netif_set_link_up(netdrv->netif);
     }
     if (netdrv->id == NW_ADAPTER_INDEX_LWIP_WIFI_STA) {
-        cfg->dhcp = 1;
-        cfg->ulwip.dhcp_enable = 1;
+        drv->ulwip->dhcp_enable = 1;
     }
-    netif_set_link_up(netdrv->netif);
+    netif_set_up(netdrv->netif);
     net_lwip2_set_netif(netdrv->id, netdrv->netif);
     net_lwip2_register_adapter(netdrv->id);
     // LLOGD("luat_netdrv_whale_boot 执行完成");
@@ -157,109 +152,34 @@ static err_t luat_netif_init(struct netif *netif) {
     return 0;
 }
 
-static int netif_ip_event_cb(lua_State *L, void* ptr) {
-    rtos_msg_t* msg = (rtos_msg_t*)lua_topointer(L, -1);
-    char buff[32] = {0};
-    luat_netdrv_t* netdrv = luat_netdrv_get(msg->arg1);
-    if (netdrv == NULL) {
-        return 0;
-    }
-    lua_getglobal(L, "sys_pub");
-    if (lua_isfunction(L, -1)) {
-        if (msg->arg2 == 0) {
-            LLOGD("IP_LOSE %d", netdrv->id);
-            lua_pushstring(L, "IP_LOSE");
-            lua_pushinteger(L, netdrv->id);
-            lua_call(L, 2, 0);
-        }
-        else {
-            ipaddr_ntoa_r(&netdrv->netif->ip_addr, buff,  32);
-            LLOGD("IP_READY %d %s", netdrv->id, buff);
-            lua_pushstring(L, "IP_READY");
-            lua_pushstring(L, buff);
-            lua_pushinteger(L, netdrv->id);
-            lua_call(L, 3, 0);
-        }
-    }
-    return 0;
-}
-
-typedef struct tmpptr {
-    luat_netdrv_t* drv;
-    uint8_t updown;
-}tmpptr_t;
-
-static void _luat_netdrv_whale_ipevent(tmpptr_t* ptr) {
-    luat_netdrv_t* drv = ptr->drv;
-    uint8_t updown = ptr->updown;
-    rtos_msg_t msg = {0};
-    void* userdata = NULL;
-    luat_netdrv_whale_t* cfg = (luat_netdrv_whale_t*)drv->userdata;
-    if (updown) {
-        netif_set_up(drv->netif);
-        if (cfg->ulwip.netif == NULL) {
-            cfg->ulwip.netif = drv->netif;
-        }
-        if (cfg->dhcp) {
-            // LLOGD("dhcp启动 %p", cfg->ulwip.netif);
-            ip_addr_set_ip4_u32(&cfg->ulwip.netif->ip_addr, 0);
-            ip_addr_set_ip4_u32(&cfg->ulwip.netif->gw, 0);
-            ulwip_dhcp_client_start(&cfg->ulwip);
-        }
-    }
-    else {
-        luat_netdrv_netif_set_down(drv->netif);
-        if (cfg->dhcp) {
-            // LLOGD("dhcp停止");
-            ip_addr_set_ip4_u32(&cfg->ulwip.netif->ip_addr, 0);
-            ip_addr_set_ip4_u32(&cfg->ulwip.netif->gw, 0);
-            ulwip_dhcp_client_stop(&cfg->ulwip);
-        }
-    }
-	network_adapter_info* info = network_adapter_fetch(drv->id, &userdata);
-    if (info == NULL || info->check_ready == NULL) {
-        // LLOGI("网络适配器(%d)不存在, 或者没有check_ready函数", adapter_index);
-        return;
-    }
-    int ready = info->check_ready(userdata);
-    net_lwip2_set_link_state(drv->id, ready);
-    msg.arg1 = drv->id;
-    msg.arg2 = ready;
-    msg.ptr = NULL;
-    msg.handler = netif_ip_event_cb;
-    luat_msgbus_put(&msg, 0);
-}
-
-void luat_netdrv_whale_ipevent(luat_netdrv_t* drv, uint8_t updown) {
-    if (drv == NULL || drv->netif == NULL) {
-        return;
-    }
-    tmpptr_t ptr = {
-        .drv = drv,
-        .updown = updown,
-    };
-    tcpip_callback_with_block(_luat_netdrv_whale_ipevent, &ptr, 1);
-}
-
-
 luat_netdrv_t* luat_netdrv_whale_create(luat_netdrv_whale_t* tmp) {
     // LLOGD("创建Whale设备");
     luat_netdrv_t* netdrv = luat_heap_malloc(sizeof(luat_netdrv_t));
-    if (netdrv == NULL) {
+    ulwip_ctx_t* ulwip = luat_heap_malloc(sizeof(ulwip_ctx_t));
+    luat_netdrv_whale_t* cfg = luat_heap_malloc(sizeof(luat_netdrv_whale_t));
+    if (netdrv == NULL || ulwip == NULL || cfg == NULL) {
+        if (netdrv)
+            luat_heap_free(netdrv);
+        if (ulwip)
+            luat_heap_free(ulwip);
+        if (cfg)
+            luat_heap_free(cfg);
         return NULL;
     }
+    memset(ulwip, 0, sizeof(ulwip_ctx_t));
+    memset(netdrv, 0, sizeof(luat_netdrv_t));
     // 把配置信息拷贝一份
-    luat_netdrv_whale_t* cfg = luat_heap_malloc(sizeof(luat_netdrv_whale_t));
     memcpy(cfg, tmp, sizeof(luat_netdrv_whale_t));
 
     // 初始化netdrv
-    memset(netdrv, 0, sizeof(luat_netdrv_t));
     netdrv->id = cfg->id;
     netdrv->netif = NULL;
     netdrv->dataout = luat_netdrv_whale_dataout;
     netdrv->boot = luat_netdrv_whale_boot;
     netdrv->userdata = cfg;
-    netdrv->dhcp = whale_dhcp;
+    netdrv->dhcp = luat_netdrv_dhcp_opt;
+    netdrv->ulwip = ulwip;
+    ulwip->adapter_index = netdrv->id;
     return netdrv;
 }
 
@@ -291,21 +211,4 @@ luat_netdrv_t*  luat_netdrv_whale_setup(luat_netdrv_conf_t* conf) {
     return drv;
 }
 
-static int whale_dhcp(luat_netdrv_t* drv, void* userdata, int enable) {
-    luat_netdrv_whale_t* cfg = (luat_netdrv_whale_t*)userdata;
-    if (cfg->dhcp == enable) {
-        return 0;
-    }
-    cfg->dhcp = (uint8_t)enable;
-    cfg->ulwip.dhcp_enable = enable;
-    if (cfg->ulwip.netif == NULL) {
-        return 0;
-    }
-    if (enable) {
-        ulwip_dhcp_client_start(&cfg->ulwip);
-    }
-    else {
-        ulwip_dhcp_client_stop(&cfg->ulwip);
-    }
-    return 0;
-}
+

+ 32 - 10
components/network/ulwip/src/ulwip_dhcp_client.c

@@ -3,6 +3,11 @@
 #include "luat_ulwip.h"
 #include "luat_crypto.h"
 
+#ifdef LUAT_USE_NETDRV
+#include "luat_netdrv.h"
+#include "luat_netdrv_event.h"
+#endif
+
 #define LUAT_LOG_TAG "ulwip"
 #include "luat_log.h"
 
@@ -67,15 +72,27 @@ on_check:
         }
 
         // 设置到netif
-        ip_addr_set_ip4_u32(&netif->ip_addr, dhcp->ip);
-        ip_addr_set_ip4_u32(&netif->netmask, dhcp->submask);
-        ip_addr_set_ip4_u32(&netif->gw,      dhcp->gateway);
+        ip4_addr_t ipaddr = {.addr=dhcp->ip};
+        ip4_addr_t netmask = {.addr=dhcp->submask};
+        ip4_addr_t gw = {.addr=dhcp->gateway};
+        netif_set_addr(netif, &ipaddr, &netmask, &gw);
         dhcp->state = DHCP_STATE_WAIT_LEASE_P1;
         if (rxbuff) {
             luat_heap_free(rxbuff);
             rxbuff = NULL;
         }
+        #ifndef LUAT_USE_NETDRV
         ulwip_netif_ip_event(ctx);
+        #else
+        luat_netdrv_t* drv = luat_netdrv_get(adapter_index);
+        if (drv == NULL || drv->netif == NULL) {
+            LLOGE("adapter %d netdrv not found", adapter_index);
+        }
+        else {
+            net_lwip2_set_link_state(adapter_index, 1);
+            luat_netdrv_send_ip_event(drv, 1);
+        }
+        #endif
         luat_rtos_timer_stop(ctx->dhcp_timer);
         luat_rtos_timer_start(ctx->dhcp_timer, 60000, 1, dhcp_client_timer_cb, ctx);
         if (ctx->event_cb) {
@@ -228,7 +245,7 @@ static void reset_dhcp_client(ulwip_ctx_t *ctx) {
 }
 
 void ulwip_dhcp_client_start(ulwip_ctx_t *ctx) {
-    // LLOGD("dhcp start netif %p", ctx->netif);
+    LLOGD("adapter %d dhcp start netif %p", ctx->adapter_index, ctx->netif);
     if (ctx->netif == NULL) {
         LLOGE("ctx->netif is NULL!!!!");
         return;
@@ -244,12 +261,16 @@ void ulwip_dhcp_client_start(ulwip_ctx_t *ctx) {
     if (!ctx->dhcp_client) {
         ctx->dhcp_client = luat_heap_malloc(sizeof(dhcp_client_info_t));
         reset_dhcp_client(ctx);
+        net_lwip2_set_dhcp_client(ctx->adapter_index, ctx->dhcp_client);
         luat_rtos_timer_create(&ctx->dhcp_timer);
         s_ctxs[ctx->adapter_index] = ctx; // 保存到全局数组中
     }
-    ip_addr_set_any(0, &ctx->netif->ip_addr);
-    ip_addr_set_any(0, &ctx->netif->ip_addr);
-    ip_addr_set_any(0, &ctx->netif->netmask);
+    ip4_addr_t ipaddr = {0};
+    ip4_addr_t netmask = {0};
+    ip4_addr_t gw = {0};
+    if (ctx->netif) {
+        netif_set_addr(ctx->netif, &ipaddr, &netmask, &gw);
+    }
     ctx->dhcp_client->state = DHCP_STATE_DISCOVER;
     ctx->dhcp_client->discover_cnt = 0;
     if (!luat_rtos_timer_is_active(ctx->dhcp_timer))
@@ -271,9 +292,10 @@ void ulwip_dhcp_client_stop(ulwip_ctx_t *ctx) {
             reset_dhcp_client(ctx);
         }
         if (ctx->netif) {
-            ip_addr_set_any(0, &ctx->netif->ip_addr);
-            ip_addr_set_any(0, &ctx->netif->ip_addr);
-            ip_addr_set_any(0, &ctx->netif->netmask);
+            ip4_addr_t ipaddr = {0};
+            ip4_addr_t netmask = {0};
+            ip4_addr_t gw = {0};
+            netif_set_addr(ctx->netif, &ipaddr, &netmask, &gw);
         }
     }
 }

+ 8 - 0
components/network/websocket/luat_lib_websocket.c

@@ -50,6 +50,11 @@ static const char *error_string[WEBSOCKET_MSG_ERROR_MAX - WEBSOCKET_MSG_ERROR_CO
 		"other"
 };
 
+#ifdef LUAT_USE_NETDRV
+#include "luat_netdrv.h"
+#include "luat_netdrv_event.h"
+#endif
+
 static luat_websocket_ctrl_t *get_websocket_ctrl(lua_State *L)
 {
 	if (luaL_testudata(L, 1, LUAT_WEBSOCKET_CTRL_TYPE))
@@ -174,6 +179,9 @@ int l_websocket_callback(lua_State *L, void *ptr)
 				lua_call(L, 4, 0);
 			}
 		}
+		#ifdef LUAT_USE_NETDRV
+		luat_netdrv_fire_socket_event_netctrl(EV_NW_SOCKET_ERROR, websocket_ctrl->netc, 5);
+		#endif
 		break;
 	}
 	default:

+ 9 - 2
components/network/websocket/luat_websocket.c

@@ -29,6 +29,10 @@ static void print_pkg(const char *tag, char *buff, luat_websocket_pkg_t *pkg)
 #define print_pkg(...)
 #endif
 
+#ifdef LUAT_USE_NETDRV
+extern void luat_netdrv_fire_socket_event_netctrl(uint32_t event_id, network_ctrl_t* ctrl, uint8_t proto);
+#endif
+
 static int32_t luat_websocket_callback(void *data, void *param);
 
 #ifdef __LUATOS__
@@ -110,7 +114,7 @@ int luat_websocket_send_packet(void *socket_info, const void *buf, unsigned int
 	int ret = network_tx(websocket_ctrl->netc, buf, count, 0, NULL, 0, &tx_len, 0);
 	if (ret < 0)
 	{
-		LLOGI("network_tx %d , close socket", ret);
+		LLOGI("send pkg err %d , close socket", ret);
 		luat_websocket_msg_cb(websocket_ctrl, WEBSOCKET_MSG_ERROR_TX, 0);
 		luat_websocket_close_socket(websocket_ctrl);
 		return 0;
@@ -588,7 +592,7 @@ int luat_websocket_read_packet(luat_websocket_ctrl_t *websocket_ctrl)
 		}
 	}
 	if (WEBSOCKET_RECV_BUF_LEN_MAX + 8 < websocket_ctrl->buffer_offset) {
-		LLOGD("pkg maybe too large");
+		LLOGD("pkg maybe too large %d", websocket_ctrl->buffer_offset);
 		return -1;
 	}
 	return 0;
@@ -648,6 +652,7 @@ static int32_t luat_websocket_callback(void *data, void *param)
 	if (event->Param1)
 	{
 		LLOGW("websocket_callback param1 %d, closing socket", event->Param1);
+		luat_websocket_msg_cb(websocket_ctrl, WEBSOCKET_MSG_ERROR_CONN, 0);
 		luat_websocket_close_socket(websocket_ctrl);
 		return 0;
 	}
@@ -655,6 +660,7 @@ static int32_t luat_websocket_callback(void *data, void *param)
 	if (ret < 0)
 	{
 		LLOGW("network_wait_event ret %d, closing socket", ret);
+		luat_websocket_msg_cb(websocket_ctrl, WEBSOCKET_MSG_ERROR_CONN, 0);
 		luat_websocket_close_socket(websocket_ctrl);
 		return -1;
 	}
@@ -672,6 +678,7 @@ int luat_websocket_connect(luat_websocket_ctrl_t *websocket_ctrl)
 	LLOGD("network_connect ret %d", ret);
 	if (ret < 0)
 	{
+		luat_websocket_msg_cb(websocket_ctrl, WEBSOCKET_MSG_ERROR_CONN, 0);
 		network_close(websocket_ctrl->netc, 0);
 		return -1;
 	}

+ 2 - 2
components/sms/binding/luat_lib_sms.c

@@ -286,7 +286,7 @@ void luat_sms_send_cb(int ret)
         }
         return;
     }
-    LLOGE("long sms callback seqNum = %d", g_s_sms_pdu_packet.seqNum);
+    LLOGI("long sms callback seqNum = %d", g_s_sms_pdu_packet.seqNum);
     // 全部短信发送完成
     if (g_s_sms_pdu_packet.seqNum == g_s_sms_pdu_packet.maxNum) {
         if (long_sms_send_idp) {
@@ -421,7 +421,7 @@ static int l_sms_send(lua_State *L) {
     g_s_sms_send.payload_len = outlen;
 
     int len = luat_sms_pdu_packet(&g_s_sms_pdu_packet);
-    LLOGW("pdu len %d", len);
+    LLOGD("pdu len %d", len);
     ret = luat_sms_send_msg_v2(g_s_sms_pdu_packet.pdu_buf, len);
     if (!ret) {
         lua_pushboolean(L, ret == 0);

+ 1 - 0
components/tp/luat_lib_tp.c

@@ -85,6 +85,7 @@ static int l_tp_handler(lua_State* L, void* ptr) {
             lua_call(L, 2, 0);
         }
     }
+    luat_tp_config->opts->read_done(luat_tp_config);
     return 0;
 }
 

+ 4 - 3
components/tp/luat_tp.c

@@ -44,16 +44,17 @@ void luat_tp_task_entry(void* param){
         //     last_y = tp_data->y_coordinate;
         // }
         
-        if (luat_tp_config->callback){
+        if (luat_tp_config->callback == NULL){
+            luat_tp_config->opts->read_done(luat_tp_config);
+        }else{
             luat_tp_config->callback(luat_tp_config,tp_data);
         }
-        luat_tp_config->opts->read_done(luat_tp_config);
     }
 }
 
 int luat_tp_init(luat_tp_config_t* luat_tp_config){
     if (g_s_tp_task_handle == NULL){
-        int ret = luat_rtos_task_create(&g_s_tp_task_handle, 4096, 10, "tp", luat_tp_task_entry, NULL, 32);
+        int ret = luat_rtos_task_create(&g_s_tp_task_handle, 4096, 80, "tp", luat_tp_task_entry, NULL, 32);
         if (ret){
             g_s_tp_task_handle = NULL;
             LLOGE("tp task create failed!");

+ 15 - 12
components/tp/luat_tp_cst9220.c

@@ -36,6 +36,9 @@
 #define U8TO32(x1,x2,x3,x4) ((((x1)&0xFF)<<24)|(((x2)&0xFF)<<16)|(((x3)&0xFF)<<8)|((x4)&0xFF))
 #define U16REV(x)  ((((x)<<8)&0xFF00)|(((x)>>8)&0x00FF))
 
+#define DISABLE (0)
+#define ENABLE  (1)
+
 enum work_mode{
     NOMAL_MODE = 0,
     GESTURE_MODE = 1,
@@ -212,7 +215,7 @@ static int tp_cst92xx_updata_tpinfo(luat_tp_config_t* luat_tp_config){
     uint8_t buf[30] = {0};
     struct tp_info *ic = &hyn_92xxdata.hw_info;
 
-    tp_cst92xx_set_workmode(luat_tp_config, 0xff,0);
+    tp_cst92xx_set_workmode(luat_tp_config, 0xff,DISABLE);
     if(hyn_wr_reg(luat_tp_config,0xD101,2,buf,0)){
         return -1;
     }
@@ -241,7 +244,7 @@ static int tp_cst92xx_updata_tpinfo(luat_tp_config_t* luat_tp_config){
 
     LLOGD("IC_info project_id:%04x ictype:%04x fw_ver:%x checksum:%#x",ic->fw_project_id,ic->fw_chip_type,ic->fw_ver,ic->ic_fw_checksum);
 
-    tp_cst92xx_set_workmode(luat_tp_config,NOMAL_MODE,1);
+    tp_cst92xx_set_workmode(luat_tp_config,NOMAL_MODE,ENABLE);
     return 0;
 }
 
@@ -449,17 +452,8 @@ static int tp_cst92xx_gpio_init(luat_tp_config_t* luat_tp_config){
     luat_gpio_mode(luat_tp_config->pin_int, Luat_GPIO_OUTPUT, Luat_GPIO_DEFAULT, Luat_GPIO_HIGH);
     luat_gpio_set(luat_tp_config->pin_rst, Luat_GPIO_HIGH);
     luat_gpio_set(luat_tp_config->pin_int, Luat_GPIO_HIGH);
-    luat_rtos_task_sleep(10);
-    return 0;
-}
-
-static int tp_cst92xx_init(luat_tp_config_t* luat_tp_config){
-    int ret = 0;
-    luat_rtos_task_sleep(100);
-    tp_cst92xx_gpio_init(luat_tp_config);
 
     luat_tp_config->int_type = Luat_GPIO_FALLING;
-
     luat_gpio_t gpio = {0};
     gpio.pin = luat_tp_config->pin_int;
     gpio.mode = Luat_GPIO_IRQ;
@@ -468,7 +462,16 @@ static int tp_cst92xx_init(luat_tp_config_t* luat_tp_config){
     gpio.irq_cb = luat_tp_irq_cb;
     gpio.irq_args = luat_tp_config;
     luat_gpio_setup(&gpio);
+
+    luat_rtos_task_sleep(10);
+    return 0;
+}
+
+static int tp_cst92xx_init(luat_tp_config_t* luat_tp_config){
+    int ret = 0;
+    luat_rtos_task_sleep(50);
     luat_tp_config->address = CST92XX_ADDRESS;
+    tp_cst92xx_gpio_init(luat_tp_config);
     tp_cst92xx_hw_reset(luat_tp_config);
     luat_rtos_task_sleep(40);
 
@@ -500,7 +503,7 @@ static int tp_cst92xx_init(luat_tp_config_t* luat_tp_config){
         return ret;
     }
     ret |= tp_cst92xx_set_workmode(luat_tp_config, NOMAL_MODE,0);
-    luat_rtos_task_sleep(20);
+    luat_rtos_task_sleep(10);
     cst92xx_init_state = 1;
     return ret;
 }

+ 4 - 2
components/u8g2/u8g2.h

@@ -59,7 +59,7 @@
 #define U8G2_H
 
 #include "u8x8.h"
-
+#include "stdio.h"
 /*
   The following macro enables 16 Bit mode. 
   Without defining this macro all calculations are done with 8 Bit (1 Byte) variables.
@@ -382,7 +382,9 @@ struct u8g2_struct
 					
 	// the following variable should be renamed to is_buffer_auto_clear
   uint8_t is_auto_page_clear; 		/* set to 0 to disable automatic clear of the buffer in firstPage() and nextPage() */
-  
+#if (defined __LUATOS__) || defined (__USER_CODE__)
+  FILE* font_file;
+#endif
 };
 
 #define u8g2_GetU8x8(u8g2) ((u8x8_t *)(u8g2))

+ 87 - 2
components/u8g2/u8g2_font.c

@@ -785,6 +785,10 @@ int8_t u8g2_font_2x_decode_glyph(u8g2_t *u8g2, const uint8_t *glyph_data)
   return d*2;
 }
 
+#if (defined __LUATOS__) || defined (__USER_CODE__)
+#include "luat_fs.h"
+#include "luat_mem.h"
+#endif
 /*
   Description:
     Find the starting point of the glyph data.
@@ -796,6 +800,77 @@ int8_t u8g2_font_2x_decode_glyph(u8g2_t *u8g2, const uint8_t *glyph_data)
 const uint8_t *u8g2_font_get_glyph_data(u8g2_t *u8g2, uint16_t encoding)
 {
   const uint8_t *font = u8g2->font;
+    uint8_t font_data[4] = {0};
+    // luat_fs_fread(font_info, 1, 21, u8g2->font_file);
+#if (defined __LUATOS__) || defined (__USER_CODE__)
+    if (u8g2->font_file){
+        luat_fs_fseek(u8g2->font_file, U8G2_FONT_DATA_STRUCT_SIZE, SEEK_SET);
+        if ( encoding <= 255 ){
+            if ( encoding >= 'a' ){
+                luat_fs_fseek(u8g2->font_file, u8g2->font_info.start_pos_lower_a, SEEK_CUR);
+            } else if ( encoding >= 'A' ){
+                luat_fs_fseek(u8g2->font_file, u8g2->font_info.start_pos_upper_A, SEEK_CUR);
+            }
+            for(;;){
+                font_data[1] = 0;
+                luat_fs_fread(font_data, 1, 2, u8g2->font_file);
+                if (font_data[1] == 0) break;
+                if (font_data[0] == encoding ){
+                    if(u8g2->font){
+                        u8g2->font = luat_heap_realloc(u8g2->font, font_data[1] - 2);
+                    }else{
+                        u8g2->font = luat_heap_malloc(font_data[1] - 2);
+                    }
+                    luat_fs_fread(u8g2->font, 1, font_data[1] - 2, u8g2->font_file);
+                    return u8g2->font;
+                }
+                luat_fs_fseek(u8g2->font_file, font_data[1] - 2, SEEK_CUR);
+            }
+        }
+        #ifdef U8G2_WITH_UNICODE
+            else{
+                uint16_t e;
+                const uint8_t *unicode_lookup_table;
+                // font += u8g2->font_info.start_pos_unicode;
+                luat_fs_fseek(u8g2->font_file, u8g2->font_info.start_pos_unicode, SEEK_CUR);
+                unicode_lookup_table = font; 
+                /* issue 596: search for the glyph start in the unicode lookup table */
+                // do{
+                //     font += u8g2_font_get_word(unicode_lookup_table, 0);
+                //     e = u8g2_font_get_word(unicode_lookup_table, 2);
+                //     unicode_lookup_table+=4;
+                // } while( e < encoding );
+                for(;;){
+                    // e = u8x8_pgm_read( font );
+                    // e <<= 8;
+                    // e |= u8x8_pgm_read( font + 1 );
+                    // if ( e == 0 )
+                    //     break;
+                    // if ( e == encoding ){
+                    //     return font+3;	/* skip encoding and glyph size */
+                    // }
+                    // font += u8x8_pgm_read( font + 2 );
+
+                    luat_fs_fread(font_data, 1, 3, u8g2->font_file);
+                    e = font_data[0]<<8 | font_data[1];
+                    if ( e == 0 )
+                        break;
+                    if ( e == encoding ){
+                        if(u8g2->font){
+                            u8g2->font = luat_heap_realloc(u8g2->font, font_data[2] - 3);
+                        }else{
+                            u8g2->font = luat_heap_malloc(font_data[2] - 3);
+                        }
+                        luat_fs_fread(u8g2->font, 1, font_data[2] - 3, u8g2->font_file);
+                        return u8g2->font;
+                    }
+                    luat_fs_fseek(u8g2->font_file, font_data[2] - 3, SEEK_CUR);
+                }  
+            }
+        #endif
+        return NULL;
+    }
+#endif
   font += U8G2_FONT_DATA_STRUCT_SIZE;
 
   
@@ -1294,14 +1369,24 @@ void u8g2_SetFontPosCenter(u8g2_t *u8g2)
 
 void u8g2_SetFont(u8g2_t *u8g2, const uint8_t  *font)
 {
-  if ( u8g2->font != font )
+  if ( u8g2->font != font || font == NULL)
   {
 //#ifdef  __unix__
 //	u8g2->last_font_data = NULL;
 //	u8g2->last_unicode = 0x0ffff;
 //#endif 
     u8g2->font = font;
-    u8g2_read_font_info(&(u8g2->font_info), font);
+#if (defined __LUATOS__) || defined (__USER_CODE__)
+    if(font == NULL){
+        uint8_t font_info[U8G2_FONT_DATA_STRUCT_SIZE] = {0};
+        luat_fs_fseek(u8g2->font_file, 0, SEEK_SET);
+        luat_fs_fread(font_info, 1, U8G2_FONT_DATA_STRUCT_SIZE, u8g2->font_file);
+        u8g2_read_font_info(&(u8g2->font_info), font_info);
+    }else
+#endif
+    {
+        u8g2_read_font_info(&(u8g2->font_info), font);
+    }
     u8g2_UpdateRefHeight(u8g2);
     /* u8g2_SetFontPosBaseline(u8g2); */ /* removed with issue 195 */
 #if (defined __LUATOS__) || defined (__USER_CODE__)

+ 1839 - 0
luat/demo/airui/demo_2.json

@@ -0,0 +1,1839 @@
+{
+    "fonts": [
+    ],
+    "images": [
+    ],
+    "pages": [
+        {
+            "children": [
+                {
+                    "auto_size": true,
+                    "class": "lv_img",
+                    "click": false,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 80,
+                        "width": 80,
+                        "x": 393,
+                        "y": 7
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "name": "image_1",
+                    "offset": {
+                        "x": 0,
+                        "y": 0
+                    },
+                    "source": "default",
+                    "styles": [
+                        {
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "part": 0,
+                            "state": 6
+                        }
+                    ]
+                },
+                {
+                    "auto_size": true,
+                    "class": "lv_line",
+                    "click": false,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 81,
+                        "width": 81,
+                        "x": 392,
+                        "y": 6
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "name": "line_1",
+                    "points": [
+                        {
+                            "x": 1,
+                            "y": 1
+                        },
+                        {
+                            "x": 1,
+                            "y": 80
+                        },
+                        {
+                            "x": 80,
+                            "y": 80
+                        },
+                        {
+                            "x": 80,
+                            "y": 1
+                        },
+                        {
+                            "x": 1,
+                            "y": 1
+                        }
+                    ],
+                    "styles": [
+                        {
+                            "Line": {
+                                "line_color": "0x23a5f5",
+                                "line_width": 3
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Line": {
+                                "line_color": "0x23a5f5",
+                                "line_width": 3
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Line": {
+                                "line_color": "0x23a5f5",
+                                "line_width": 3
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Line": {
+                                "line_color": "0x23a5f5",
+                                "line_width": 3
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Line": {
+                                "line_color": "0x23a5f5",
+                                "line_width": 3
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Line": {
+                                "line_color": "0x23a5f5",
+                                "line_width": 3
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Line": {
+                                "line_color": "0x23a5f5",
+                                "line_width": 3
+                            },
+                            "part": 0,
+                            "state": 6
+                        }
+                    ],
+                    "y_invert": false
+                },
+                {
+                    "align": 2,
+                    "class": "lv_label",
+                    "click": false,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 19,
+                        "width": 128,
+                        "x": 261,
+                        "y": 34
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "long_mode": 0,
+                    "name": "label_1",
+                    "recolor": false,
+                    "styles": [
+                        {
+                            "Border": {
+                                "border_color": "0x269be3"
+                            },
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0x269be3"
+                            },
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0x269be3"
+                            },
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0x269be3"
+                            },
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0x269be3"
+                            },
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0x269be3"
+                            },
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0x269be3"
+                            },
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 6
+                        }
+                    ],
+                    "text": "上海合宙通讯科技有限公司"
+                },
+                {
+                    "checkable": false,
+                    "class": "lv_btn",
+                    "click": true,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "fit": 0,
+                    "geometry": {
+                        "height": 35,
+                        "width": 100,
+                        "x": 20,
+                        "y": 11
+                    },
+                    "hidden": false,
+                    "layout": 1,
+                    "locked": false,
+                    "name": "button_1",
+                    "state": 0,
+                    "styles": [
+                        {
+                            "Background": {
+                                "bg_color": "0x006ecf"
+                            },
+                            "Border": {
+                                "border_color": "0xeaeaea",
+                                "border_width": 5
+                            },
+                            "Value": {
+                                "value_color": "0xffffff",
+                                "value_font": "Simsun 16",
+                                "value_str": "Home"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0xeaeaea",
+                                "border_width": 5
+                            },
+                            "Value": {
+                                "value_font": "Simsun 16",
+                                "value_str": "Home"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x006ecf"
+                            },
+                            "Border": {
+                                "border_color": "0xeaeaea",
+                                "border_width": 5
+                            },
+                            "Value": {
+                                "value_color": "0xffffff",
+                                "value_font": "Simsun 16",
+                                "value_str": "Home"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x006ecf"
+                            },
+                            "Border": {
+                                "border_color": "0xeaeaea",
+                                "border_width": 5
+                            },
+                            "Value": {
+                                "value_color": "0xffffff",
+                                "value_font": "Simsun 16",
+                                "value_str": "Home"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x006ecf"
+                            },
+                            "Border": {
+                                "border_color": "0xeaeaea",
+                                "border_width": 5
+                            },
+                            "Value": {
+                                "value_color": "0xffffff",
+                                "value_font": "Simsun 16",
+                                "value_str": "Home"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Border": {
+                                "border_width": 5
+                            },
+                            "Value": {
+                                "value_font": "Simsun 16",
+                                "value_str": "Home"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Border": {
+                                "border_width": 5
+                            },
+                            "Value": {
+                                "value_font": "Simsun 16",
+                                "value_str": "Home"
+                            },
+                            "part": 0,
+                            "state": 6
+                        }
+                    ]
+                },
+                {
+                    "angle": [
+                        0,
+                        108
+                    ],
+                    "bg_angle": [
+                        0,
+                        360
+                    ],
+                    "class": "lv_arc",
+                    "click": true,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 75,
+                        "width": 75,
+                        "x": 30,
+                        "y": 229
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "name": "arc_1",
+                    "rotation": 0,
+                    "styles": [
+                        {
+                            "Border": {
+                                "border_color": "0xffffff"
+                            },
+                            "Line": {
+                                "line_color": "0xd9683b",
+                                "line_width": 8
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0xffffff"
+                            },
+                            "Line": {
+                                "line_color": "0xd9683b",
+                                "line_width": 8
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Line": {
+                                "line_color": "0xd9683b",
+                                "line_width": 8
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Line": {
+                                "line_color": "0xd9683b",
+                                "line_width": 8
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0xffffff"
+                            },
+                            "Line": {
+                                "line_color": "0xd9683b",
+                                "line_width": 8
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0xffffff"
+                            },
+                            "Line": {
+                                "line_color": "0xd9683b",
+                                "line_width": 8
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Border": {
+                                "border_color": "0xffffff"
+                            },
+                            "Line": {
+                                "line_color": "0xd9683b",
+                                "line_width": 8
+                            },
+                            "part": 0,
+                            "state": 6
+                        },
+                        {
+                            "part": 1,
+                            "state": 0
+                        },
+                        {
+                            "part": 1,
+                            "state": 1
+                        },
+                        {
+                            "part": 1,
+                            "state": 2
+                        },
+                        {
+                            "part": 1,
+                            "state": 3
+                        },
+                        {
+                            "part": 1,
+                            "state": 4
+                        },
+                        {
+                            "part": 1,
+                            "state": 5
+                        },
+                        {
+                            "part": 1,
+                            "state": 6
+                        }
+                    ],
+                    "type": 0,
+                    "value": 30
+                },
+                {
+                    "arrow": 0,
+                    "class": "lv_dropdown",
+                    "click": true,
+                    "dir": 0,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 35,
+                        "width": 117,
+                        "x": 362,
+                        "y": 137
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "max_height": 240,
+                    "name": "dropdown_1",
+                    "open": false,
+                    "options": "SPI0\nSPI1\nI2C0\nI2C1",
+                    "styles": [
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 6
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 64,
+                            "state": 0
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 64,
+                            "state": 1
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 64,
+                            "state": 2
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 64,
+                            "state": 3
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 64,
+                            "state": 4
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 64,
+                            "state": 5
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 64,
+                            "state": 6
+                        },
+                        {
+                            "part": 65,
+                            "state": 0
+                        },
+                        {
+                            "part": 65,
+                            "state": 1
+                        },
+                        {
+                            "part": 65,
+                            "state": 2
+                        },
+                        {
+                            "part": 65,
+                            "state": 3
+                        },
+                        {
+                            "part": 65,
+                            "state": 4
+                        },
+                        {
+                            "part": 65,
+                            "state": 5
+                        },
+                        {
+                            "part": 65,
+                            "state": 6
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 66,
+                            "state": 0
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 66,
+                            "state": 1
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 66,
+                            "state": 2
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 66,
+                            "state": 3
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 66,
+                            "state": 4
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 66,
+                            "state": 5
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 66,
+                            "state": 6
+                        }
+                    ]
+                },
+                {
+                    "checked": false,
+                    "class": "lv_checkbox",
+                    "click": true,
+                    "disabled": false,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 23,
+                        "width": 143,
+                        "x": 20,
+                        "y": 68
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "name": "check_box_1",
+                    "styles": [
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 6
+                        }
+                    ],
+                    "text": "Enable/Disable"
+                },
+                {
+                    "class": "lv_switch",
+                    "click": true,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 35,
+                        "width": 70,
+                        "x": 390,
+                        "y": 217
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "name": "switch_1",
+                    "state": false,
+                    "styles": [
+                        {
+                            "Background": {
+                                "bg_color": "0x85c370"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x85c370"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x85c370"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x85c370"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x85c370"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x85c370"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x85c370"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 6
+                        },
+                        {
+                            "part": 1,
+                            "state": 0
+                        },
+                        {
+                            "part": 1,
+                            "state": 1
+                        },
+                        {
+                            "part": 1,
+                            "state": 2
+                        },
+                        {
+                            "part": 1,
+                            "state": 3
+                        },
+                        {
+                            "part": 1,
+                            "state": 4
+                        },
+                        {
+                            "part": 1,
+                            "state": 5
+                        },
+                        {
+                            "part": 1,
+                            "state": 6
+                        },
+                        {
+                            "part": 2,
+                            "state": 0
+                        },
+                        {
+                            "part": 2,
+                            "state": 1
+                        },
+                        {
+                            "part": 2,
+                            "state": 2
+                        },
+                        {
+                            "part": 2,
+                            "state": 3
+                        },
+                        {
+                            "part": 2,
+                            "state": 4
+                        },
+                        {
+                            "part": 2,
+                            "state": 5
+                        },
+                        {
+                            "part": 2,
+                            "state": 6
+                        }
+                    ]
+                },
+                {
+                    "class": "lv_textarea",
+                    "click": true,
+                    "cursor_blink_time": 144,
+                    "cursor_pos": 0,
+                    "drag": false,
+                    "edge_flash": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 87,
+                        "width": 234,
+                        "x": 122,
+                        "y": 231
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "max_length": 0,
+                    "name": "text_area_1",
+                    "one_line_mode": false,
+                    "password_mode": false,
+                    "placeholder": "",
+                    "scroll_propagation": false,
+                    "scrollbar_mode": 2,
+                    "styles": [
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 6
+                        },
+                        {
+                            "part": 1,
+                            "state": 0
+                        },
+                        {
+                            "part": 1,
+                            "state": 1
+                        },
+                        {
+                            "part": 1,
+                            "state": 2
+                        },
+                        {
+                            "part": 1,
+                            "state": 3
+                        },
+                        {
+                            "part": 1,
+                            "state": 4
+                        },
+                        {
+                            "part": 1,
+                            "state": 5
+                        },
+                        {
+                            "part": 1,
+                            "state": 6
+                        },
+                        {
+                            "part": 2,
+                            "state": 0
+                        },
+                        {
+                            "part": 2,
+                            "state": 1
+                        },
+                        {
+                            "part": 2,
+                            "state": 2
+                        },
+                        {
+                            "part": 2,
+                            "state": 3
+                        },
+                        {
+                            "part": 2,
+                            "state": 4
+                        },
+                        {
+                            "part": 2,
+                            "state": 5
+                        },
+                        {
+                            "part": 2,
+                            "state": 6
+                        },
+                        {
+                            "part": 3,
+                            "state": 0
+                        },
+                        {
+                            "part": 3,
+                            "state": 1
+                        },
+                        {
+                            "part": 3,
+                            "state": 2
+                        },
+                        {
+                            "part": 3,
+                            "state": 3
+                        },
+                        {
+                            "part": 3,
+                            "state": 4
+                        },
+                        {
+                            "part": 3,
+                            "state": 5
+                        },
+                        {
+                            "part": 3,
+                            "state": 6
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 0
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 1
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 2
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 3
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 4
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 5
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 6
+                        }
+                    ],
+                    "text": "Text area",
+                    "text_align": 0
+                },
+                {
+                    "brightness": 120,
+                    "class": "lv_led",
+                    "click": true,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 43,
+                        "width": 43,
+                        "x": 427,
+                        "y": 273
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "name": "led_1",
+                    "styles": [
+                        {
+                            "Background": {
+                                "bg_color": "0x7f5358"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x7f5358"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x7f5358"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x7f5358"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x7f5358"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x7f5358"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x7f5358"
+                            },
+                            "part": 0,
+                            "state": 6
+                        }
+                    ],
+                    "switch": 1
+                },
+                {
+                    "animation_state": 0,
+                    "animation_time": 200,
+                    "class": "lv_bar",
+                    "click": false,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 30,
+                        "width": 150,
+                        "x": 20,
+                        "y": 165
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "name": "bar_1",
+                    "range": {
+                        "max": 100,
+                        "min": 0
+                    },
+                    "styles": [
+                        {
+                            "Background": {
+                                "bg_color": "0xe8e8e8"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xe8e8e8"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xe8e8e8"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xe8e8e8"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xe8e8e8"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xe8e8e8"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xe8e8e8"
+                            },
+                            "part": 0,
+                            "state": 6
+                        },
+                        {
+                            "part": 1,
+                            "state": 0
+                        },
+                        {
+                            "part": 1,
+                            "state": 1
+                        },
+                        {
+                            "part": 1,
+                            "state": 2
+                        },
+                        {
+                            "part": 1,
+                            "state": 3
+                        },
+                        {
+                            "part": 1,
+                            "state": 4
+                        },
+                        {
+                            "part": 1,
+                            "state": 5
+                        },
+                        {
+                            "part": 1,
+                            "state": 6
+                        }
+                    ],
+                    "type": 0,
+                    "value": 36
+                },
+                {
+                    "animation_state": 0,
+                    "animation_time": 200,
+                    "class": "lv_slider",
+                    "click": true,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 25,
+                        "width": 150,
+                        "x": 20,
+                        "y": 116
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "name": "slider_1",
+                    "range": {
+                        "max": 100,
+                        "min": 0
+                    },
+                    "styles": [
+                        {
+                            "Background": {
+                                "bg_color": "0x4f9ace"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x2ba5d9"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x2ba5d9"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x2ba5d9"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x2ba5d9"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x2ba5d9"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0x2ba5d9"
+                            },
+                            "Outline": {
+                                "outline_color": "0x1caece"
+                            },
+                            "part": 0,
+                            "state": 6
+                        },
+                        {
+                            "part": 1,
+                            "state": 0
+                        },
+                        {
+                            "part": 1,
+                            "state": 1
+                        },
+                        {
+                            "part": 1,
+                            "state": 2
+                        },
+                        {
+                            "part": 1,
+                            "state": 3
+                        },
+                        {
+                            "part": 1,
+                            "state": 4
+                        },
+                        {
+                            "part": 1,
+                            "state": 5
+                        },
+                        {
+                            "part": 1,
+                            "state": 6
+                        },
+                        {
+                            "part": 2,
+                            "state": 0
+                        },
+                        {
+                            "part": 2,
+                            "state": 1
+                        },
+                        {
+                            "part": 2,
+                            "state": 2
+                        },
+                        {
+                            "part": 2,
+                            "state": 3
+                        },
+                        {
+                            "part": 2,
+                            "state": 4
+                        },
+                        {
+                            "part": 2,
+                            "state": 5
+                        },
+                        {
+                            "part": 2,
+                            "state": 6
+                        }
+                    ],
+                    "type": 0,
+                    "value": 70
+                },
+                {
+                    "brightness": 255,
+                    "class": "lv_led",
+                    "click": true,
+                    "drag": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 43,
+                        "width": 43,
+                        "x": 370,
+                        "y": 272
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "name": "led_3",
+                    "styles": [
+                        {
+                            "Background": {
+                                "bg_color": "0xb0ffaf"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xb0ffaf"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xb0ffaf"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xb0ffaf"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xb0ffaf"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xb0ffaf"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Background": {
+                                "bg_color": "0xb0ffaf"
+                            },
+                            "part": 0,
+                            "state": 6
+                        }
+                    ],
+                    "switch": 0
+                },
+                {
+                    "class": "lv_textarea",
+                    "click": true,
+                    "cursor_blink_time": 144,
+                    "cursor_pos": 38,
+                    "drag": false,
+                    "edge_flash": false,
+                    "events": [
+                    ],
+                    "geometry": {
+                        "height": 71,
+                        "width": 137,
+                        "x": 197,
+                        "y": 143
+                    },
+                    "hidden": false,
+                    "locked": false,
+                    "max_length": 0,
+                    "name": "text_area_2",
+                    "one_line_mode": false,
+                    "password_mode": false,
+                    "placeholder": "",
+                    "scroll_propagation": false,
+                    "scrollbar_mode": 2,
+                    "styles": [
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 0,
+                            "state": 6
+                        },
+                        {
+                            "part": 1,
+                            "state": 0
+                        },
+                        {
+                            "part": 1,
+                            "state": 1
+                        },
+                        {
+                            "part": 1,
+                            "state": 2
+                        },
+                        {
+                            "part": 1,
+                            "state": 3
+                        },
+                        {
+                            "part": 1,
+                            "state": 4
+                        },
+                        {
+                            "part": 1,
+                            "state": 5
+                        },
+                        {
+                            "part": 1,
+                            "state": 6
+                        },
+                        {
+                            "part": 2,
+                            "state": 0
+                        },
+                        {
+                            "part": 2,
+                            "state": 1
+                        },
+                        {
+                            "part": 2,
+                            "state": 2
+                        },
+                        {
+                            "part": 2,
+                            "state": 3
+                        },
+                        {
+                            "part": 2,
+                            "state": 4
+                        },
+                        {
+                            "part": 2,
+                            "state": 5
+                        },
+                        {
+                            "part": 2,
+                            "state": 6
+                        },
+                        {
+                            "part": 3,
+                            "state": 0
+                        },
+                        {
+                            "part": 3,
+                            "state": 1
+                        },
+                        {
+                            "part": 3,
+                            "state": 2
+                        },
+                        {
+                            "part": 3,
+                            "state": 3
+                        },
+                        {
+                            "part": 3,
+                            "state": 4
+                        },
+                        {
+                            "part": 3,
+                            "state": 5
+                        },
+                        {
+                            "part": 3,
+                            "state": 6
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 0
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 1
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 2
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 3
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 4
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 5
+                        },
+                        {
+                            "Text": {
+                                "text_font": "Simsun 16"
+                            },
+                            "part": 4,
+                            "state": 6
+                        }
+                    ],
+                    "text": "姓名: {{person.name}}\n年龄: {{person.age}}",
+                    "text_align": 1
+                },
+                {
+                    "checkable": false,
+                    "class": "lv_btn",
+                    "click": true,
+                    "drag": false,
+                    "events": [
+                        "btn_event1#Clicked#bind_script#update_age"
+                    ],
+                    "fit": 0,
+                    "geometry": {
+                        "height": 35,
+                        "width": 133,
+                        "x": 199,
+                        "y": 93
+                    },
+                    "hidden": false,
+                    "layout": 1,
+                    "locked": false,
+                    "name": "button_2",
+                    "state": 0,
+                    "styles": [
+                        {
+                            "Value": {
+                                "value_font": "Simsun 16",
+                                "value_str": "点我年龄+1"
+                            },
+                            "part": 0,
+                            "state": 0
+                        },
+                        {
+                            "Value": {
+                                "value_font": "Simsun 16",
+                                "value_str": "点我年龄+1"
+                            },
+                            "part": 0,
+                            "state": 1
+                        },
+                        {
+                            "Value": {
+                                "value_font": "Simsun 16",
+                                "value_str": "点我年龄+1"
+                            },
+                            "part": 0,
+                            "state": 2
+                        },
+                        {
+                            "Value": {
+                                "value_font": "Simsun 16",
+                                "value_str": "点我年龄+1"
+                            },
+                            "part": 0,
+                            "state": 3
+                        },
+                        {
+                            "Value": {
+                                "value_font": "Simsun 16",
+                                "value_str": "点我年龄+1"
+                            },
+                            "part": 0,
+                            "state": 4
+                        },
+                        {
+                            "Value": {
+                                "value_font": "Simsun 16",
+                                "value_str": "点我年龄+1"
+                            },
+                            "part": 0,
+                            "state": 5
+                        },
+                        {
+                            "Value": {
+                                "value_font": "Simsun 16",
+                                "value_str": "点我年龄+1"
+                            },
+                            "part": 0,
+                            "state": 6
+                        }
+                    ]
+                }
+            ],
+            "events": [
+            ],
+            "geometry": {
+                "height": 320,
+                "width": 480,
+                "x": 0,
+                "y": 0
+            },
+            "name": "mainpage",
+            "styles": [
+                {
+                    "bg_color": "0xffffff",
+                    "disable": false,
+                    "part": 0,
+                    "state": 0
+                }
+            ],
+            "visible": true
+        }
+    ],
+    "project_name": "demo_1",
+    "project_settings": {
+        "color_depth": 32,
+        "keyBoard": {
+            "chinese_input": false,
+            "chinese_library": "mini",
+            "display_kb": false,
+            "keyboard_font_size": 18,
+            "keyboard_font_type": "SourceHanSerifSC_Regular"
+        },
+        "resolution": {
+            "height": 320,
+            "width": 480
+        },
+        "screen_retated": 90
+    },
+    "ui_version": "7.11.0",
+    "version": "1.0.0"
+}

+ 36 - 4
luat/demo/airui/main.lua

@@ -81,17 +81,49 @@ function tp_init()
     end
 end
 
+
+local num = 10
+local username = "张三"
+-- 用户数据,可以自定义,要和airui.json中的控件设置文本中格式匹配
+-- 例如当前的例子:label的文本中就使用为 {{person.name}} 和 {{person.age}}
+local appdata = {
+    person = {
+        name = username,
+        age = num
+    }
+}
+
+-- 自定义的回调函数
+local function event_handler(obj, event)
+    if (event == lvgl.EVENT_CLICKED) then
+        appdata.person.age = appdata.person.age + 1
+        airui.refresh_text("text_area_2", appdata)
+        log.info("button clicked age=", appdata.person.age)
+    end
+end
+
+-- 用户自定义函数,要和airui.json中的控件下的events字段对应
+local appfuncs = {
+    update_age = event_handler
+}
+
 sys.taskInit(function()
-    local uiJson = io.open("/luadb/ui.json")
+    -- local uiJson = io.open("/luadb/ui.json")
+    -- local ui_path = "/luadb/ui.json"
+    local ui_path = "/luadb/demo_2.json"
+    local uiJson = io.open(ui_path)
     local ui = json.decode(uiJson:read("*a"))
     log.info("ui", ui, ui.pages[1].children[1].name)
-
+    
     lcd_init()
     log.info("初始化lvgl", lvgl.init(ui.project_settings.resolution.width, ui.project_settings.resolution.height))
     tp_init()
     
-    airui.init("/luadb/ui.json")
-end)
+    airui.init(ui_path, {data = appdata, funcs = appfuncs})
 
+    local button = airui.get_widget("button_2")     -- 获得名称为 button_2 的按钮控件对象
+    -- sys.wait(5000)  -- 等待5s
+    -- airui.del_widget(button) -- 删除按钮
+end)
 
 sys.run()

+ 13 - 0
luat/demo/mobile/main.lua

@@ -121,6 +121,19 @@ sys.subscribe("SIM_IND", function(status, value)
 	if status == "SIM_WC" then
         log.info("sim", "write counter", value)
     end
+end)
+
+sys.subscribe("RRC_IND", function(event, v1, v2, v3, v4)
+	log.info("rrc status", event)
+	if event == "DRX" then
+		log.info("drx周期", v1, "ms")
+	end
+	if event == "IDLE_MEAS_THRESHOLD" then
+		log.info("sIntraSearchP", v1)
+		log.info("sNonIntraSearchP", v2)
+		log.info("sIntraSearchQ", v3)
+		log.info("sNonIntraSearchQ", v4)
+	end
 end)
 
 -- 用户代码已结束---------------------------------------------

+ 3 - 0
luat/include/luat_uart.h

@@ -223,6 +223,9 @@ void luat_uart_soft_sleep_enable(uint8_t is_enable);
 
 int luat_uart_wait_485_tx_done(int uartid);
 void luat_uart_patch(int *param);
+
+void luat_uart_set_app_recv(int id, luat_uart_recv_callback_t cb);
+void luat_uart_set_app_sent(int id, luat_uart_sent_callback_t cb);
 /** @}*/
 /** @}*/
 #endif

+ 2 - 1
luat/modules/luat_lib_gpio.c

@@ -102,7 +102,8 @@ int l_gpio_debounce_timer_handler(lua_State *L, void* ptr) {
     lua_geti(L, LUA_REGISTRYINDEX, gpios[pin].lua_ref);
     if (!lua_isnil(L, -1)) {
         lua_pushinteger(L, gpios[pin].latest_state);
-        lua_call(L, 1, 0);
+        lua_pushinteger(L, pin);
+        lua_call(L, 2, 0);
     }
     return 0;
 }

+ 1 - 0
luat/modules/luat_lib_mcu.c

@@ -327,6 +327,7 @@ static int l_mcu_alt_ctrl(lua_State* L)
 @api mcu.ticks2(mode)
 @int 模式, 看后面的用法说明
 @return int 根据mode的不同,返回值的含义不同
+@return int 根据mode的不同,返回值的含义不同
 @usage
 -- 本函数于2024.5.7新增
 -- 与mcu.ticks()的区别是,底层计数器是64bit的, 在可预计的将来不会溢出

+ 8 - 37
luat/modules/luat_lib_pm.c

@@ -9,44 +9,15 @@
 @usage
 --[[
 休眠模式简介
-
--- IDLE 正常运行模式
--- LIGHT 轻睡眠模式:
-        CPU暂停
-        RAM保持供电
-        定时器/网络事件/IO中断均可自动唤醒
-        唤醒后程序继续运行
-        普通GPIO掉电,外设驱动掉电
-        AON_GPIO保持电平
--- DEEP 深睡眠模式
-        CPU暂停
-        核心RAM掉电, 保留RAM维持供电
-        普通GPIO掉电,外设驱动掉电
-        AON_GPIO保持休眠前的电平
-        dtimer定时器可唤醒
-        wakeup脚可唤醒
-        唤醒后程序从头运行,休眠前的运行时数据全丢
--- HIB 休眠模式
-        CPU暂停
-        RAM掉电, 保留RAM也掉电
-        普通GPIO掉电,外设驱动掉电
-        AON_GPIO保持休眠前的电平
-        dtimer定时器可唤醒
-        wakeup脚可唤醒
-        唤醒后程序从头运行,休眠前的运行时数据全丢
-
-对部分模块,例如Air780EXXX, DEEP/HIB对用户代码没有区别
-
-除pm.shutdown()外, RTC总是运行的, 除非掉电
+-- 全速模式
+-- PRO低功耗模式
+-- PSM+模式
+
+以上模式均使用 pm.power(pm.WORK_MODE, mode) 来设置
+-- mode=0   正常运行,就是无休眠
+-- mode=1   轻度休眠, CPU停止, RAM保持, 可中断唤醒, 可定时器唤醒, 可网络唤醒. 支持从休眠处继续运行
+-- mode=3   彻底休眠, CPU停止, RAM掉电, 支持特殊唤醒管脚唤醒, 支持定时器唤醒. 唤醒后脚本从头开始执行
 ]]
-
--- 定时器唤醒, 请使用 pm.dtimerStart()
--- wakeup唤醒
-    -- 如Air101/Air103, 有独立的wakeup脚, 不需要配置,可直接控制唤醒
-    -- 如Air780EXXX系列, 有多个wakeup可用, 通过gpio.setup()配置虚拟GPIO进行唤醒配置,参考demo/gpio/virtualIO
-
-pm.request(pm.IDLE) -- 通过切换不同的值请求进入不同的休眠模式
--- 对应Air780EXXX系列, 执行后并不一定马上进入休眠模式, 如无后续数据传输需求,可先进入飞行模式,然后快速休眠
 */
 #include "lua.h"
 #include "lauxlib.h"

+ 18 - 11
luat/modules/luat_lib_spi.c

@@ -6,6 +6,23 @@
 @demo spi
 @video https://www.bilibili.com/video/BV1VY411M7YH
 @tag LUAT_USE_SPI
+@usage
+-- 本库支持2套API风格
+-- 1. 老的API,spi.xxx 方式,需要自己控制软件cs引脚,不同设备要手动重新配置spi参数
+-- 2. 新的API(推荐使用), spidevice对象方式,不需要手动控制cs引脚,不同设备也无需重复配置参数,设备内部自动管理
+
+
+-- 老API
+spi.setup(0,nil,0,0,8,2000000,spi.MSB,1,1)
+local result = spi.send(0, "123")--发送123
+local recv = spi.recv(0, 4)--接收4字节数据
+spi.close(0)
+-- 新API
+local spi_device = spi.deviceSetup(0,17,0,0,8,2000000,spi.MSB,1,1)
+local result = spi_device:send("123")--发送123
+local recv = spi_device:recv(4)--接收4字节数据
+spi_device:close()
+
 */
 #include "luat_base.h"
 #include "luat_log.h"
@@ -441,9 +458,7 @@ static int l_spi_recv(lua_State *L) {
     {
         luat_spi_device_t *spi_device = (luat_spi_device_t *)luaL_testudata(L, 1, META_SPI);
         if (spi_device){
-            luat_spi_lock(spi_device->bus_id);
             luat_spi_device_recv(spi_device, recv_buff, len);
-            luat_spi_unlock(spi_device->bus_id);
             ret = len;
         }
         else {
@@ -571,7 +586,7 @@ static int l_spi_device_setup(lua_State *L) {
 @return int 成功返回0,否则返回其他值
 @usage
 -- 初始化spi
-spi_device.close()
+spi_device:close()
 */
 static int l_spi_device_close(lua_State *L) {
     luat_spi_device_t* spi_device = (luat_spi_device_t*)lua_touserdata(L, 1);
@@ -636,9 +651,7 @@ static int l_spi_device_transfer(lua_State *L) {
         if(recv_buff == NULL)
             return 0;
     }
-    luat_spi_lock(spi_device->bus_id);
     int ret = luat_spi_device_transfer(spi_device, send_buff, send_length, recv_buff, recv_length);
-    luat_spi_unlock(spi_device->bus_id);
     if (send_mode == LUA_TTABLE){
         luat_heap_free(send_buff);
     }
@@ -675,9 +688,7 @@ static int l_spi_device_send(lua_State *L) {
         luat_zbuff_t *buff = (luat_zbuff_t *)luaL_checkudata(L, 2, LUAT_ZBUFF_TYPE);
         send_buff = (char*)(buff->addr+buff->cursor);
         len = buff->len - buff->cursor;
-        luat_spi_lock(spi_device->bus_id);
         ret = luat_spi_device_send(spi_device, send_buff, len);
-        luat_spi_unlock(spi_device->bus_id);
         lua_pushinteger(L, ret);
     }else if (lua_istable(L, 2)){
         len = lua_rawlen(L, 2); //返回数组的长度
@@ -687,9 +698,7 @@ static int l_spi_device_send(lua_State *L) {
             send_buff[i] = (char)lua_tointeger(L, -1);
             lua_pop(L, 1); //将刚刚获取的元素值从栈中弹出
         }
-        luat_spi_lock(spi_device->bus_id);
         ret = luat_spi_device_send(spi_device, send_buff, len);
-        luat_spi_unlock(spi_device->bus_id);
         lua_pushinteger(L, ret);
         luat_heap_free(send_buff);
     }else if(lua_isstring(L, 2)){
@@ -718,9 +727,7 @@ static int l_spi_device_recv(lua_State *L) {
     int len = luaL_optinteger(L, 2,1);
     char* recv_buff = luat_heap_malloc(len);
     if(recv_buff == NULL) return 0;
-    luat_spi_lock(spi_device->bus_id);
     int ret = luat_spi_device_recv(spi_device, recv_buff, len);
-    luat_spi_unlock(spi_device->bus_id);
     if (ret > 0) {
         lua_pushlstring(L, recv_buff, ret);
         luat_heap_free(recv_buff);

+ 20 - 1
luat/modules/luat_lib_uart.c

@@ -45,6 +45,7 @@ typedef struct luat_uart_cb {
 } luat_uart_cb_t;
 static luat_uart_cb_t uart_cbs[MAX_DEVICE_COUNT + MAX_USB_DEVICE_COUNT];
 static luat_uart_recv_callback_t uart_app_recvs[MAX_DEVICE_COUNT + MAX_USB_DEVICE_COUNT];
+static luat_uart_sent_callback_t uart_app_sents[MAX_DEVICE_COUNT + MAX_USB_DEVICE_COUNT];
 #ifdef LUAT_USE_SOFT_UART
 #ifndef __LUAT_C_CODE_IN_RAM__
 #define __LUAT_C_CODE_IN_RAM__
@@ -346,6 +347,21 @@ void luat_uart_set_app_recv(int id, luat_uart_recv_callback_t cb) {
 	}
 }
 
+void luat_uart_set_app_sent(int id, luat_uart_sent_callback_t cb) {
+	#ifdef LUAT_USE_DRV_UART
+	if (luat_drv_uart_exist(id))
+	#else
+    if (luat_uart_exist(id))
+	#endif
+	{
+        uart_app_sents[id] = cb;
+        luat_setup_cb(id, 1, 1); // 暂时覆盖
+    }
+	else {
+		LLOGW("not exist uart id=%d", id);
+	}
+}
+
 int l_vuart_state_handler(lua_State *L, void* ptr) {
     (void)ptr;
     rtos_msg_t* msg = (rtos_msg_t*)lua_topointer(L, -1);
@@ -385,6 +401,9 @@ int l_uart_handler(lua_State *L, void* ptr) {
     // sent event
     if (msg->arg2 == 0) {
         // LLOGD("uart%ld sent callback", uart_id);
+		if (uart_app_sents[uart_id]) {
+            uart_app_sents[uart_id](uart_id, (void *)msg->arg2);
+        }
         if (uart_cbs[uart_id].sent) {
             lua_geti(L, LUA_REGISTRYINDEX, uart_cbs[uart_id].sent);
             if (lua_isfunction(L, -1)) {
@@ -409,7 +428,7 @@ int l_uart_handler(lua_State *L, void* ptr) {
             }
         }
         else if (uart_app_recvs[uart_id] == NULL) {
-            LLOGD("uart%ld no received callback", uart_id);
+            //LLOGD("uart%ld no received callback", uart_id);
         }
     }
 

+ 22 - 6
luat/modules/luat_lib_zbuff.c

@@ -395,6 +395,15 @@ buff:pack(">IIHA", 0x1234, 0x4567, 0x12,"abcdefg") -- 按格式写入几个数
 -- < 小端
 -- > 大端
 -- = 默认大小端
+
+-- 例子
+buff:pack(
+">IIHA", -- 格式字符串:大端序,依次为[4字节无符号整型, 4字节无符号整型, 2字节无符号短整型, 字符串]
+0x1234, -- 参数1:整数值,写入为4字节(大端:00 00 12 34)
+0x4567, -- 参数2:整数值,写入为4字节(大端:00 00 45 67)
+0x12, -- 参数3:整数值,写入为2字节(大端:00 12)
+"abcdefg" -- 参数4:字符串,写入7字节ASCII码(61 62 63 64 65 66 67)
+)
  */
 #define PACKNUMBER(OP, T)                                    \
     case OP:                                                 \
@@ -1421,19 +1430,26 @@ static int l_zbuff_equal(lua_State *L)
 /**
 将当前zbuff数据转base64,输出到下一个zbuff中
 @api buff:toBase64(dst)
-@userdata zbuff指针, 必须大于目标长度, 即buff:used() * 1.35
-@return int 转换后的长度
+@userdata zbuff指针
+@return int 转换后的实际长度
 @usage
-buff:toBase64(dst) -- dst:len必须大于buff:used() * 1.35
+-- dst:len必须大于buff:used() * 1.35 + 3, 确保有足够空间存放base64数据
+buff:toBase64(dst)
 */
 #include "luat_str.h"
 static int l_zbuff_to_base64(lua_State *L) {
     luat_zbuff_t *buff = ((luat_zbuff_t *)luaL_checkudata(L, 1, LUAT_ZBUFF_TYPE));
     luat_zbuff_t *buff2 = ((luat_zbuff_t *)luaL_checkudata(L, 2, LUAT_ZBUFF_TYPE));
     size_t olen = 0;
-    luat_str_base64_encode(buff2->addr, buff2->len, &olen, buff->addr, buff->used);
-    buff2->used = olen;
-    lua_pushinteger(L, olen);
+    int ret = luat_str_base64_encode(buff2->addr, buff2->len, &olen, buff->addr, buff->used);
+    if (ret) {
+        LLOGE("zbuff toBase64 failed %d", ret);
+        lua_pushinteger(L, 0);
+    }
+    else {
+        buff2->used = olen;
+        lua_pushinteger(L, olen);
+    }
     return 1;
 };
 

+ 0 - 81
module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/airaudio.lua

@@ -1,81 +0,0 @@
---[[
-@module  airaudio
-@summary 初始化codec测试功能模块
-@version 001.000.000
-@date    2025.07.11
-@author  李源龙
-@usage
-使用Air780EHV核心板外接AirAudio_1000,初始化配置:
-Air780EHM            AirAudio_1000
-GND(任意)            GND
-VDD_EXT              VCC
-19/GPIO22            PA_EN
-5/SPK+               SPK+
-4/SPK-               SPK-
-]]
-local airaudio = {}
-
-local i2c_id = 0            -- i2c_id 0
-
-
-local pa_pin = gpio.AUDIOPA_EN -- 喇叭pa功放脚
-local power_pin = 20 -- es8311电源脚
-
-
-local i2s_id = 0            -- i2s_id 0
-local i2s_mode = 0          -- i2s模式 0 主机 1 从机
-local i2s_sample_rate = 16000   -- 采样率
-local i2s_bits_per_sample = 16  -- 数据位数
-local i2s_channel_format = i2s.MONO_R   -- 声道, 0 左声道, 1 右声道, 2 立体声
-local i2s_communication_format = i2s.MODE_LSB   -- 格式, 可选MODE_I2S, MODE_LSB, MODE_MSB
-local i2s_channel_bits = 16     -- 声道的BCLK数量
-
-local multimedia_id = 0         -- 音频通道 0
-local pa_on_level = 1           -- PA打开电平 1 高电平 0 低电平
-local power_delay = 3           -- 在DAC启动前插入的冗余时间,单位100ms
-local pa_delay = 100            -- 在DAC启动后,延迟多长时间打开PA,单位1ms
-local power_on_level = 1        -- 电源控制IO的电平,默认拉高
-local power_time_delay = 600    -- 音频播放完毕时,PA与DAC关闭的时间间隔,单位1ms
-local taskName = "task_tts"
-
-local play_string = "降功耗,找合宙"
-local voice_vol = 60        -- 喇叭音量
-local mic_vol = 80          -- 麦克风音量
-
-function audio_setup()
-    sys.wait(100)
-
-    i2c.setup(i2c_id,i2c.FAST)
-    i2s.setup(i2s_id, i2s_mode, i2s_sample_rate, i2s_bits_per_sample, i2s_channel_format, i2s_communication_format,i2s_channel_bits)
-
-    audio.config(multimedia_id, pa_pin, pa_on_level, power_delay, pa_delay, power_pin, power_on_level, power_time_delay)
-    audio.setBus(multimedia_id, audio.BUS_I2S,{chip = "es8311",i2cid = i2c_id , i2sid = i2s_id, voltage = audio.VOLTAGE_1800})	--通道0的硬件输出通道设置为I2S
-
-    audio.vol(multimedia_id, voice_vol)
-    audio.micVol(multimedia_id, mic_vol)
-
-end
-
-local function audio_callback(id, event)
-    local succ,stop,file_cnt = audio.getError(0)
-    if not succ then
-        if stop then
-            log.info("用户停止播放")
-        else
-            log.info("第", file_cnt, "个文件解码失败")
-        end
-    end
-    sysplus.sendMsg(taskName, MSG_PD)
-end
-
-
-function airaudio.init()
-    sys.wait(100)
-    gpio.setup(power_pin, 1, gpio.PULLUP)   -- 打开音频编解码供电
-    gpio.setup(pa_pin, 1, gpio.PULLUP)      -- pa供电
-    audio_setup()
-    audio.on(0, audio_callback)
-end
-
-
-return airaudio

+ 541 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/exaudio.lua

@@ -0,0 +1,541 @@
+--[[
+@module exaudio
+@summary exaudio扩展库
+@version 1.1
+@date    2025.09.01
+@author  梁健
+@usage
+]]
+local exaudio = {}
+
+-- 常量定义
+local I2S_ID = 0
+local I2S_MODE = 0          -- 0:主机 1:从机
+local I2S_SAMPLE_RATE = 16000
+local I2S_CHANNEL_FORMAT = i2s.MONO_R  
+local I2S_COMM_FORMAT = i2s.MODE_LSB   -- 可选MODE_I2S, MODE_LSB, MODE_MSB
+local I2S_CHANNEL_BITS = 16
+local MULTIMEDIA_ID = 0
+local EX_MSG_PLAY_DONE = "playDone"
+local ES8311_ADDR = 0x18    -- 7位地址
+local CHIP_ID_REG = 0x00    -- 芯片ID寄存器地址
+
+-- 模块常量
+exaudio.PLAY_DONE = 1         --   音频播放完毕的事件之一
+exaudio.RECORD_DONE = 1       --   音频录音完毕的事件之一  
+exaudio.AMR_NB = 0
+exaudio.AMR_WB = 1
+exaudio.PCM_8000 = 2
+exaudio.PCM_16000 = 3 
+exaudio.PCM_24000 = 4
+exaudio.PCM_32000 = 5
+
+
+-- 默认配置参数
+local audio_setup_param = {
+    model = "es8311",         -- dac类型: "es8311","es8211"
+    i2c_id = 0,               -- i2c_id: 0,1
+    pa_ctrl = 0,              -- 音频放大器电源控制管脚
+    dac_ctrl = 0,             -- 音频编解码芯片电源控制管脚
+    dac_delay = 3,            -- DAC启动前冗余时间(100ms)
+    pa_delay = 100,           -- DAC启动后延迟打开PA的时间(ms)
+    dac_time_delay = 600,     -- 播放完毕后PA与DAC关闭间隔(ms)
+    bits_per_sample = 16,     -- 采样位数
+    pa_on_level = 1           -- PA打开电平 1:高 0:低        
+}
+
+local audio_play_param = {
+    type = 0,                 -- 0:文件 1:TTS 2:流式
+    content = nil,            -- 播放内容
+    cbfnc = nil,              -- 播放完毕回调
+    priority = 0,             -- 优先级(数值越大越高)
+    sampling_rate = 16000,    -- 采样率(仅流式)
+    sampling_depth = 16,      -- 采样位深(仅流式)
+    signed_or_unsigned = true -- PCM是否有符号(仅流式)
+}
+
+local audio_record_param = {
+    format = 0,               -- 录制格式,支持exaudio.AMR_NB,exaudio.AMR_WB,exaudio.PCM_8000,exaudio.PCM_16000,exaudio.PCM_24000,exaudio.PCM_32000
+    time = 5,                 -- 录制时间(秒)
+    path = nil,               -- 文件路径或流式回调
+    cbfnc = nil               -- 录音完毕回调
+}
+
+-- 内部变量
+local pcm_buff0 = nil
+local pcm_buff1 = nil
+local voice_vol = 55
+local mic_vol = 80
+
+-- 定义全局队列表
+local audio_play_queue = {
+    data = {},       -- 存储字符串的数组
+    sequenceIndex = 1  -- 用于跟踪插入顺序的索引
+}
+
+-- 向队列中添加字符串(按调用顺序插入)
+local function audio_play_queue_push(str)
+    if type(str) == "string" then
+        -- 存储格式: {index = 顺序索引, value = 字符串值}
+        table.insert(audio_play_queue.data, {
+            index = audio_play_queue.sequenceIndex,
+            value = str
+        })
+        audio_play_queue.sequenceIndex = audio_play_queue.sequenceIndex + 1
+        return true
+    end
+    return false
+end
+
+-- 从队列中取出最早插入的字符串(按顺序取出)
+local function audio_play_queue_pop()
+    if #audio_play_queue.data > 0 then
+        -- 取出并移除第一个元素
+        local item = table.remove(audio_play_queue.data, 1)
+        return item.value  -- 返回值
+    end
+    return nil
+end
+-- 清空队列中所有数据
+function audio_queue_clear()
+    -- 清空数组
+    audio_play_queue.data = {}
+    -- 重置顺序索引
+    audio_play_queue.sequenceIndex = 1
+    return true
+end
+
+-- 工具函数:参数检查
+local function check_param(param, expected_type, name)
+    if type(param) ~= expected_type then
+        log.error(string.format("参数错误: %s 应为 %s 类型", name, expected_type))
+        return false
+    end
+    return true
+end
+
+-- 音频回调处理
+local function audio_callback(id, event, point)
+    -- log.info("audio_callback", "event:", event, 
+    --         "MORE_DATA:", audio.MORE_DATA, 
+    --         "DONE:", audio.DONE,
+    --         "RECORD_DATA:", audio.RECORD_DATA,
+    --         "RECORD_DONE:", audio.RECORD_DONE)
+
+    if event == audio.MORE_DATA then
+        audio.write(MULTIMEDIA_ID,audio_play_queue_pop())
+    elseif event == audio.DONE then
+        if type(audio_play_param.cbfnc) == "function" then
+            audio_play_param.cbfnc(exaudio.PLAY_DONE)
+        end
+        audio_queue_clear()  -- 清空流式播放数据队列
+        sys.publish(EX_MSG_PLAY_DONE)
+        
+    elseif event == audio.RECORD_DATA then
+        if type(audio_record_param.path) == "function" then
+            local buff, len = point == 0 and pcm_buff0 or pcm_buff1,
+                             point == 0 and pcm_buff0:used() or pcm_buff1:used()
+            audio_record_param.path(buff, len)
+        end
+        
+    elseif event == audio.RECORD_DONE then
+        if type(audio_record_param.cbfnc) == "function" then
+            audio_record_param.cbfnc(exaudio.RECORD_DONE)
+        end
+    end
+end
+
+-- 读取ES8311芯片ID
+local function read_es8311_id()
+
+
+    -- 发送读取请求
+    local send_ok = i2c.send(audio_setup_param.i2c_id, ES8311_ADDR, CHIP_ID_REG)
+    if not send_ok then
+        log.error("发送芯片ID读取请求失败")
+        return false
+    end
+
+    -- 读取数据
+    local data = i2c.recv(audio_setup_param.i2c_id, ES8311_ADDR, 1)
+    if data and #data == 1 then
+        return true
+    end
+
+    log.error("读取ES8311芯片ID失败")
+    return false
+end
+
+-- 音频硬件初始化
+local function audio_setup()
+    -- I2C配置
+    if not i2c.setup(audio_setup_param.i2c_id, i2c.FAST) then
+        log.error("I2C初始化失败")
+        return false
+    end
+    -- 初始化I2S
+    local result, data = i2s.setup(
+        I2S_ID, 
+        I2S_MODE, 
+        I2S_SAMPLE_RATE, 
+        audio_setup_param.bits_per_sample, 
+        I2S_CHANNEL_FORMAT, 
+        I2S_COMM_FORMAT,
+        I2S_CHANNEL_BITS
+    )
+
+    if not result then
+        log.error("I2S设置失败")
+        return false
+    end
+    -- 配置音频通道
+    audio.config(
+        MULTIMEDIA_ID, 
+        audio_setup_param.pa_ctrl, 
+        audio_setup_param.pa_on_level, 
+        audio_setup_param.dac_delay, 
+        audio_setup_param.pa_delay, 
+        audio_setup_param.dac_ctrl, 
+        1,  -- power_on_level
+        audio_setup_param.dac_time_delay
+    )
+    -- 设置总线
+    audio.setBus(
+        MULTIMEDIA_ID, 
+        audio.BUS_I2S,
+        {
+            chip = audio_setup_param.model,
+            i2cid = audio_setup_param.i2c_id,
+            i2sid = I2S_ID,
+            voltage = audio.VOLTAGE_1800
+        }
+    )
+
+  
+    -- 设置音量
+    audio.vol(MULTIMEDIA_ID, voice_vol)
+    audio.micVol(MULTIMEDIA_ID, mic_vol)
+    audio.pm(MULTIMEDIA_ID, audio.RESUME)
+    
+    -- 检查芯片连接
+    if audio_setup_param.model == "es8311" and not read_es8311_id() then
+        log.error("ES8311通讯失败,请检查硬件")
+        return false
+    end
+
+    -- 注册回调
+    audio.on(MULTIMEDIA_ID, audio_callback)
+    return true
+end
+
+-- 模块接口:初始化
+function exaudio.setup(audioConfigs)
+    -- 检查必要参数
+    if not  audio  then
+        log.error("不支持audio 库,请选择支持audio 的core")
+        return false
+    end
+    if not audioConfigs or type(audioConfigs) ~= "table" then
+        log.error("配置参数必须为table类型")
+        return false
+    end
+    -- 检查codec型号
+    if not audioConfigs.model or 
+       (audioConfigs.model ~= "es8311" and audioConfigs.model ~= "es8211") then
+        log.error("请指定正确的codec型号(es8311或es8211)")
+        return false
+    end
+    audio_setup_param.model = audioConfigs.model
+    -- 针对ES8311的特殊检查
+    if audioConfigs.model == "es8311" then
+        if not check_param(audioConfigs.i2c_id, "number", "i2c_id") then
+            return false
+        end
+        audio_setup_param.i2c_id = audioConfigs.i2c_id
+    end
+
+    -- 检查功率放大器控制管脚
+    if audioConfigs.pa_ctrl == nil then
+        log.warn("pa_ctrl(功率放大器控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
+    end
+    audio_setup_param.pa_ctrl = audioConfigs.pa_ctrl
+
+    -- 检查功率放大器控制管脚
+    if audioConfigs.dac_ctrl == nil then
+        log.warn("dac_ctrl(音频编解码控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
+    end
+    audio_setup_param.dac_ctrl = audioConfigs.dac_ctrl
+
+
+    -- 处理可选参数
+    local optional_params = {
+        {name = "dac_delay", type = "number"},
+        {name = "pa_delay", type = "number"},
+        {name = "dac_time_delay", type = "number"},
+        {name = "bits_per_sample", type = "number"},
+        {name = "pa_on_level", type = "number"}
+    }
+
+    for _, param in ipairs(optional_params) do
+        if audioConfigs[param.name] ~= nil then
+            if check_param(audioConfigs[param.name], param.type, param.name) then
+                audio_setup_param[param.name] = audioConfigs[param.name]
+            else
+                return false
+            end
+        end
+    end
+
+    -- 确保采样位数有默认值
+    audio_setup_param.bits_per_sample = audio_setup_param.bits_per_sample or 16
+    return audio_setup()
+end
+
+-- 模块接口:开始播放
+function exaudio.play_start(playConfigs)
+    if not playConfigs or type(playConfigs) ~= "table" then
+        log.error("播放配置必须为table类型")
+        return false
+    end
+
+    -- 检查播放类型
+    if not check_param(playConfigs.type, "number", "type") then
+        log.error("type必须为数值(0:文件,1:TTS,2:流式)")
+        return false
+    end
+    audio_play_param.type = playConfigs.type
+
+    -- 处理优先级
+    if playConfigs.priority ~= nil then
+        if check_param(playConfigs.priority, "number", "priority") then
+            if playConfigs.priority > audio_play_param.priority then
+                log.error("是否完成播放",audio.isEnd(MULTIMEDIA_ID))
+                if not audio.isEnd(MULTIMEDIA_ID) then
+                    if audio.play(MULTIMEDIA_ID) ~= true then
+                        return false
+                    end
+                    sys.waitUntil(EX_MSG_PLAY_DONE)
+                end
+                audio_play_param.priority = playConfigs.priority
+            end
+        else
+            return false
+        end
+    end
+
+    -- 处理不同播放类型
+    local play_type = audio_play_param.type
+    if play_type == 0 then  -- 文件播放
+        if not playConfigs.content then
+            log.error("文件播放需要指定content(文件路径或路径表)")
+            return false
+        end
+
+        local content_type = type(playConfigs.content)
+        if content_type == "table" then
+            for _, path in ipairs(playConfigs.content) do
+                if type(path) ~= "string" then
+                    log.error("播放列表元素必须为字符串路径")
+                    return false
+                end
+            end
+        elseif content_type ~= "string" then
+            log.error("文件播放content必须为字符串或路径表")
+            return false
+        end
+
+        audio_play_param.content = playConfigs.content
+        if audio.play(MULTIMEDIA_ID, audio_play_param.content) ~= true then
+            return false
+        end
+
+    elseif play_type == 1 then  -- TTS播放
+        if not audio.tts then
+            log.error("本固件不支持TTS,请更换支持TTS 的固件")
+            return false
+        end
+        if not check_param(playConfigs.content, "string", "content") then
+            log.error("TTS播放content必须为字符串")
+            return false
+        end
+        audio_play_param.content = playConfigs.content
+        if audio.tts(MULTIMEDIA_ID, audio_play_param.content)  ~= true  then
+            return false
+        end
+
+    elseif play_type == 2 then  -- 流式播放
+        if not check_param(playConfigs.sampling_rate, "number", "sampling_rate") then
+            return false
+        end
+        if not check_param(playConfigs.sampling_depth, "number", "sampling_depth") then
+            return false
+        end
+
+        audio_play_param.content = playConfigs.content
+        audio_play_param.sampling_rate = playConfigs.sampling_rate
+        audio_play_param.sampling_depth = playConfigs.sampling_depth
+        
+        if playConfigs.signed_or_unsigned ~= nil then
+            audio_play_param.signed_or_unsigned = playConfigs.signed_or_unsigned
+        end
+
+        audio.start(
+            MULTIMEDIA_ID, 
+            audio.PCM, 
+            1, 
+            playConfigs.sampling_rate, 
+            playConfigs.sampling_depth, 
+            audio_play_param.signed_or_unsigned
+        )
+        -- 发送初始数据
+        if audio.write(MULTIMEDIA_ID, string.rep("\0", 512)) ~= true then
+            return false
+        end
+    end
+
+    -- 处理回调函数
+    if playConfigs.cbfnc ~= nil then
+        if check_param(playConfigs.cbfnc, "function", "cbfnc") then
+            audio_play_param.cbfnc = playConfigs.cbfnc
+        else
+            return false
+        end
+    else
+        audio_play_param.cbfnc = nil
+    end
+    return true
+end
+
+-- 模块接口:流式播放数据写入
+function exaudio.play_stream_write(data)
+    audio_play_queue_push(data)
+    return true
+end
+
+-- 模块接口:停止播放
+function exaudio.play_stop()
+    return audio.play(MULTIMEDIA_ID)
+end
+
+-- 模块接口:检查播放是否结束
+function exaudio.is_end()
+    return audio.isEnd(MULTIMEDIA_ID)
+end
+
+-- 模块接口:获取错误信息
+function exaudio.get_error()
+    return audio.getError(MULTIMEDIA_ID)
+end
+
+-- 模块接口:开始录音
+function exaudio.record_start(recodConfigs)
+    if not recodConfigs or type(recodConfigs) ~= "table" then
+        log.error("录音配置必须为table类型")
+        return false
+    end
+    -- 检查录音格式
+    if recodConfigs.format == nil or type(recodConfigs.format) ~= "number" or recodConfigs.format > 5 then
+        log.error("请指定正确的录音格式")
+        return false
+    end
+    audio_record_param.format = recodConfigs.format
+
+    -- 处理录音时间
+    if recodConfigs.time ~= nil then
+        if check_param(recodConfigs.time, "number", "time") then
+            audio_record_param.time = recodConfigs.time
+        else
+            return false
+        end
+    else
+        audio_record_param.time = 0
+    end
+
+    -- 处理存储路径/回调
+    if not recodConfigs.path then
+        log.error("必须指定录音路径或流式回调函数")
+        return false
+    end
+    audio_record_param.path = recodConfigs.path
+
+    -- 转换录音格式
+    local recod_format, amr_quailty
+    if audio_record_param.format == exaudio.AMR_NB then
+        recod_format = audio.AMR_NB
+        amr_quailty = 7
+    elseif audio_record_param.format == exaudio.AMR_WB then
+        recod_format = audio.AMR_WB
+        amr_quailty = 8
+    elseif audio_record_param.format == exaudio.PCM_8000 then
+        recod_format = 8000
+    elseif audio_record_param.format == exaudio.PCM_16000 then
+        recod_format = 16000
+    elseif audio_record_param.format == exaudio.PCM_24000 then
+        recod_format = 24000
+    elseif audio_record_param.format == exaudio.PCM_32000 then
+        recod_format = 32000
+    end
+
+    -- 处理回调函数
+    if recodConfigs.cbfnc ~= nil then
+        if check_param(recodConfigs.cbfnc, "function", "cbfnc") then
+            audio_record_param.cbfnc = recodConfigs.cbfnc
+        else
+            return false
+        end
+    else
+        audio_record_param.cbfnc = nil
+    end
+    -- 开始录音
+    local path_type = type(audio_record_param.path)
+    if path_type == "string" then
+        return audio.record(
+            MULTIMEDIA_ID, 
+            recod_format, 
+            audio_record_param.time, 
+            amr_quailty, 
+            audio_record_param.path
+        )
+    elseif path_type == "function" then
+        -- 初始化缓冲区
+        if not pcm_buff0 or not pcm_buff1 then
+            pcm_buff0 = zbuff.create(16000)
+            pcm_buff1 = zbuff.create(16000)
+        end
+        return audio.record(
+            MULTIMEDIA_ID, 
+            recod_format, 
+            audio_record_param.time, 
+            amr_quailty, 
+            nil, 
+            3,
+            pcm_buff0,
+            pcm_buff1
+        )
+    end
+    log.error("录音路径必须为字符串或函数")
+    return false
+end
+
+-- 模块接口:停止录音
+function exaudio.record_stop()
+    return audio.recordStop(MULTIMEDIA_ID)
+end
+
+-- 模块接口:设置音量
+function exaudio.vol(play_volume)
+    if check_param(play_volume, "number", "音量值") then
+        return audio.vol(MULTIMEDIA_ID, play_volume)
+    end
+    return false
+end
+
+-- 模块接口:设置麦克风音量
+function exaudio.mic_vol(record_volume)
+    if check_param(record_volume, "number", "麦克风音量值") then
+        return audio.micVol(MULTIMEDIA_ID, record_volume)  
+    end
+    return false
+end
+
+return exaudio

+ 561 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/extalk.lua

@@ -0,0 +1,561 @@
+--[[
+@module extalk
+@summary extalk扩展库
+@version 1.1.1
+@date    2025.09.18
+@author  梁健
+@usage
+    local extalk = require "extalk"
+    -- 配置并初始化
+    extalk.setup({
+        key = "your_product_key",
+        heart_break_time = 30,
+        contact_list_cbfnc = function(dev_list) end,
+        state_cbfnc = function(state) end
+    })
+    -- 发起对讲
+    extalk.start("remote_device_id")
+    -- 结束对讲
+    extalk.stop()
+]]
+
+local extalk = {}
+
+-- 模块常量(保留原始数据结构)
+extalk.START = 1     -- 通话开始
+extalk.STOP = 2      -- 通话结束
+extalk.UNRESPONSIVE = 3  -- 未响应
+extalk.ONE_ON_ONE = 5  -- 一对一来电
+extalk.BROADCAST = 6 -- 广播
+
+local AIRTALK_TASK_NAME = "airtalk_task"
+
+-- 消息类型常量(保留原始数据结构)
+local MSG_CONNECT_ON_IND = 0
+local MSG_CONNECT_OFF_IND = 1
+local MSG_AUTH_IND = 2
+local MSG_SPEECH_ON_IND = 3
+local MSG_SPEECH_OFF_IND = 4
+local MSG_SPEECH_CONNECT_TO = 5
+local MSG_SPEECH_STOP_TEST_END = 22
+
+-- 设备状态常量(保留原始数据结构)
+local SP_T_NO_READY = 0           -- 离线状态无法对讲
+local SP_T_IDLE = 1               -- 对讲空闲状态
+local SP_T_CONNECTING = 2         -- 主动发起对讲
+local SP_T_CONNECTED = 3          -- 对讲中
+
+local SUCC = "success"
+
+-- 全局状态变量(保留原始数据结构)
+local g_state = SP_T_NO_READY   -- 设备状态
+local g_mqttc = nil             -- mqtt客户端
+local g_local_id                -- 本机ID
+local g_remote_id               -- 对端ID
+local g_s_type                  -- 对讲的模式,字符串形式
+local g_s_topic                 -- 对讲用的topic
+local g_s_mode                  -- 对讲的模式
+local g_dev_list                -- 对讲列表
+local g_dl_topic                -- 下行消息topic模板
+
+-- 配置参数
+local extalk_configs_local = {
+    key = 0,               -- 项目key,一般需要和main的PRODUCT_KEY保持一致
+    heart_break_time = 0,  -- 心跳间隔(单位秒)
+    contact_list_cbfnc = nil, -- 联系人回调函数,含设备号和昵称
+    state_cbfnc = nil,  -- 状态回调,分为对讲开始,对讲结束,未响应
+}
+
+-- 工具函数:参数检查
+local function check_param(param, expected_type, name)
+    if type(param) ~= expected_type then
+        log.error(string.format("参数错误: %s 应为 %s 类型,实际为 %s", 
+            name, expected_type, type(param)))
+        return false
+    end
+    return true
+end
+
+-- 发送鉴权消息
+local function auth()
+    if g_state == SP_T_NO_READY and g_mqttc then
+        local topic = string.format("ctrl/uplink/%s/0001", g_local_id)
+        local payload = json.encode({
+            ["key"] = extalk_configs_local.key, 
+            ["device_type"] = 1
+        })
+        g_mqttc:publish(topic, payload)
+    end
+end
+
+-- 发送心跳消息
+local function heart()
+    if g_state == SP_T_CONNECTED and g_mqttc then
+        local topic = string.format("ctrl/uplink/%s/0005", g_local_id)
+        local payload = json.encode({
+            ["from"] = g_local_id, 
+            ["to"] = g_remote_id
+        })
+        g_mqttc:publish(topic, payload)
+    end
+end
+
+-- 开始对讲
+local function speech_on(ssrc, sample)
+    g_state = SP_T_CONNECTED
+    g_mqttc:subscribe(g_s_topic)
+    airtalk.set_topic(g_s_topic)
+    airtalk.set_ssrc(ssrc)
+    log.info("对讲模式", g_s_mode)
+    airtalk.speech(true, g_s_mode, sample)
+    sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, true) 
+    sys.timerLoopStart(heart, extalk_configs_local.heart_break_time * 1000)
+    sys.timerStopAll(wait_speech_to)
+end
+
+-- 结束对讲
+local function speech_off(need_upload, need_ind)
+    if g_state == SP_T_CONNECTED then
+        g_mqttc:unsubscribe(g_s_topic)
+        airtalk.speech(false)
+        g_s_topic = nil
+    end
+    
+    g_state = SP_T_IDLE
+    sys.timerStopAll(auth)
+    sys.timerStopAll(heart)
+    sys.timerStopAll(wait_speech_to)
+    
+    if need_upload and g_mqttc then
+        local topic = string.format("ctrl/uplink/%s/0004", g_local_id)
+        g_mqttc:publish(topic, json.encode({["to"] = g_remote_id}))
+    end
+
+    if need_ind then
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_OFF_IND, true)
+    end
+end
+
+-- 对讲超时处理
+local function wait_speech_to()
+    log.info("主动请求对讲超时无应答")
+    speech_off(true, false)
+end
+
+-- 命令处理:请求对讲应答
+local function handle_speech_response(obj)
+    if g_state ~= SP_T_CONNECTING then
+        log.error("state", g_state, "need", SP_T_CONNECTING)
+        return
+    end
+
+    if obj and obj["result"] == SUCC and g_s_topic == obj["topic"] then
+        -- 开始对讲
+        local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
+        speech_on(obj["ssrc"], sample_rate)
+        return
+    else
+        log.info(obj["result"], obj["topic"], g_s_topic)
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, false)
+    end
+    
+    g_s_topic = nil
+    g_state = SP_T_IDLE
+end
+
+-- 命令处理:对端来电
+local function handle_incoming_call(obj)
+    if not obj or not obj["topic"] or not obj["ssrc"] or not obj["audio_code"] or not obj["type"] then
+        local response = {
+            ["result"] = "failed", 
+            ["topic"] = obj and obj["topic"] or "", 
+            ["info"] = "无效的请求参数"
+        }
+        g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+        return
+    end
+
+    -- 非空闲状态无法接收来电
+    if g_state ~= SP_T_IDLE then
+        log.error("state", g_state, "need", SP_T_IDLE)
+        local response = {
+            ["result"] = "failed", 
+            ["topic"] = obj["topic"], 
+            ["info"] = "device is busy"
+        }
+        g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+        return
+    end
+
+    local response, from = {}, nil
+    
+    -- 提取对端ID
+    from = string.match(obj["topic"], "audio/(.*)/.*/.*")
+    if not from then
+        response = {
+            ["result"] = "failed", 
+            ["topic"] = obj["topic"], 
+            ["info"] = "topic error"
+        }
+        g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+        return
+    end
+
+    -- 处理一对一通话
+    if obj["type"] == "one-on-one" then
+        g_s_topic = obj["topic"]
+        g_remote_id = from
+        g_s_type = "one-on-one"
+        g_s_mode = airtalk.MODE_PERSON
+        
+        -- 触发回调
+        if extalk_configs_local.state_cbfnc then
+            extalk_configs_local.state_cbfnc({
+                state = extalk.ONE_ON_ONE, 
+                id = from 
+            })
+        end
+        
+        response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
+        local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
+        speech_on(obj["ssrc"], sample_rate)
+    end
+
+    -- 处理广播
+    if obj["type"] == "broadcast" then
+        g_s_topic = obj["topic"]
+        g_remote_id = from
+        g_s_mode = airtalk.MODE_GROUP_LISTENER
+        g_s_type = "broadcast"
+        
+        -- 触发回调
+        if extalk_configs_local.state_cbfnc then
+            extalk_configs_local.state_cbfnc({
+                state = extalk.BROADCAST, 
+                id = from 
+            })
+        end
+        
+        response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
+        local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
+        speech_on(obj["ssrc"], sample_rate)
+    end
+
+    -- 发送响应
+    g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+end
+
+-- 命令处理:对端挂断
+local function handle_remote_hangup(obj)
+    local response = {}
+    
+    if g_state == SP_T_IDLE then
+        response = {["result"] = "failed", ["info"] = "no speech"}
+    else
+        log.info("0103", obj, obj["type"], g_s_type)
+        if obj and obj["type"] == g_s_type then
+            response = {["result"] = SUCC, ["info"] = ""}
+            speech_off(false, true)
+        else
+            response = {["result"] = "failed", ["info"] = "type mismatch"}
+        end
+    end
+    
+    g_mqttc:publish(string.format("ctrl/uplink/%s/8103", g_local_id), json.encode(response))
+end
+
+-- 命令处理:更新设备列表
+local function handle_device_list_update(obj)
+    local response = {}
+    if obj then
+        g_dev_list = obj["dev_list"]
+        response = {["result"] = SUCC, ["info"] = ""}
+    else
+        response = {["result"] = "failed", ["info"] = "json info error"}
+    end
+    
+    g_mqttc:publish(string.format("ctrl/uplink/%s/8101", g_local_id), json.encode(response))
+end
+
+-- 命令处理:鉴权结果
+local function handle_auth_result(obj)
+    if obj and obj["result"] == SUCC then
+        g_mqttc:publish(string.format("ctrl/uplink/%s/0002", g_local_id), "")  -- 更新列表
+    else
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, 
+            "鉴权失败" .. (obj and obj["info"] or "")) 
+    end
+end
+
+-- 命令处理:设备列表更新应答
+local function handle_device_list_response(obj)
+    if obj and obj["result"] == SUCC then
+        g_dev_list = obj["dev_list"]
+        if extalk_configs_local.contact_list_cbfnc then
+            extalk_configs_local.contact_list_cbfnc(g_dev_list)
+        end
+        g_state = SP_T_IDLE
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, true)  -- 完整登录流程结束
+    else
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "更新设备列表失败") 
+    end
+end
+
+-- 命令解析路由表
+local cmd_handlers = {
+    ["8003"] = handle_speech_response,  -- 请求对讲应答
+    ["0102"] = handle_incoming_call,    -- 平台通知对端对讲开始
+    ["0103"] = handle_remote_hangup,    -- 平台通知终端对讲结束
+    ["0101"] = handle_device_list_update,-- 平台通知终端更新对讲设备列表
+    ["8001"] = handle_auth_result,      -- 平台对鉴权应答
+    ["8002"] = handle_device_list_response -- 平台对终端获取终端列表应答
+}
+
+-- 解析接收到的消息
+local function analyze_v1(cmd, topic, obj)
+    -- 忽略心跳和结束对讲的应答
+    if cmd == "8005" or cmd == "8004" then
+        return
+    end
+    
+    -- 查找并执行对应的命令处理器
+    local handler = cmd_handlers[cmd]
+    if handler then
+        handler(obj)
+    else
+        log.warn("未处理的命令", cmd)
+    end
+end
+
+-- MQTT回调处理
+local function mqtt_cb(mqttc, event, topic, payload)
+    log.info(event, topic or "")
+    
+    if event == "conack" then
+        -- MQTT连接成功,开始自定义鉴权流程
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
+        g_mqttc:subscribe("ctrl/downlink/" .. g_local_id .. "/#")
+    elseif event == "suback" then
+        if g_state == SP_T_NO_READY then
+            if topic then
+                auth()
+            else
+                sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, 
+                    "订阅失败" .. "ctrl/downlink/" .. g_local_id .. "/#") 
+            end
+        elseif g_state == SP_T_CONNECTED and not topic then
+            speech_off(false, true)
+        end
+    elseif event == "recv" then
+        local result = string.match(topic, g_dl_topic)
+        if result then 
+            local obj = json.decode(payload)
+            analyze_v1(result, topic, obj)
+        end
+    elseif event == "disconnect" then
+        speech_off(false, true)
+        g_state = SP_T_NO_READY
+    elseif event == "error" then
+        log.error("MQTT错误发生")
+    end
+end
+
+-- 任务消息处理
+local function task_cb(msg)
+    if msg[1] == MSG_SPEECH_CONNECT_TO then
+        speech_off(true, false)
+    else
+        log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
+    end
+end
+
+-- 对讲事件回调
+local function airtalk_event_cb(event, param)
+    log.info("airtalk event", event, param)
+    if event == airtalk.EVENT_ERROR then
+        if param == airtalk.ERROR_NO_DATA then
+            log.error("长时间没有收到音频数据")
+            speech_off(true, true)
+        end
+    end
+end
+
+-- MQTT任务主循环
+local function airtalk_mqtt_task()
+    local msg, online = nil, false
+    
+    -- 初始化本地ID
+    g_local_id = mobile.imei()
+    g_dl_topic = "ctrl/downlink/" .. g_local_id .. "/(%w%w%w%w)"
+    
+    -- 创建MQTT客户端
+    g_mqttc = mqtt.create(nil, "mqtt.airtalk.luatos.com", 1883, false, {rxSize = 32768})
+    
+    -- 配置对讲参数
+    airtalk.config(airtalk.PROTOCOL_MQTT, g_mqttc, 200) -- 缓冲至少200ms播放
+    airtalk.on(airtalk_event_cb)
+    airtalk.start()
+    
+    -- 配置MQTT客户端
+    g_mqttc:auth(g_local_id, g_local_id, mobile.muid())
+    g_mqttc:keepalive(240) -- 默认值240s
+    g_mqttc:autoreconn(true, 15000) -- 自动重连机制
+    g_mqttc:debug(false)
+    g_mqttc:on(mqtt_cb)
+    
+    log.info("设备信息", g_local_id, mobile.muid())
+    
+    -- 开始连接
+    g_mqttc:connect()
+    online = false
+    
+    while true do
+        -- 等待MQTT连接成功
+        msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
+        log.info("connected")
+        
+        -- 处理登录流程
+        while not online do
+            msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, 30000) -- 30秒超时
+            
+            if type(msg) == 'table' then
+                online = msg[2]
+                if online then
+                    -- 鉴权通过,60分钟后重新鉴权
+                    sys.timerLoopStart(auth, 3600000)
+                else
+                    log.info(msg[3])
+                    -- 鉴权失败,5分钟后重试
+                    sys.timerLoopStart(auth, 300000)
+                end
+            else
+                -- 超时未收到鉴权结果,重新发送
+                auth()
+            end
+        end
+        
+        log.info("对讲管理平台已连接")
+        
+        -- 处理在线状态下的消息
+        while online do
+            msg = sys.waitMsg(AIRTALK_TASK_NAME)
+            
+            if type(msg) == 'table' and type(msg[1]) == "number" then
+                if msg[1] == MSG_SPEECH_STOP_TEST_END then
+                    if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
+                        log.info("没有对讲", g_state)
+                    else
+                        speech_off(true, false)
+                    end
+                elseif msg[1] == MSG_SPEECH_ON_IND then
+                    if extalk_configs_local.state_cbfnc then
+                        local state = msg[2] and extalk.START or extalk.UNRESPONSIVE
+                        extalk_configs_local.state_cbfnc({state = state})
+                    end
+                elseif msg[1] == MSG_SPEECH_OFF_IND then
+                    if extalk_configs_local.state_cbfnc then
+                        extalk_configs_local.state_cbfnc({state = extalk.STOP})
+                    end
+                elseif msg[1] == MSG_CONNECT_OFF_IND then
+                    log.info("connect", msg[2])
+                    online = msg[2]
+                end
+            else
+                log.info(type(msg), type(msg and msg[1]))
+            end
+            
+            msg = nil -- 清理引用
+        end
+        
+        online = false -- 重置在线状态
+    end
+end
+
+-- 模块初始化
+function extalk.setup(extalk_configs)
+    if not extalk_configs or type(extalk_configs) ~= "table" then
+        log.error("AirTalk配置必须为table类型")
+        return false
+    end
+
+    -- 检查配置参数
+    if not check_param(extalk_configs.key, "string", "key") then
+        return false
+    end
+    extalk_configs_local.key = extalk_configs.key
+
+    if not check_param(extalk_configs.heart_break_time, "number", "heart_break_time") then
+        return false
+    end
+    extalk_configs_local.heart_break_time = extalk_configs.heart_break_time
+
+    if not check_param(extalk_configs.contact_list_cbfnc, "function", "contact_list_cbfnc") then
+        return false
+    end
+    extalk_configs_local.contact_list_cbfnc = extalk_configs.contact_list_cbfnc
+
+    if not check_param(extalk_configs.state_cbfnc, "function", "state_cbfnc") then
+        return false
+    end
+    extalk_configs_local.state_cbfnc = extalk_configs.state_cbfnc
+
+    -- 启动任务
+    sys.taskInitEx(airtalk_mqtt_task, AIRTALK_TASK_NAME, task_cb)
+    return true
+end
+
+-- 开始对讲
+function extalk.start(id)
+    if g_state ~= SP_T_IDLE then
+        log.warn("正在对讲无法开始,当前状态:", g_state)
+        return false
+    end
+
+    if id == nil then
+        -- 广播模式
+        g_remote_id = "all"
+        g_state = SP_T_CONNECTING
+        g_s_mode = airtalk.MODE_GROUP_SPEAKER
+        g_s_type = "broadcast"
+        g_s_topic = string.format("audio/%s/all/%s", 
+            g_local_id, string.sub(tostring(mcu.ticks()), -4, -1))
+        
+        g_mqttc:publish(string.format("ctrl/uplink/%s/0003", g_local_id), 
+            json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
+        sys.timerStart(wait_speech_to, 15000)
+    else
+        -- 一对一模式
+        log.info("向", id, "主动发起对讲")
+        if id == g_local_id then
+            log.error("不允许本机给本机拨打电话")
+            return false
+        end
+        
+        g_state = SP_T_CONNECTING
+        g_remote_id = id
+        g_s_mode = airtalk.MODE_PERSON
+        g_s_type = "one-on-one"
+        g_s_topic = string.format("audio/%s/%s/%s", 
+            g_local_id, id, string.sub(tostring(mcu.ticks()), -4, -1))
+        
+        g_mqttc:publish(string.format("ctrl/uplink/%s/0003", g_local_id), 
+            json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
+        sys.timerStart(wait_speech_to, 15000)
+    end
+    
+    return true
+end
+
+-- 结束对讲
+function extalk.stop()
+    if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
+        log.info("没有对讲,当前状态:", g_state)
+        return false
+    end
+
+    log.info("主动断开对讲")
+    speech_off(true, false)
+    return true
+end
+
+return extalk

+ 199 - 46
module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/main.lua

@@ -1,60 +1,213 @@
-
 --[[
-@module  main
-@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
-@version 001.000.000
-@date    2025.07.11
-@author  李源龙
-@usage
-本demo演示的功能为:
-使用Air780EHV核心板外接AirAudio_1000,通过airtalk库,通过MQTT实现对讲功能,对讲网页地址:https://airtalk.openluat.com/
+    演示airtalk基本功能
+    按一次boot,开始1对1对讲,再按一次boot,结束对讲
+    按一次powerkey,开始1对多对讲,再按一次powerkey或者boot,结束对讲
 ]]
 
---[[
-必须定义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 = "airtalk"
-VERSION = "001.000.000"
+PROJECT = "airtalk_demo"
+VERSION = "1.0.2"
+PRODUCT_KEY = "29uptfBkJMwFC7x7QeW10UPO3LecPYFu" -- 到 iot.openluat.com 创建项目,获取正确的项目id
+
+-- 引入必要模块
+
+local extalk = require "extalk"
+local exaudio = require "exaudio"
+
+-- 配置日志格式
+log.style(1)
+
+-- 常量定义
+local USER_TASK_NAME = "user_task"
+local MSG_KEY_PRESS = 12  -- 按键消息
+
+-- 全局状态变量
+local g_dev_list = nil    -- 设备列表
+local g_speech_active = false  -- 对讲状态标记
+
+-- 音频初始化参数
+local audio_setup_param = {
+    model = "es8311",       -- 音频编解码类型,可填入"es8311","es8211"
+    i2c_id = 0,             -- i2c_id,可填入0,1 并使用pins工具配置对应的管脚
+    pa_ctrl = gpio.AUDIOPA_EN,          -- 音频放大器电源控制管脚
+    dac_ctrl = 20,         -- 音频编解码芯片电源控制管脚    
+}
+
+-- 联系人列表回调函数
+local function contact_list_callback(dev_list)
+    g_dev_list = dev_list
+    if dev_list and #dev_list > 0 then
+        log.info("联系人列表更新:")
+        for i = 1, #dev_list do
+            log.info(string.format("  %d. ID: %s, 名称: %s", 
+                i, dev_list[i]["id"], dev_list[i]["name"] or "未知"))
+        end
+    else
+        log.info("联系人列表为空")
+    end
+end
+
+-- 对讲状态回调函数
+local function speech_state_callback(event_table)
+    if not event_table then return end
+    
+    if event_table.state == extalk.START then
+        log.info("对讲开始,可以说话了")
+        g_speech_active = true
+    elseif event_table.state == extalk.STOP then
+        log.info("对讲结束")
+        g_speech_active = false
+    elseif event_table.state == extalk.UNRESPONSIVE then
+        log.info("对端未响应")
+        g_speech_active = false
+    elseif event_table.state == extalk.ONE_ON_ONE then
+        g_speech_active = true
+        
+        local dev_name = "未知设备"
+        if g_dev_list then
+            for i = 1, #g_dev_list do
+                if g_dev_list[i]["id"] == event_table.id then
+                    dev_name = g_dev_list[i]["name"] or "未知设备"
+                    break
+                end
+            end
+        end
+        log.info(string.format("%s 来电", dev_name))
+    elseif event_table.state == extalk.BROADCAST then
+        g_speech_active = true
+        
+        local dev_name = "未知设备"
+        if g_dev_list then
+            for i = 1, #g_dev_list do
+                if g_dev_list[i]["id"] == event_table.id then
+                    dev_name = g_dev_list[i]["name"] or "未知设备"
+                    break
+                end
+            end
+        end
+        log.info(string.format("%s 开始广播", dev_name))
+    end
+end
+
+-- extalk配置参数
+local extalk_configs = {
+    key = PRODUCT_KEY,
+    heart_break_time = 120,  -- 心跳间隔(单位秒)
+    contact_list_cbfnc = contact_list_callback,
+    state_cbfnc = speech_state_callback,
+}
+
+-- 按键回调函数 - Boot键
+local function boot_key_callback()
+    sys.sendMsg(USER_TASK_NAME, MSG_KEY_PRESS, false)  -- false表示Boot键
+end
+
+-- 按键回调函数 - Power键
+local function power_key_callback()
+    sys.sendMsg(USER_TASK_NAME, MSG_KEY_PRESS, true)   -- true表示Power键
+end
+
+-- 初始化按键
+local function init_buttons()
+    -- 配置Boot键 (GPIO0)
+    gpio.setup(0, boot_key_callback, gpio.PULLDOWN, gpio.RISING)
+    gpio.debounce(0, 200, 1)  -- 200ms去抖
+    
+    -- 配置Power键
+    gpio.setup(gpio.PWR_KEY, power_key_callback, gpio.PULLUP, gpio.FALLING)
+    gpio.debounce(gpio.PWR_KEY, 200, 1)  -- 200ms去抖
+end
+
+-- 查找第一个可用的对端设备ID
+local function find_first_remote_device()
+    if not g_dev_list or #g_dev_list == 0 then
+        log.warn("没有找到可用的设备")
+        return nil
+    end
+    
+    local local_id = mobile.imei()
+    for i = 1, #g_dev_list do
+        local dev_id = g_dev_list[i]["id"]
+        if dev_id and dev_id ~= local_id then
+            return dev_id
+        end
+    end
+    
+    log.warn("没有找到其他可用设备")
+    return nil
+end
 
--- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
--- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
--- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
--- 启动errDump日志存储并且上传功能,600秒上传一次
--- if errDump then
---     errDump.config(true, 600)
--- end
+-- 处理按键消息
+local function handle_key_press(is_power_key)
+    if g_speech_active then
+        -- 当前正在对讲,按任何键都结束对讲
+        log.info("结束当前对讲")
+        extalk.stop()
+        g_speech_active = false
+    else
+        -- 当前未在对讲,根据按键类型开始不同对讲
+        if is_power_key then
+            -- Power键:开始一对多广播
+            log.info("开始一对多广播")
+            extalk.start()  -- 不带参数表示广播
+        else
+            -- Boot键:开始一对一对讲
+            log.info("开始一对一对讲")
+            local remote_id = find_first_remote_device()
+            if remote_id then
+                extalk.start(remote_id)
+            else
+                log.error("无法开始一对一对讲,没有找到可用设备")
+            end
+        end
+    end
+end
 
 
--- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
--- 可以使用合宙的iot.openluat.com平台进行远程升级
--- 也可以使用客户自己搭建的平台进行远程升级
--- 远程升级的详细用法,可以参考fota的demo进行使用
 
+-- 用户主任务
+local function user_main_task()
+    -- 初始化音频
+    local audio_init_ok = exaudio.setup(audio_setup_param)
+    if not audio_init_ok then
+        log.error("音频初始化失败")
+        return
+    end
+    log.info("音频初始化成功")
+    
+    -- 初始化extalk
+    local extalk_init_ok = extalk.setup(extalk_configs)
+    if not extalk_init_ok then
+        log.error("extalk初始化失败")
+        return
+    end
+    log.info("extalk初始化成功")
+    
+    -- 等待按键消息并处理
+    while true do
+        local msg = sys.waitMsg(USER_TASK_NAME, MSG_KEY_PRESS)
+        if msg and msg[1] == MSG_KEY_PRESS then
+            handle_key_press(msg[2])  -- msg[2]区分是Power键(true)还是Boot键(false)
+        end
+    end
+end
 
--- 启动一个循环定时器
--- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
--- 方便分析内存使用是否有异常
--- sys.timerLoopStart(function()
---     log.info("mem.lua", rtos.meminfo())
---     log.info("mem.sys", rtos.meminfo("sys"))
--- end, 3000)
+-- 初始化按键
+init_buttons()
 
-talk=require "talk"
+-- 启动用户任务
+sys.taskInitEx(user_main_task, USER_TASK_NAME)
 
---运行talk.run(),启动talk
+-- 内存监控任务
 sys.taskInit(function()
-    log.info("start")
-    talk.run()
+    while true do
+        sys.wait(60000)  -- 每分钟检查一次
+        log.info("系统状态监控:")
+        log.info("  时间:", os.time())
+        log.info("  Lua内存:", rtos.meminfo("lua"))
+        log.info("  系统内存:", rtos.meminfo("sys"))
+        log.info("  PSRAM内存:", rtos.meminfo("psram"))
+    end
 end)
 
--- 用户代码已结束---------------------------------------------
--- 结尾总是这一句
+-- 启动系统
 sys.run()
--- sys.run()之后后面不要加任何语句!!!!!

+ 0 - 128
module/Air780EHM_Air780EHV_Air780EGH/demo/airtalk/talk.lua

@@ -1,128 +0,0 @@
---[[
-@module  talk
-@summary airtalk测试功能模块
-@version 001.000.000
-@date    2025.07.11
-@author  李源龙
-@usage
-使用Air780EHV核心板外接AirAudio_1000,连接MQTT服务器,订阅对应的主题和平台进行对讲功能,还支持使用powerkey按键进行对讲功能:
-Air780EHM            AirAudio_1000
-GND(任意)            GND
-VDD_EXT              VCC
-19/GPIO22            PA_EN
-5/SPK+               SPK+
-4/SPK-               SPK-
-]]
-local talk = {}
-
-local run_state = false
-local airaudio  = require "airaudio"
-local input_key = false
-
-
--- 初始化fskv
---speech_topic是topic,自己设定,需要和平台的topic一致,mqtt_host是mqtt服务器地址,mqtt_port是mqtt服务器端口,
---mqtt_isssl是是否使用ssl连接,client_id是mqtt客户端id,user_name是mqtt用户名,password是mqtt密码
-local speech_topic = "12345678910"
-local mqtt_host = "lbsmqtt.openluat.com"
-local mqtt_port = 1886
-local mqtt_isssl = false
-local client_id = nil
-local user_name = "mqtt_hz_test_1"
-local password = "Ck8WpNCp"
-local mqttc = nil
-local message = ""
-local event = ""
-local talk_state = ""
-
-local function airtalk_event_cb(event, param)
-    log.info("talk event", event, param)
-    event  = event
-end
-
-
--- MQTT回调函数
-local function mqtt_cb(mqtt_client, event, data, payload)
-    log.info("mqtt", "event", event, mqtt_client, data, payload)
-    -- 连接成功时订阅主题
-end
-
-
---初始化airtalk,连接MQTT
-local function init_talk()
-    log.info("init_call")
-    --初始化codec
-    airaudio.init() 
-    client_id = mobile.imei()
-
-    mqttc = mqtt.create(nil, mqtt_host, mqtt_port, mqtt_isssl, {rxSize = 4096})
-    airtalk.config(airtalk.PROTOCOL_DEMO_MQTT_8K, mqttc, 200) -- 缓冲至少200ms播放
-    airtalk.on(airtalk_event_cb)
-    airtalk.start(client_id, speech_topic)
-
-    mqttc:auth(client_id,user_name,password) -- client_id必填,其余选填
-    mqttc:keepalive(240) -- 默认值240s
-    mqttc:autoreconn(true, 3000) -- 自动重连机制
-    mqttc:debug(false)
-    mqttc:on(mqtt_cb)
-
-    -- mqttc自动处理重连, 除非自行关闭
-    mqttc:connect()
-
-end
-
--- 重新初始化对讲函数
-local function reinit_talk()
-    log.info("talk", "重新初始化对讲")
-    
-    -- 安全停止对讲
-    if airtalk and airtalk.stop then
-        airtalk.stop()
-    end
-    if mqttc then
-        mqttc:close()
-    end
-    
-    -- 重新初始化对讲
-    sys.taskInit(init_talk)
-end
-
---初始化airtalk
-function talk.run()
-    log.info("talk.run")
-    -- lcd.setFont(lcd.font_opposansm12_chinese)
-    run_state = true
-    init_talk()
-    speech_topic = fskv.get("talk_channel")
-    log.info("get  speech_topic",speech_topic)
-end
-
---停止语音采集
-local function stop_talk()
-    talk_state = "语音停止采集"
-    airtalk.uplink(false)
-    log.info("STATE", talk_state)
-end
-
---开启语音采集
-local function start_talk()
-    talk_state = "语音采集上传中"
-    airtalk.uplink(true)
-    log.info("STATE", talk_state)
-end
-
---设置防抖
-gpio.debounce(gpio.PWR_KEY,1000)
-gpio.setup(gpio.PWR_KEY,function(val)
-    if val == 1 then
-        log.info("talk", "暂停",val)
-        stop_talk()
-    else
-        log.info("talk2", "录音上传",val)
-        start_talk()
-
-    end
-end,gpio.PULLUP)
-
-
-return talk

BIN
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/10.amr


+ 541 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/exaudio.lua

@@ -0,0 +1,541 @@
+--[[
+@module exaudio
+@summary exaudio扩展库
+@version 1.1
+@date    2025.09.01
+@author  梁健
+@usage
+]]
+local exaudio = {}
+
+-- 常量定义
+local I2S_ID = 0
+local I2S_MODE = 0          -- 0:主机 1:从机
+local I2S_SAMPLE_RATE = 16000
+local I2S_CHANNEL_FORMAT = i2s.MONO_R  
+local I2S_COMM_FORMAT = i2s.MODE_LSB   -- 可选MODE_I2S, MODE_LSB, MODE_MSB
+local I2S_CHANNEL_BITS = 16
+local MULTIMEDIA_ID = 0
+local EX_MSG_PLAY_DONE = "playDone"
+local ES8311_ADDR = 0x18    -- 7位地址
+local CHIP_ID_REG = 0x00    -- 芯片ID寄存器地址
+
+-- 模块常量
+exaudio.PLAY_DONE = 1         --   音频播放完毕的事件之一
+exaudio.RECORD_DONE = 1       --   音频录音完毕的事件之一  
+exaudio.AMR_NB = 0
+exaudio.AMR_WB = 1
+exaudio.PCM_8000 = 2
+exaudio.PCM_16000 = 3 
+exaudio.PCM_24000 = 4
+exaudio.PCM_32000 = 5
+
+
+-- 默认配置参数
+local audio_setup_param = {
+    model = "es8311",         -- dac类型: "es8311","es8211"
+    i2c_id = 0,               -- i2c_id: 0,1
+    pa_ctrl = 0,              -- 音频放大器电源控制管脚
+    dac_ctrl = 0,             -- 音频编解码芯片电源控制管脚
+    dac_delay = 3,            -- DAC启动前冗余时间(100ms)
+    pa_delay = 100,           -- DAC启动后延迟打开PA的时间(ms)
+    dac_time_delay = 600,     -- 播放完毕后PA与DAC关闭间隔(ms)
+    bits_per_sample = 16,     -- 采样位数
+    pa_on_level = 1           -- PA打开电平 1:高 0:低        
+}
+
+local audio_play_param = {
+    type = 0,                 -- 0:文件 1:TTS 2:流式
+    content = nil,            -- 播放内容
+    cbfnc = nil,              -- 播放完毕回调
+    priority = 0,             -- 优先级(数值越大越高)
+    sampling_rate = 16000,    -- 采样率(仅流式)
+    sampling_depth = 16,      -- 采样位深(仅流式)
+    signed_or_unsigned = true -- PCM是否有符号(仅流式)
+}
+
+local audio_record_param = {
+    format = 0,               -- 录制格式,支持exaudio.AMR_NB,exaudio.AMR_WB,exaudio.PCM_8000,exaudio.PCM_16000,exaudio.PCM_24000,exaudio.PCM_32000
+    time = 5,                 -- 录制时间(秒)
+    path = nil,               -- 文件路径或流式回调
+    cbfnc = nil               -- 录音完毕回调
+}
+
+-- 内部变量
+local pcm_buff0 = nil
+local pcm_buff1 = nil
+local voice_vol = 55
+local mic_vol = 80
+
+-- 定义全局队列表
+local audio_play_queue = {
+    data = {},       -- 存储字符串的数组
+    sequenceIndex = 1  -- 用于跟踪插入顺序的索引
+}
+
+-- 向队列中添加字符串(按调用顺序插入)
+local function audio_play_queue_push(str)
+    if type(str) == "string" then
+        -- 存储格式: {index = 顺序索引, value = 字符串值}
+        table.insert(audio_play_queue.data, {
+            index = audio_play_queue.sequenceIndex,
+            value = str
+        })
+        audio_play_queue.sequenceIndex = audio_play_queue.sequenceIndex + 1
+        return true
+    end
+    return false
+end
+
+-- 从队列中取出最早插入的字符串(按顺序取出)
+local function audio_play_queue_pop()
+    if #audio_play_queue.data > 0 then
+        -- 取出并移除第一个元素
+        local item = table.remove(audio_play_queue.data, 1)
+        return item.value  -- 返回值
+    end
+    return nil
+end
+-- 清空队列中所有数据
+function audio_queue_clear()
+    -- 清空数组
+    audio_play_queue.data = {}
+    -- 重置顺序索引
+    audio_play_queue.sequenceIndex = 1
+    return true
+end
+
+-- 工具函数:参数检查
+local function check_param(param, expected_type, name)
+    if type(param) ~= expected_type then
+        log.error(string.format("参数错误: %s 应为 %s 类型", name, expected_type))
+        return false
+    end
+    return true 
+end
+
+-- 音频回调处理
+local function audio_callback(id, event, point)
+    -- log.info("audio_callback", "event:", event, 
+    --         "MORE_DATA:", audio.MORE_DATA, 
+    --         "DONE:", audio.DONE,
+    --         "RECORD_DATA:", audio.RECORD_DATA,
+    --         "RECORD_DONE:", audio.RECORD_DONE)
+
+    if event == audio.MORE_DATA then
+        audio.write(MULTIMEDIA_ID,audio_play_queue_pop())
+    elseif event == audio.DONE then
+        if type(audio_play_param.cbfnc) == "function" then
+            audio_play_param.cbfnc(exaudio.PLAY_DONE)
+        end
+        audio_queue_clear()  -- 清空流式播放数据队列
+        sys.publish(EX_MSG_PLAY_DONE)
+        
+    elseif event == audio.RECORD_DATA then
+        if type(audio_record_param.path) == "function" then
+            local buff, len = point == 0 and pcm_buff0 or pcm_buff1,
+                             point == 0 and pcm_buff0:used() or pcm_buff1:used()
+            audio_record_param.path(buff, len)
+        end
+        
+    elseif event == audio.RECORD_DONE then
+        if type(audio_record_param.cbfnc) == "function" then
+            audio_record_param.cbfnc(exaudio.RECORD_DONE)
+        end
+    end
+end
+
+-- 读取ES8311芯片ID
+local function read_es8311_id()
+
+
+    -- 发送读取请求
+    local send_ok = i2c.send(audio_setup_param.i2c_id, ES8311_ADDR, CHIP_ID_REG)
+    if not send_ok then
+        log.error("发送芯片ID读取请求失败")
+        return false
+    end
+
+    -- 读取数据
+    local data = i2c.recv(audio_setup_param.i2c_id, ES8311_ADDR, 1)
+    if data and #data == 1 then
+        return true
+    end
+
+    log.error("读取ES8311芯片ID失败")
+    return false
+end
+
+-- 音频硬件初始化
+local function audio_setup()
+    -- I2C配置
+    if not i2c.setup(audio_setup_param.i2c_id, i2c.FAST) then
+        log.error("I2C初始化失败")
+        return false
+    end
+    -- 初始化I2S
+    local result, data = i2s.setup(
+        I2S_ID, 
+        I2S_MODE, 
+        I2S_SAMPLE_RATE, 
+        audio_setup_param.bits_per_sample, 
+        I2S_CHANNEL_FORMAT, 
+        I2S_COMM_FORMAT,
+        I2S_CHANNEL_BITS
+    )
+
+    if not result then
+        log.error("I2S设置失败")
+        return false
+    end
+    -- 配置音频通道
+    audio.config(
+        MULTIMEDIA_ID, 
+        audio_setup_param.pa_ctrl, 
+        audio_setup_param.pa_on_level, 
+        audio_setup_param.dac_delay, 
+        audio_setup_param.pa_delay, 
+        audio_setup_param.dac_ctrl, 
+        1,  -- power_on_level
+        audio_setup_param.dac_time_delay
+    )
+    -- 设置总线
+    audio.setBus(
+        MULTIMEDIA_ID, 
+        audio.BUS_I2S,
+        {
+            chip = audio_setup_param.model,
+            i2cid = audio_setup_param.i2c_id,
+            i2sid = I2S_ID,
+            voltage = audio.VOLTAGE_1800
+        }
+    )
+
+  
+    -- 设置音量
+    audio.vol(MULTIMEDIA_ID, voice_vol)
+    audio.micVol(MULTIMEDIA_ID, mic_vol)
+    audio.pm(MULTIMEDIA_ID, audio.RESUME)
+    
+    -- 检查芯片连接
+    if audio_setup_param.model == "es8311" and not read_es8311_id() then
+        log.error("ES8311通讯失败,请检查硬件")
+        return false
+    end
+
+    -- 注册回调
+    audio.on(MULTIMEDIA_ID, audio_callback)
+    return true
+end
+
+-- 模块接口:初始化
+function exaudio.setup(audioConfigs)
+    -- 检查必要参数
+    if not  audio  then
+        log.error("不支持audio 库,请选择支持audio 的core")
+        return false
+    end
+    if not audioConfigs or type(audioConfigs) ~= "table" then
+        log.error("配置参数必须为table类型")
+        return false
+    end
+    -- 检查codec型号
+    if not audioConfigs.model or 
+       (audioConfigs.model ~= "es8311" and audioConfigs.model ~= "es8211") then
+        log.error("请指定正确的codec型号(es8311或es8211)")
+        return false
+    end
+    audio_setup_param.model = audioConfigs.model
+    -- 针对ES8311的特殊检查
+    if audioConfigs.model == "es8311" then
+        if not check_param(audioConfigs.i2c_id, "number", "i2c_id") then
+            return false
+        end
+        audio_setup_param.i2c_id = audioConfigs.i2c_id
+    end
+
+    -- 检查功率放大器控制管脚
+    if audioConfigs.pa_ctrl == nil then
+        log.warn("pa_ctrl(功率放大器控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
+    end
+    audio_setup_param.pa_ctrl = audioConfigs.pa_ctrl
+
+    -- 检查功率放大器控制管脚
+    if audioConfigs.dac_ctrl == nil then
+        log.warn("dac_ctrl(音频编解码控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
+    end
+    audio_setup_param.dac_ctrl = audioConfigs.dac_ctrl
+
+
+    -- 处理可选参数
+    local optional_params = {
+        {name = "dac_delay", type = "number"},
+        {name = "pa_delay", type = "number"},
+        {name = "dac_time_delay", type = "number"},
+        {name = "bits_per_sample", type = "number"},
+        {name = "pa_on_level", type = "number"}
+    }
+
+    for _, param in ipairs(optional_params) do
+        if audioConfigs[param.name] ~= nil then
+            if check_param(audioConfigs[param.name], param.type, param.name) then
+                audio_setup_param[param.name] = audioConfigs[param.name]
+            else
+                return false
+            end
+        end
+    end
+
+    -- 确保采样位数有默认值
+    audio_setup_param.bits_per_sample = audio_setup_param.bits_per_sample or 16
+    return audio_setup()
+end
+
+-- 模块接口:开始播放
+function exaudio.play_start(playConfigs)
+    if not playConfigs or type(playConfigs) ~= "table" then
+        log.error("播放配置必须为table类型")
+        return false
+    end
+
+    -- 检查播放类型
+    if not check_param(playConfigs.type, "number", "type") then
+        log.error("type必须为数值(0:文件,1:TTS,2:流式)")
+        return false
+    end
+    audio_play_param.type = playConfigs.type
+
+    -- 处理优先级
+    if playConfigs.priority ~= nil then
+        if check_param(playConfigs.priority, "number", "priority") then
+            if playConfigs.priority > audio_play_param.priority then
+                log.error("是否完成播放",audio.isEnd(MULTIMEDIA_ID))
+                if not audio.isEnd(MULTIMEDIA_ID) then
+                    if audio.play(MULTIMEDIA_ID) ~= true then
+                        return false
+                    end
+                    sys.waitUntil(EX_MSG_PLAY_DONE)
+                end
+                audio_play_param.priority = playConfigs.priority
+            end
+        else
+            return false
+        end
+    end
+
+    -- 处理不同播放类型
+    local play_type = audio_play_param.type
+    if play_type == 0 then  -- 文件播放
+        if not playConfigs.content then
+            log.error("文件播放需要指定content(文件路径或路径表)")
+            return false
+        end
+
+        local content_type = type(playConfigs.content)
+        if content_type == "table" then
+            for _, path in ipairs(playConfigs.content) do
+                if type(path) ~= "string" then
+                    log.error("播放列表元素必须为字符串路径")
+                    return false
+                end
+            end
+        elseif content_type ~= "string" then
+            log.error("文件播放content必须为字符串或路径表")
+            return false
+        end
+
+        audio_play_param.content = playConfigs.content
+        if audio.play(MULTIMEDIA_ID, audio_play_param.content) ~= true then
+            return false
+        end
+
+    elseif play_type == 1 then  -- TTS播放
+        if not audio.tts then
+            log.error("本固件不支持TTS,请更换支持TTS 的固件")
+            return false
+        end
+        if not check_param(playConfigs.content, "string", "content") then
+            log.error("TTS播放content必须为字符串")
+            return false
+        end
+        audio_play_param.content = playConfigs.content
+        if audio.tts(MULTIMEDIA_ID, audio_play_param.content)  ~= true  then
+            return false
+        end
+
+    elseif play_type == 2 then  -- 流式播放
+        if not check_param(playConfigs.sampling_rate, "number", "sampling_rate") then
+            return false
+        end
+        if not check_param(playConfigs.sampling_depth, "number", "sampling_depth") then
+            return false
+        end
+
+        audio_play_param.content = playConfigs.content
+        audio_play_param.sampling_rate = playConfigs.sampling_rate
+        audio_play_param.sampling_depth = playConfigs.sampling_depth
+        
+        if playConfigs.signed_or_unsigned ~= nil then
+            audio_play_param.signed_or_unsigned = playConfigs.signed_or_unsigned
+        end
+
+        audio.start(
+            MULTIMEDIA_ID, 
+            audio.PCM, 
+            1, 
+            playConfigs.sampling_rate, 
+            playConfigs.sampling_depth, 
+            audio_play_param.signed_or_unsigned
+        )
+        -- 发送初始数据
+        if audio.write(MULTIMEDIA_ID, string.rep("\0", 512)) ~= true then
+            return false
+        end
+    end
+
+    -- 处理回调函数
+    if playConfigs.cbfnc ~= nil then
+        if check_param(playConfigs.cbfnc, "function", "cbfnc") then
+            audio_play_param.cbfnc = playConfigs.cbfnc
+        else
+            return false
+        end
+    else
+        audio_play_param.cbfnc = nil
+    end
+    return true
+end
+
+-- 模块接口:流式播放数据写入
+function exaudio.play_stream_write(data)
+    audio_play_queue_push(data)
+    return true
+end
+
+-- 模块接口:停止播放
+function exaudio.play_stop()
+    return audio.play(MULTIMEDIA_ID)
+end
+
+-- 模块接口:检查播放是否结束
+function exaudio.is_end()
+    return audio.isEnd(MULTIMEDIA_ID)
+end
+
+-- 模块接口:获取错误信息
+function exaudio.get_error()
+    return audio.getError(MULTIMEDIA_ID)
+end
+
+-- 模块接口:开始录音
+function exaudio.record_start(recodConfigs)
+    if not recodConfigs or type(recodConfigs) ~= "table" then
+        log.error("录音配置必须为table类型")
+        return false
+    end
+    -- 检查录音格式
+    if recodConfigs.format == nil or type(recodConfigs.format) ~= "number" or recodConfigs.format > 5 then
+        log.error("请指定正确的录音格式")
+        return false
+    end
+    audio_record_param.format = recodConfigs.format
+
+    -- 处理录音时间
+    if recodConfigs.time ~= nil then
+        if check_param(recodConfigs.time, "number", "time") then
+            audio_record_param.time = recodConfigs.time
+        else
+            return false
+        end
+    else
+        audio_record_param.time = 0
+    end
+
+    -- 处理存储路径/回调
+    if not recodConfigs.path then
+        log.error("必须指定录音路径或流式回调函数")
+        return false
+    end
+    audio_record_param.path = recodConfigs.path
+
+    -- 转换录音格式
+    local recod_format, amr_quailty
+    if audio_record_param.format == exaudio.AMR_NB then
+        recod_format = audio.AMR_NB
+        amr_quailty = 7
+    elseif audio_record_param.format == exaudio.AMR_WB then
+        recod_format = audio.AMR_WB
+        amr_quailty = 8
+    elseif audio_record_param.format == exaudio.PCM_8000 then
+        recod_format = 8000
+    elseif audio_record_param.format == exaudio.PCM_16000 then
+        recod_format = 16000
+    elseif audio_record_param.format == exaudio.PCM_24000 then
+        recod_format = 24000
+    elseif audio_record_param.format == exaudio.PCM_32000 then
+        recod_format = 32000
+    end
+
+    -- 处理回调函数
+    if recodConfigs.cbfnc ~= nil then
+        if check_param(recodConfigs.cbfnc, "function", "cbfnc") then
+            audio_record_param.cbfnc = recodConfigs.cbfnc
+        else
+            return false
+        end
+    else
+        audio_record_param.cbfnc = nil
+    end
+    -- 开始录音
+    local path_type = type(audio_record_param.path)
+    if path_type == "string" then
+        return audio.record(
+            MULTIMEDIA_ID, 
+            recod_format, 
+            audio_record_param.time, 
+            amr_quailty, 
+            audio_record_param.path
+        )
+    elseif path_type == "function" then
+        -- 初始化缓冲区
+        if not pcm_buff0 or not pcm_buff1 then
+            pcm_buff0 = zbuff.create(16000)
+            pcm_buff1 = zbuff.create(16000)
+        end
+        return audio.record(
+            MULTIMEDIA_ID, 
+            recod_format, 
+            audio_record_param.time, 
+            amr_quailty, 
+            nil, 
+            3,
+            pcm_buff0,
+            pcm_buff1
+        )
+    end
+    log.error("录音路径必须为字符串或函数")
+    return false
+end
+
+-- 模块接口:停止录音
+function exaudio.record_stop()
+    return audio.recordStop(MULTIMEDIA_ID)
+end
+
+-- 模块接口:设置音量
+function exaudio.vol(play_volume)
+    if check_param(play_volume, "number", "音量值") then
+        return audio.vol(MULTIMEDIA_ID, play_volume)
+    end
+    return false
+end
+
+-- 模块接口:设置麦克风音量
+function exaudio.mic_vol(record_volume)
+    if check_param(record_volume, "number", "麦克风音量值") then
+        return audio.micVol(MULTIMEDIA_ID, record_volume)  
+    end
+    return false
+end
+
+return exaudio

+ 62 - 192
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/main.lua

@@ -1,202 +1,72 @@
--- LuaTools需要PROJECT和VERSION这两个信息
+
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.09.08
+@author  梁健
+@usage
+本demo演示的核心功能为:
+1、play_file.lua: 播放音频文件,可支持wav,amr,mp3 格式音频
+
+2、play_tts: 支持文字转普通话输出需要固件支持
+
+3、play_stream: 流式播放音频,仅支持PCM 格式,可以将音频推流到云端,用来对接大模型或者流式录音的应用。
+
+4、record_file: 录音到文件,仅支持PCM 格式
+
+5、record_stream:  流式录音,仅支持PCM,可以将音频流不断的拉取,可用来对接大模型
+
+6、1.mp3: 用于测试本地mp3文件播放
+
+7、test.pcm: 用于测试pcm 流式播放(实际可以云端下载)
+
+
+更多说明参考本目录下的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进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+
+--[[
+本demo可直接在Air8000整机开发板上运行
+]]
+
 PROJECT = "audio"
 VERSION = "1.0.0"
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
 
---[[]
-运行环境:Air780EHV核心板+AirAUDIO_1000配件板
-最后修改时间:2025-6-17
-使用了如下IO口:
-[5, "spk+", " PIN5脚, 用于喇叭正极"],
-[6, "spk-", " PIN6脚, 用于喇叭负极"],
-[20, "AudioPA_EN", " PIN20脚, 用于PA使能脚"],
-3.3V
-GND
-SD卡的使用IO口:
-[83, "SPI0CS", " PIN83脚, 用于SD卡片选脚"],
-[84, "SPI0MISO," PIN84脚, 用于SD卡数据脚"],
-[85, "SPI0MOSI", " PIN85脚, 用于SD卡数据脚"],
-[86, "SPI0CLK", " PIN86脚, 用于SD卡时钟脚"],
-[24, "VDD_EXT", " PIN24脚, 用于给SD卡供电脚"],
-GND
-执行逻辑为:
-设置i2s和音频参数,写了四种操作方式
-1、播放脚本区的文件
-2、挂载SD卡,通过HTTP下载到SD卡,播放SD卡中的文件
-3、通过HTTP下载到文件区,播放文件区中的文件
-4、通过HTTP下载到内存里面,播放内存中的文件
-]]
--- sys库是标配
-_G.sys = require("sys")
-_G.sysplus = require("sysplus")
-
---目前代码里面提供了4种播放方式,对应mode的值,默认是1
---1:播放脚本区的文件
---2:挂载SD卡,通过HTTP下载到SD卡,播放SD卡中的文件
---3:通过HTTP下载到文件区,播放文件区中的文件
---4:通过HTTP下载到内存里面,播放内存中的文件
-local mode = 1
-local i2c_id = 0 -- i2c_id 0
-
-local pa_pin = gpio.AUDIOPA_EN -- 喇叭pa功放脚
-local power_pin = 20 -- es8311电源脚
-
-local i2s_id = 0 -- i2s_id 0
-local i2s_mode = 0 -- i2s模式 0 主机 1 从机
-local i2s_sample_rate = 16000 -- 采样率
-local i2s_bits_per_sample = 16 -- 数据位数
-local i2s_channel_format = i2s.MONO_R -- 声道, 0 左声道, 1 右声道, 2 立体声
-local i2s_communication_format = i2s.MODE_LSB -- 格式, 可选MODE_I2S, MODE_LSB, MODE_MSB
-local i2s_channel_bits = 16 -- 声道的BCLK数量
-
-local multimedia_id = 0 -- 音频通道 0
-local pa_on_level = 1 -- PA打开电平 1 高电平 0 低电平
-local power_delay = 3 -- 在DAC启动前插入的冗余时间,单位100ms
-local pa_delay = 100 -- 在DAC启动后,延迟多长时间打开PA,单位1ms
-local power_on_level = 1 -- 电源控制IO的电平,默认拉高
-local power_time_delay = 100 -- 音频播放完毕时,PA与DAC关闭的时间间隔,单位1ms
-
-local voice_vol = 50 -- 喇叭音量
-local mic_vol = 80 -- 麦克风音量
-
-gpio.setup(power_pin, 1, gpio.PULLUP) -- 设置ES83111电源脚
-gpio.setup(pa_pin, 1, gpio.PULLUP) -- 设置功放PA脚
-
-function audio_setup()
-    log.info("audio_setup")
-    sys.wait(200)
-
-    i2c.setup(i2c_id, i2c.FAST)
-    i2s.setup(
-        i2s_id,
-        i2s_mode,
-        i2s_sample_rate,
-        i2s_bits_per_sample,
-        i2s_channel_format,
-        i2s_communication_format,
-        i2s_channel_bits
-    )
-
-    audio.config(multimedia_id, pa_pin, pa_on_level, power_delay, pa_delay, power_pin, power_on_level, power_time_delay)
-    audio.setBus(
-        multimedia_id,
-        audio.BUS_I2S,
-        {
-            chip = "es8311",
-            i2cid = i2c_id,
-            i2sid = i2s_id
-        }
-    ) -- 通道0的硬件输出通道设置为I2S
-    audio.vol(multimedia_id, voice_vol)
-    audio.micVol(multimedia_id, mic_vol)
-    sys.publish("AUDIO_READY")
-end
 
--- 配置好audio外设
-sys.taskInit(audio_setup)
-
-local taskName = "task_audio"
-
-local MSG_MD = "moreData" -- 播放缓存有空余
-local MSG_PD = "playDone" -- 播放完成所有数据
-
-audio.on(0,function(id, event)
-        -- 使用play来播放文件时只有播放完成回调
-        local succ, stop, file_cnt = audio.getError(0)
-        if not succ then
-            if stop then
-                log.info("用户停止播放")
-            else
-                log.info("第", file_cnt, "个文件解码失败")
-            end
-        end
-        sysplus.sendMsg(taskName, MSG_PD)
-    end
-)
-
-local function audio_task()
-    local result
-    sys.waitUntil("AUDIO_READY")
-    result = sys.waitUntil("IP_READY", 15000)
-    if result then
-        if mode == 1 then
-        elseif mode == 2 then
-            --以下内容为SD卡挂载的方式,如果有需要用到SD卡可以打开下面的挂载流程
-            -- -- 此为spi方式
-            local spi_id, pin_cs = 0, 8
-            -- 仅SPI方式需要自行初始化spi, sdio不需要
-            spi.setup(spi_id, nil, 0, 0, pin_cs, 400 * 1000)
-            gpio.setup(pin_cs, 1)
-            fatfs.mount(fatfs.SPI, "/sd", spi_id, pin_cs, 24 * 1000 * 1000)
-            local code, headers, body =
-                http.request("GET", "http://airtest.openluat.com:2900/download/1.mp3", nil, nil, {dst = "/sd/1.mp3"}).wait()
-            --存到sd卡里面
-            log.info("下载完成", code, headers, body)
-        elseif mode == 3 then
-            local code, headers, body =
-                http.request("GET", "http://airtest.openluat.com:2900/download/1.mp3", nil, nil, {dst = "/1.mp3"}).wait()
-            --存到本地文件区,适用于多次播放
-            log.info("下载完成", code, headers, body)
-        elseif mode == 4 then
-            local code, headers, body =
-                http.request("GET", "http://airtest.openluat.com:2900/download/1.mp3", nil, nil, {dst = "/ram/1.mp3"}).wait()
-            --存到内存里面,适用于下载播放一次,然后不需要再次播放或者不重启的时候可以继续播放,重启后需要重新下载
-            log.info("下载完成", code, headers, body)
-        end
-    end
-
-    while true do
-        log.info("开始播放")
-        if result then
-            if mode == 1 then
-                result = audio.play(0, "/luadb/1.mp3") -- 播放本地脚本区文件
-            elseif mode == 2 then
-                result = audio.play(0, "/sd/1.mp3") -- 播放sd卡里面文件
-            elseif mode == 3 then
-                result = audio.play(0, "/1.mp3") --播放HTTP下载的文件
-            elseif mode == 4 then
-                result = audio.play(0, "/ram/1.mp3") --播放HTTP下载到内存的文件
-            end
-        end
-        if result then
-            --等待音频通道的回调消息,或者切换歌曲的消息
-            while true do
-                msg = sysplus.waitMsg(taskName, nil)
-                if type(msg) == "table" then
-                    if msg[1] == MSG_PD then
-                        log.info("播放结束")
-                        break
-                    end
-                else
-                    log.error(type(msg), msg)
-                end
-            end
-        else
-            log.debug("解码失败!")
-            sys.wait(1000)
-        end
-        if not audio.isEnd(0) then
-            log.info("手动关闭")
-            audio.playStop(0)
-        end
-        if audio.pm then
-            audio.pm(0, audio.STANDBY) --PM模式 待机模式,PA断电,codec待机状态,系统不能进低功耗状态,如果PA不可控,codec进入静音模式
-        end
-        -- audio.pm(0,audio.SHUTDOWN)	--低功耗可以选择SHUTDOWN或者POWEROFF,如果codec无法断电用SHUTDOWN
-        log.info("mem", "sys", rtos.meminfo("sys"))
-        log.info("mem", "lua", rtos.meminfo("lua"))
-        sys.wait(1000)
-    end
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
 end
 
-sys.timerLoopStart(
-    function()
-        log.info("mem.lua", rtos.meminfo())
-        log.info("mem.sys", rtos.meminfo("sys"))
-    end,
-    3000
-)
 
-sysplus.taskInitEx(audio_task, taskName)
+-- require "play_file"     --   播放音频文件,可支持wav,amr,mp3 格式音频
+-- require "play_tts"      -- 支持文字转普通话输出需要固件支持
+-- require "play_stream"        -- 流式播放音频,仅支持PCM 格式,可以将音频推流到云端,用来对接大模型或者流式录音的应用。
+require "record_file"        -- 录音到文件
+-- require "record_stream"        -- 流式录音   
+
+-- 音频对内存影响较大,不断的打印内存,用于判断是否异常
+sys.timerLoopStart(function()
+    log.info("mem.lua", rtos.meminfo())
+    log.info("mem.sys", rtos.meminfo("sys"))
+ end, 3000)
 
 -- 用户代码已结束---------------------------------------------
 -- 结尾总是这一句

+ 101 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/play_file.lua

@@ -0,0 +1,101 @@
+--[[
+@module  play_file
+@summary 播放文件
+@version 1.0
+@date    2025.09.08
+@author  梁健
+@usage
+
+本文件为播放文件的应用功能模块,核心业务逻辑为:
+1、自动播放一个1.mp3音乐,
+2、点powerkey 按键进行音频切换,点击boot 按键停止音频播放
+3、点击boot 按键停止音频播放
+本文件没有对外接口,直接在main.lua中require "play_file"就可以加载运行;
+]]
+
+exaudio = require("exaudio")
+local taskName = "task_audio"
+
+
+-- 音频初始化设置参数,exaudio.setup 传入参数
+local audio_setup_param ={
+    model= "es8311",          -- 音频编解码类型,可填入"es8311","es8211"
+    i2c_id = 0,          -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚
+    pa_ctrl = 162,         -- 音频放大器电源控制管脚
+    dac_ctrl = 164,        --  音频编解码芯片电源控制管脚    
+}
+
+--  播放结束回调
+local function play_end(event)
+    if event == exaudio.PLAY_DONE then
+        log.info("播放完成",exaudio.is_end())
+    end
+end
+
+--  音频播放的配置
+local audio_play_param ={
+    type= 0,                -- 播放类型,有0,播放文件,1.播放tts 2. 流式播放
+                            -- 如果是播放文件,支持mp3,amr,wav格式
+                            -- 如果是tts,内容格式见:https://wiki.luatos.com/chips/air780e/tts.html?highlight=tts
+                            -- 流式播放,仅支持PCM 格式音频,如果是流式播放,则sampling_rate, sampling_depth,signed_or_unsigned 必填写
+    content = "/luadb/1.mp3",          -- 如果播放类型为0时,则填入string 是播放单个音频文件,如果是表则是播放多段音频文件。
+    cbfnc = play_end,            -- 播放完毕回调函数
+}
+
+
+---------------------------------
+---通过BOOT 按键进行播放停止操作---
+---------------------------------
+local function stop_audio()
+    log.info("停止播放")
+    sys.sendMsg(taskName, MSG_KEY_PRESS, "STOP_AUDIO")
+end
+--按下boot 停止播放
+gpio.setup(0, stop_audio, gpio.PULLDOWN, gpio.RISING)
+gpio.debounce(0, 200, 1) -- 防抖,防止频繁触发
+
+---------------------------------
+---通过POWERKEY按键进行音频切换---
+---------------------------------
+
+local function next_audio()
+    log.info("切换播放")
+    sys.sendMsg(taskName, MSG_KEY_PRESS, "NEXT_AUDIO")
+end
+
+--按下powerkey 打断播放,播放优先级更高的音频
+gpio.setup(gpio.PWR_KEY, next_audio, gpio.PULLUP, gpio.FALLING)
+gpio.debounce(gpio.PWR_KEY, 200, 1) -- 防抖,防止频繁触发
+
+
+---------------------------------
+-----主task,处理播放音频---------
+---------------------------------
+
+
+local index_number = 1
+local audio_path = nil
+local function audio_task()
+    log.info("开始播放音频文件")
+    if exaudio.setup(audio_setup_param) then
+        exaudio.play_start(audio_play_param) -- 仅仅支持task 中运行
+        while true do
+            local msg = sys.waitMsg(taskName, MSG_KEY_PRESS)   -- 等待按键触发
+            if msg[2] ==  "NEXT_AUDIO" then  
+                
+                if index_number %2 == 0 then     --  切换音频路径
+                    audio_path = "/luadb/1.mp3"
+                else
+                    audio_path = "/luadb/10.amr"
+                end
+
+                exaudio.play_start({type= 0, content = audio_path,cbfnc = play_end,priority = index_number})
+                index_number= index_number +1 
+            elseif msg[2] ==  "STOP_AUDIO" then
+                exaudio.play_stop()
+            end 
+        end
+    end
+    
+end
+sys.taskInitEx(audio_task, taskName)

+ 108 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/play_stream.lua

@@ -0,0 +1,108 @@
+--[[
+@module  play_stream
+@summary 流式播放
+@version 1.0
+@date    2025.09.08
+@author  梁健
+@usage
+
+本文件为流式播放应用功能模块,核心业务逻辑为:
+1、创建一个播放流式音频task(task_audio)
+2、创建一个模拟获取流式音频的task(audio_get_data)
+3、此task通过流式传输不断向exaudio.play_stream_write填入播放的音频
+4、播放task 不断播放传入流式音频
+5、使用powerkey 按键进行音量减小,点击boot 按键进行音量增加
+本文件没有对外接口,直接在main.lua中require "play_stream"就可以加载运行;
+]]
+
+exaudio = require("exaudio")
+
+
+-- 音频初始化设置参数,exaudio.setup 传入参数
+local audio_setup_param ={
+    model= "es8311",          -- dac类型,可填入"es8311","es8211"
+    i2c_id = 0,          -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚
+    pa_ctrl = 162,         -- 音频放大器电源控制管脚
+    dac_ctrl = 164,        --  音频编解码芯片电源控制管脚 
+}
+
+-- 播放完成回调
+local function play_end(event)
+    if event == exaudio.PLAY_DONE then
+        log.info("播放完成",exaudio.is_end())
+
+    end
+end 
+
+-- 流式播放音频播放的配置
+local audio_play_param ={
+    type= 2,                -- 播放类型,有0,播放文件,1.播放tts 2. 流式播放
+                            -- 如果是播放文件,支持mp3,amr,wav格式
+                            -- 如果是tts,内容格式见:https://wiki.luatos.com/chips/air780e/tts.html?highlight=tts
+                            -- 流式播放,仅支持PCM 格式音频,如果是流式播放,则sampling_rate, sampling_depth,signed_or_unsigned 必填写
+    cbfnc = play_end,            -- 播放完毕回调函数
+    sampling_rate = 16000,  -- 采样率,仅为流式播放起作用
+    sampling_depth =  16,   -- 采样位位深,仅流式播放的时候才有作用
+    signed_or_unsigned = true  -- PCM 的数据是否有符号,仅为流式播放起作用
+}
+
+---------------------------------
+---通过BOOT 按键增大音量---
+---------------------------------
+local volume_number = 50 
+local function add_volume()
+    volume_number = volume_number + 20
+    log.info("增大音量",volume_number)
+    exaudio.vol(volume_number)
+end
+
+gpio.setup(0, add_volume, gpio.PULLDOWN, gpio.RISING)
+gpio.debounce(0, 200, 1)
+
+---------------------------------
+---通过POWERKEY按键减小音量-------
+---------------------------------
+
+local function down_volume()
+    volume_number = volume_number - 15
+    log.info("减小音量",volume_number)
+    exaudio.vol(volume_number)
+end
+
+gpio.setup(gpio.PWR_KEY, down_volume, gpio.PULLUP, gpio.FALLING)
+gpio.debounce(gpio.PWR_KEY, 200, 1)   -- 防抖,防止频繁触发
+
+
+---------------------------------
+---------模拟获取音频task---------
+---------------------------------
+local function audio_get_data()
+    log.info("开始流式获取音频数据")
+    local file = io.open("/luadb/test.pcm", "rb")   -- 模拟流式播放音源,实际的音频数据来源也可以来自网络或者本地存储
+    while true do
+        local read_data = file:read(4096)  --  读取文件,模拟流式音频源,需要1024 的倍数
+        if read_data  == nil then
+            file:close()                -- 模拟音频获取完毕,关闭音频文件
+            break
+        end
+        exaudio.play_stream_write(read_data)  -- 流式写入音频数据
+        sys.wait(100)                   -- 写数据需要留出事件给其他task 运行代码
+    end
+end
+
+sys.taskInitEx(audio_get_data, "audio_get_data")
+
+
+---------------------------------
+------------通过主task------------
+---------------------------------
+local taskName = "task_audio"
+local function audio_task()
+    log.info("开始流式播报")
+    if exaudio.setup(audio_setup_param) then
+        exaudio.play_start(audio_play_param)
+        log.info("播放状态",exaudio.is_end())
+    end
+end
+
+sys.taskInitEx(audio_task, taskName)

+ 104 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/play_tts.lua

@@ -0,0 +1,104 @@
+--[[
+@module  play_tts
+@summary 文字转语音
+@version 1.0
+@date    2025.09.08
+@author  梁健
+@usage
+
+本文件为流式播放应用功能模块,核心业务逻辑为:
+1、播放一个TTS
+2、点powerkey 按键进行tts 的音色切换
+3、点击boot 按键停止音频播放
+本文件没有对外接口,直接在main.lua中require "play_tts"就可以加载运行;
+]]
+exaudio = require("exaudio")
+local taskName = "task_audio"
+
+-- 音频初始化设置参数,exaudio.setup 传入参数
+local audio_setup_param ={
+    model= "es8311",          -- 音频编解码类型,可填入"es8311","es8211"
+    i2c_id = 0,          -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚
+    pa_ctrl = 162,         -- 音频放大器电源控制管脚
+    dac_ctrl = 164,        --  音频编解码芯片电源控制管脚    
+}
+
+local function play_end(event)
+    if event == exaudio.PLAY_DONE then
+        log.info("播放完成",exaudio.is_end())
+        exaudio.play_stop()
+    end
+end 
+
+local audio_play_param ={
+    type= 1,                -- 播放类型,有0,播放文件,1.播放tts 2. 流式播放
+                            -- 如果是播放文件,支持mp3,amr,wav格式
+                            -- 如果是tts,内容格式见:https://docs.openluat.com/air780epm/common/tts/
+                            -- 流式播放,仅支持PCM 格式音频,如果是流式播放,则sampling_rate, sampling_depth,signed_or_unsigned 必填写
+    content = "支付宝到账,1千万元",          -- 如果播放类型为0时,则填入string 是播放单个音频文件,如果是表则是播放多段音频文件。
+    cbfnc = play_end,            -- 播放完毕回调函数
+}
+
+
+---------------------------------
+---通过BOOT 按键进行播放停止操作---
+---------------------------------
+local function stop_audio()
+    log.info("停止播放")
+    sys.sendMsg(taskName, MSG_KEY_PRESS, "STOP_AUDIO")
+end
+--按下boot 停止播放
+gpio.setup(0, stop_audio, gpio.PULLDOWN, gpio.RISING)
+gpio.debounce(0, 200, 1)
+
+---------------------------------
+---通过POWERKEY按键进行音频切换---
+---------------------------------
+
+local function next_audio()
+    log.info("切换播放")
+    sys.sendMsg(taskName, MSG_KEY_PRESS, "NEXT_AUDIO")
+end
+
+--按下powerkey 打断播放,播放优先级更高的音频
+gpio.setup(gpio.PWR_KEY, next_audio, gpio.PULLUP, gpio.FALLING)
+gpio.debounce(gpio.PWR_KEY, 200, 1)  -- 防抖,防止频繁触发
+
+
+---------------------------------------------------------------------------------------------------
+---------------主task------------------------------------------------------------------------------
+--- 关于TTS 音色设置请见: https://docs.openluat.com/air780epm/common/tts/
+---------------------------------------------------------------------------------------------------
+
+
+local index_number = 1
+local audio_path = nil
+local function audio_task()
+    log.info("开始播放TTS")
+    if exaudio.setup(audio_setup_param) then
+        exaudio.play_start(audio_play_param) -- 仅仅支持task 中运行
+        while true do
+            local msg = sys.waitMsg(taskName, MSG_KEY_PRESS)   -- 等待按键触发
+            if msg[2] ==  "NEXT_AUDIO" then      
+                if index_number %5 == 0 then     --  切换播报音色
+                    audio_path = "[m51]支付宝到账,1千万元"   -- 许久
+                elseif index_number %5 == 1 then
+                    audio_path = "[m52]支付宝到账,1千万元"   -- 许多
+                elseif index_number %5 == 2 then
+                    audio_path = "[m53]支付宝到账,1千万元"   -- 晓萍
+                elseif index_number %5 == 3 then                    
+                    audio_path = "[m54]支付宝到账,1千万元"   -- 唐老鸭
+                elseif index_number %5 == 4 then                    
+                    audio_path = "[m55]支付宝到账,1千万元"   -- 许宝宝 
+                end
+
+                exaudio.play_start({type= 1, content = audio_path,cbfnc = play_end,priority = index_number})
+                index_number= index_number +1 
+            elseif msg[2] ==  "STOP_AUDIO" then
+                exaudio.play_stop()
+            end 
+        end
+    end
+    
+end
+sys.taskInitEx(audio_task, taskName)

+ 106 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/readme.md

@@ -0,0 +1,106 @@
+## 功能模块介绍
+
+1、main.lua:主程序入口;
+
+2、play_file.lua: 播放音频文件,可支持wav,amr,mp3 格式音频
+
+3、play_tts: 支持文字转普通话输出需要固件支持
+
+4、play_stream: 流式播放音频,仅支持PCM 格式,可以将音频推流到云端,用来对接大模型或者流式录音的应用。
+
+5、record_file: 录音到文件,仅支持PCM 格式
+
+6、record_stream:  流式录音,仅支持PCM,可以将音频流不断的拉取,可用来对接大模型
+
+7、1.mp3: 用于测试本地mp3文件播放
+
+8、test.pcm: 用于测试pcm 流式播放(实际可以云端下载)
+
+
+
+
+
+## 常量的介绍
+
+1、exaudio.PLAY_DONE : 当播放音频结束时,会在回调函数返回播放完成的时间
+
+2、exaudio.RECORD_DONE : 当录音结束时,会在回调函数返回播放完成的时间
+
+3、exaudio.AMR_NB : 仅录音时有用,表示使用AMR_NB 方式录音
+
+4、exaudio.AMR_WB : 仅录音时有用,表示使用AMR_WB 方式录音
+
+5、exaudio.PCM_8000/exaudio.PCM_16000/exaudio.PCM_24000/exaudio.PCM_32000 :  仅录音时有用,表示使用8000/16000/24000/32000/秒 的速度对音频进行采样
+
+
+## 演示功能概述
+
+1、play_flie.lua 自动播放一个1.mp3音乐,点powerkey 按键进行音频切换,点击boot 按键停止音频播放
+
+2、play_tts.lua 播放一个TTS,点powerkey 按键进行tts 的音色切换,点击boot 按键停止音频播放
+
+3、play_stream.lua 流式播放PCM,使用test.pcm 模拟音频来源,通过流式传输不断填入播放的音频,使用powerkey 按键进行音量减小,点击boot 按键进行音量增加
+
+4、record_file.lua 录音到文件,演示了pcm 录音到文件,使用powerkey 按键进行录音音量减小,点击boot 按键进行录音音量增加
+
+5、record_stream.lua 流式录音(仅支持PCM),不断输出录音的数据地址和录音长度,供给应用层调用
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air8000/luatos/app/image/netdrv_multi.jpg)
+
+1、Air8000开发板一块
+
+2、喇叭一个
+
+2、插入喇叭到开发板中
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8000 V2014版本固件](https://docs.openluat.com/air8000/luatos/firmware/)(理论上,2025年7月26日之后发布的固件都可以)
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、demo脚本代码main.lua中,按照自己的需求选择对应的功能
+
+- 如果需要测试播放音频文件,则选择play_file 文件
+
+- 如果需要测试播放tts,则选择play_tts 文件
+
+- 如果需要测试流式播放音频,则选择play_stream 文件
+
+- 如果需要测试录音频到文件,则选择record_file 文件
+
+- 如果需要测试流式录音,则选择record_stream 文件
+
+
+3、Luatools烧录内核固件和修改后的demo脚本代码
+
+4、烧录成功后,自动开机运行,如果出现以下日志,播放或者或者录音完成
+
+``` lua
+I/user.播放完成 true
+I/user.录音完成 
+I/user.录音后文件大小 
+```
+
+5、 在测试播放音频文件的时候,点powerkey 按键进行音频切换,切换内容是MP3,AMR格式,切换是通过播放优先级进行区分的,注意音频格式仅仅支持:MP3,WAV,AMR,点击boot 按键停止音频播放
+
+6、 在测试播放TTS的时候,点powerkey 按键进行TTS 音色切换,点击boot 按键停止音频播放,注意:仅支持中文TTS。
+
+
+7、在进行流式播放测试的时候,使用test.pcm 模拟音频来源,通过流式传输不断填入播放的音频,使用powerkey 按键进行音量减小,点击boot 按键进行音量增加,注意流式播放目前仅支持PCM 格式音频,可选择不同的采样率,以及位深
+
+8、在测试录音到文件(仅支持PCM),演示了pcm 录音到文件,使用powerkey 按键进行录音音量减小,点击boot 按键进行录音音量增加
+
+9、在测试流式录音(仅支持PCM),不断输出录音的数据地址和录音长度,供给应用层调用
+
+
+

+ 84 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/record_file.lua

@@ -0,0 +1,84 @@
+--[[
+@module  record_file
+@summary 录音到文件
+@version 1.0
+@date    2025.09.08
+@author  梁健
+@usage
+
+录音到文件,核心业务逻辑为:
+1、主程序录音到/record.amr 文件
+2、使用powerkey 按键进行录音音量减小
+3、点击boot 按键进行录音音量增加
+本文件没有对外接口,直接在main.lua中require "record_file"就可以加载运行;
+]]
+exaudio = require("exaudio")
+
+-- 音频初始化设置参数,exaudio.setup 传入参数
+local audio_setup_param ={
+    model= "es8311",          -- dac类型,可填入"es8311","es8211"
+    i2c_id = 0,          -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚
+    pa_ctrl = 162,         -- 音频放大器电源控制管脚
+    dac_ctrl = 164,        --  音频编解码芯片电源控制管脚
+    bits_per_sample = 16  -- 录音的位深,可选8,16,24 位,但是当选择音频格式为AMR_NB,自动调节为8位,当音频格式为AMR_WB,自动调节为16位
+}
+local recordPath = "/record.amr"
+
+-- 录音完成回调
+local function record_end(event)
+    if event == exaudio.RECORD_DONE then
+        log.info("录音后文件大小",io.fileSize(recordPath))
+    end
+end 
+
+-- 录音配置参数,exaudio.record_start 的入参
+local audio_record_param ={
+    format= exaudio.PCM_32000,    -- 录制格式,有exaudio.AMR_NB,exaudio.AMR_WB,exaudio.PCM_8000,exaudio.PCM_16000,exaudio.PCM_24000,exaudio.PCM_32000
+                                  -- 如果选择exaudio.AMR_WB,则需要固件支持volte 功能
+    time = 5,               -- 录制时间,单位(秒)
+    path = recordPath,             -- 如果填入的是字符串,则表示是文件路径,录音会传输到这个路径里
+                                   -- 如果填入的是函数,则表示是流式录音,录音的数据会传输到此函数内,返回的是zbuf地址,以及数据长度
+                                   -- 如果是流式录音,则仅支持PCM 格式 
+    cbfnc = record_end,            -- 录音完毕回调函数
+}
+
+
+---------------------------------
+---通过BOOT 按键增大录音---
+---------------------------------
+local volume_number = 50 
+local function add_volume()
+    volume_number = volume_number + 20
+    log.info("增大录音音量",volume_number)
+    exaudio.mic_vol(volume_number)
+end
+--按下boot 停止播放
+gpio.setup(0, add_volume, gpio.PULLDOWN, gpio.RISING)
+gpio.debounce(0, 200, 1)
+
+---------------------------------
+---通过POWERKEY按键减小录音-------
+---------------------------------
+
+local function down_volume()
+    volume_number = volume_number - 15
+    log.info("减小录音音量",volume_number)
+    exaudio.mic_vol(volume_number)
+end
+
+gpio.setup(gpio.PWR_KEY, down_volume, gpio.PULLUP, gpio.FALLING)
+gpio.debounce(gpio.PWR_KEY, 200, 1)   -- 防抖,防止频繁触发
+
+---------------------------------
+---音频 task 初始化函数-----------
+---------------------------------
+local taskName = "task_audio"
+local function audio_task()
+    sys.wait(100)
+    log.info("开始录制音频到文件")
+    if exaudio.setup(audio_setup_param) then
+        exaudio.record_start(audio_record_param)
+    end
+end
+
+sys.taskInitEx(audio_task, taskName)

+ 55 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/record_stream.lua

@@ -0,0 +1,55 @@
+--[[
+@module  record_stream
+@summary 流式录音
+@version 1.0
+@date    2025.09.08
+@author  梁健
+@usage
+
+流式录音(仅支持PCM),核心业务逻辑为:
+1、主程序录音进行流式录音
+2、录音过程中不断的进行recode_data_callback回调,回调内容为音频流的地址和长度
+本文件没有对外接口,直接在main.lua中require "record_stream"就可以加载运行;
+]]
+exaudio = require("exaudio")
+
+-- 音频初始化设置参数,exaudio.setup 传入参数
+local audio_setup_param ={
+    model= "es8311",          -- dac类型,可填入"es8311","es8211"
+    i2c_id = 0,          -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚
+    pa_ctrl = 162,         -- 音频放大器电源控制管脚
+    dac_ctrl = 164,        --  音频编解码芯片电源控制管脚
+    bits_per_sample = 16  -- 录音的位深,可选8,16,24 位,但是当选择音频格式为AMR_NB,自动调节为8位,当音频格式为AMR_WB,自动调节为16位
+}
+
+-- 录音的数据流回调函数,注意不可以在回调函数中加入耗时和延迟的操作
+local function  recode_data_callback(addr,data_len)
+    log.info("收到音频流,地址为:",addr,"有效数据长度为:",data_len)
+end
+local function record_end(event)
+    if event == exaudio.RECORD_DONE then
+        log.info("录音完成")
+    end
+end 
+
+-- 录音配置参数,exaudio.record_start 的入参
+local audio_record_param ={
+    format= exaudio.PCM_16000,    -- 录制格式,有exaudio.AMR_NB,exaudio.AMR_WB,exaudio.PCM_8000,exaudio.PCM_16000,exaudio.PCM_24000,exaudio.PCM_32000
+    time = 5,               -- 录制时间,单位(秒)
+    path = recode_data_callback,             -- 如果填入的是字符串,则表示是文件路径,录音会传输到这个路径里
+                                        -- 如果填入的是函数,则表示是流式录音,录音的数据会传输到此函数内,返回的是zbuf地址,以及数据长度
+                                        -- 如果是流式录音,则仅支持PCM 格式 
+    cbfnc = record_end,            -- 录音完毕回调函数
+}
+
+
+
+local taskName = "task_audio"
+local function audio_task()
+    log.info("开始流式录制音频到文件")
+    if exaudio.setup(audio_setup_param) then
+        exaudio.record_start(audio_record_param)
+    end
+end
+
+sys.taskInitEx(audio_task, taskName)

BIN
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/test.pcm


BIN
module/Air780EHM_Air780EHV_Air780EGH/demo/audio/音频硬件框架.png


+ 119 - 0
module/Air780EPM/demo/accessory_board/AirSHT30_1000/AirSHT30_1000.lua

@@ -0,0 +1,119 @@
+--本文件中的主机是指I2C主机,具体指Air780EPM
+--本文件中的从机是指I2C从机,具体指AirSHT30_1000配件板上的sht30温湿度传感器芯片
+
+local AirSHT30_1000 = 
+{
+    -- i2c_id:主机的i2c id;
+}   
+
+-- 从机地址为0x44
+local slave_addr = 0x44
+
+--电平设为3.3v
+pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
+--设置gpio2输出,给camera_sda、camera_scl引脚提供上拉
+gpio.setup(2, 1)
+
+-- 计算数据表data中所有数据元素的crc8校验值
+local function crc8(data)
+    local crc = 0xFF
+    for i = 1, #data do
+        crc = bit.bxor(crc, data[i])
+        for j = 1, 8 do
+            crc = crc * 2
+            if crc >= 0x100 then
+                crc = bit.band(bit.bxor(crc, 0x31), 0xff)
+            end
+        end
+    end
+    return crc
+end
+
+
+--打开AirSHT30_1000;
+
+--i2c_id:number类型;
+--        主机使用的I2C ID,用来控制AirSHT30_1000;
+--        取值范围:仅支持0和1;
+--        如果没有传入此参数,则默认为0;
+
+--返回值:成功返回true,失败返回false
+function AirSHT30_1000.open(i2c_id)
+    --如果i2c_id为nil,则赋值为默认值0
+    if i2c_id==nil then i2c_id=0 end
+
+    --检查参数的合法性
+    if not (i2c_id == 0 or i2c_id == 1) then
+        log.error("AirSHT30_1000.open", "invalid i2c_id", i2c_id)
+        return false
+    end
+
+    AirSHT30_1000.i2c_id = i2c_id
+    
+    --初始化I2C
+    if i2c.setup(i2c_id, i2c.FAST) ~= 1 then
+        log.error("AirSHT30_1000.open", "i2c.setup error", i2c_id)
+        return false
+    end
+
+    return true
+end
+
+--读取温湿度数据;
+
+--返回值:失败返回false;
+--       成功返回两个值,第一个为摄氏温度值(number类型,例如23.6表示23.6摄氏度),第二个为百分比湿度值(number类型,例如67表示67%的湿度)
+function AirSHT30_1000.read()
+
+    -- 发送启动测量命令(高精度)
+    i2c.send(AirSHT30_1000.i2c_id, slave_addr, {0x24, 0x00})
+        
+    -- 等待测量完成(SHT30高精度测量需~15ms)
+    sys.wait(20)
+    
+    -- 读取6字节数据(温度高/低 + CRC,湿度高/低 + CRC)
+    local data = i2c.recv(AirSHT30_1000.i2c_id, slave_addr, 6)
+
+    -- 如果没有读取到6字节数据
+    if type(data)~="string" or data:len()~=6 then
+        log.error("AirSHT30_1000.read", "i2c.recv error")
+        return false
+    end
+
+    -- log.info("AirSHT30_1000.read", data:toHex())
+
+    --如果校验值正确
+    if crc8({data:byte(1), data:byte(2)}) == data:byte(3) and crc8({data:byte(4), data:byte(5)}) == data:byte(6) then 
+        -- 提取原始温度值
+        local temp_raw = (data:byte(1) << 8) | data:byte(2)
+        -- 提取原始湿度值
+        local hum_raw = (data:byte(4) << 8) | data:byte(5)
+        
+        -- 转换为实际值(根据SHT30数据手册公式)
+        local temprature = (-45 + 175 * temp_raw / 65535.0)
+        local humidity = (100 * hum_raw / 65535.0)
+        
+        -- 打印输出结果(保留2位小数)
+        -- log.info("AirSHT30_1000.read", "temprature", string.format("%.2f ℃", temprature))
+        -- log.info("AirSHT30_1000.read", "temprature", string.format("%.2f %%RH", humidity))
+
+        return temprature, humidity
+    else
+        log.error("AirSHT30_1000.read", "crc error", i2c_id)
+        return false
+    end
+end
+
+
+--关闭AirSHT30_1000
+
+--返回值:成功返回true,失败返回false
+function AirSHT30_1000.close()
+    --close接口没有返回值,理论上不会关闭失败
+    i2c.close(AirSHT30_1000.i2c_id)
+
+    return true
+end
+
+
+return AirSHT30_1000

+ 63 - 0
module/Air780EPM/demo/accessory_board/AirSHT30_1000/main.lua

@@ -0,0 +1,63 @@
+--[[
+必须定义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进行远程升级,根据自己项目的需求,自定义格式即可
+
+AirSHT30_1000是合宙设计生产的一款I2C接口的SHT30温湿度传感器配件板;
+本demo演示的核心功能为:
+Air8101核心板+AirSHT30_1000配件板,每隔1秒读取1次温湿度数据;
+更多说明参考本目录下的readme.md文件
+]]
+PROJECT = "AirSHT30_1000"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+--  end, 3000)
+
+ -- 加载sht30应用模块
+ require "sht30_app"
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 55 - 0
module/Air780EPM/demo/accessory_board/AirSHT30_1000/readme.md

@@ -0,0 +1,55 @@
+
+## 演示功能概述
+
+AirSHT30_1000是合宙设计生产的一款I2C接口的SHT30温湿度传感器配件板;
+
+本demo演示的核心功能为:
+
+Air780EPM开发板+AirSHT30_1000配件板,每隔1秒读取1次温湿度数据;
+
+
+## 核心板+配件板资料
+
+[Air780EPM开发板+配件板相关资料](https://docs.openluat.com/air780epm/product/shouce/)
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/accessory/AirSHT30_1000/image/connect_780epm.png)
+
+1、Air780EPM开发板
+
+2、AirSHT30_1000配件板
+
+3、母对母的杜邦线4根
+
+| Air780EPM开发板 | AirSHT30_1000配件板|
+| ------------ | ------------------ |
+|     3V3(VDD_EXT)     |         3V3        |
+|     GND   |         GND        |
+|  I2C1_SDA(CAMERA_SDA)  |         SDA        |
+| I2C1_SCL(CAMERA_SCL) |         SCL        |
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air780EPM最新版本的内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/)
+
+
+## 演示操作步骤
+
+1、搭建好演示硬件环境
+
+2、不需要修改demo脚本代码
+
+3、Luatools烧录内核固件和demo脚本代码
+
+4、烧录成功后,自动开机运行
+
+5、通过观察Luatools的运行日志,每隔1秒出现一次类似于下面的打印,就表示测试正常
+
+``` lua
+[2025-09-18 15:29:03.155][000000001.262] I/user.read_sht30_task_func temprature 27.43 ℃
+[2025-09-18 15:29:03.159][000000001.262] I/user.read_sht30_task_func humidity 57.58 %RH

+ 34 - 0
module/Air780EPM/demo/accessory_board/AirSHT30_1000/sht30_app.lua

@@ -0,0 +1,34 @@
+--加载AirSHT30_1000驱动文件
+local air_sht30 = require "AirSHT30_1000"
+
+
+--每隔1秒读取一次温湿度数据
+local function read_sht30_task_func()
+    --打开sht30硬件
+    air_sht30.open(1)
+
+    while true do
+        --读取温湿度数据
+        local temprature, humidity = air_sht30.read()
+
+        --读取成功
+        if temprature then
+            -- 打印输出结果(保留2位小数)
+            log.info("read_sht30_task_func", "temprature", string.format("%.2f ℃", temprature))
+            log.info("read_sht30_task_func", "humidity", string.format("%.2f %%RH", humidity))
+        --读取失败
+        else
+            log.error("read_sht30_task_func", "read error")
+        end
+
+        --等待1秒
+        sys.wait(1000)
+    end
+
+    --关闭sht30硬件
+    air_sht30.close()
+end
+
+--创建一个task,并且运行task的主函数read_sht30_task_func
+sys.taskInit(read_sht30_task_func)
+

+ 2 - 2
module/Air780EPM/demo/network_routing/4g_out_ethernet_in_wifi_in/main.lua → module/Air780EPM/demo/network_routing/4g_out_ethernet_in/main.lua

@@ -6,7 +6,7 @@
 @author  魏健强
 @usage
 本demo演示的核心功能为:
-1.设置多网融合功能,4G提供网络供wifi和以太网设备上网
+1.设置多网融合功能,4G提供网络供以太网设备上网
 更多说明参考本目录下的readme.md文件
 ]]
 --[[
@@ -19,7 +19,7 @@ VERSION:项目版本号,ascii string类型
             因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
         如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
 ]]
-PROJECT = "4g_out_ethernet_in_wifi_in"
+PROJECT = "4g_out_ethernet_in"
 VERSION = "001.000.000"
 
 

+ 0 - 0
module/Air780EPM/demo/network_routing/4g_out_ethernet_in_wifi_in/netif_app.lua → module/Air780EPM/demo/network_routing/4g_out_ethernet_in/netif_app.lua


+ 3 - 7
module/Air780EPM/demo/network_routing/4g_out_ethernet_in_wifi_in/readme.md → module/Air780EPM/demo/network_routing/4g_out_ethernet_in/readme.md

@@ -41,17 +41,13 @@
 
 1、搭建好硬件环境,按接线图连接硬件,
 
-2、按需修改WiFi热点配置(在netif_app.lua中):
-ssid = "AP热点名称"
-password = "AP热点密码"
+2、烧录内核固件和本项目的Lua脚本:main.lua:主程序入口,netif_app.lua:网络管理模块
 
-4、烧录内核固件和本项目的Lua脚本:main.lua:主程序入口,netif_app.lua:网络管理模块
-
-5、启动设备,观察日志输出:
+3、启动设备,观察日志输出:
 
 ``` lua
 [INFO] exnetif setproxy success
 [INFO] http执行结果 200 ... 
 ```
 
-6、其他设备通过以太网接入780EPM,其他设备都能正常上网,则表示验证成功。
+4、其他设备通过以太网接入780EPM,其他设备都能正常上网,则表示验证成功。

+ 199 - 0
module/Air8000/demo/accessory_board/AIRSPINAND_1000/AIRSPINAND_1000.lua

@@ -0,0 +1,199 @@
+--[[
+@module  LITTLE_FLASh_NAND
+@summary LITTLE_FLASh_NAND测试功能模块
+@version 1.0
+@date    2025.9.05
+@author  马亚丹
+@usage
+本demo演示的功能为:使用Air8000核心板通过SPI库实现对 NAND Flash的操作,演示读数据写数据、删除数据等操作。
+以 Air8000核心板为例, 接线如下:
+Air8000       AirSPINAND_1000配件版
+GND(任意)          GND
+VDD_EXT            VCC
+GPIO12/SPI1_CS     CS,片选
+SPI1_SLK           CLK,时钟
+SPI1_MOSI          DI,主机输出,从机输入
+SPI1_MISO          DO,主机输入,从机输出
+--使用SPI1,硬件SPI CS接在gpio12上
+
+运行核心逻辑:
+1.以对象的方式配置参数,初始化启用SPI,返回SPI对象
+2.用SPI对象初始化flash设备,返回flash设备对象
+3.用lf库挂载flash设备对象为文件系统
+4.读取文件系统的信息,以确认内存足够用于文件操作
+5.操作文件读写,并验证写入一致性,追加文件等。
+
+]]
+
+-- SPI配置参数
+local SPI_ID = 1        -- SPI总线ID,根据实际情况修改
+local CS_PIN = 12       -- CS引脚,根据实际情况修改
+local CPHA = 0          -- 时钟相位
+local CPOL = 0          -- 时钟极性
+local data_Width = 8    -- 数据宽度(位)
+local bandrate = 2*1000*1000 -- 波特率(Hz),初始化为2MHz
+
+
+-- 1. 以对象方式设置并启用 SPI,返回设备对象
+local function spiDev_init_func()
+    log.info("lf_fs", "SPI_ID", SPI_ID, "CS_PIN", CS_PIN)
+
+    --以对象的方式初始化spi,高位在前,主模式,全双工模式
+    local spi_device = spi.deviceSetup(SPI_ID, CS_PIN, CPHA, CPOL, data_Width, bandrate, spi.MSB, 1, 0)
+
+    log.info("硬件spi", "初始化,波特率:", spi_device, bandrate)
+    if not spi_device then
+        log.error("SPI初始化", "失败")
+        return nil
+    end
+    log.info("SPI初始化", "成功,波特率:20MHz")
+    return spi_device
+end
+
+
+-- 2. 初始化Flash设备,返回设备对象
+local function init_flash_device(spi_device)
+    log.info("Flash初始化", "开始")
+    local flash_device = lf.init(spi_device)
+    if not flash_device then
+        log.error("Flash初始化", "失败")
+        return nil
+    end
+    log.info("Flash初始化", "成功,设备:", flash_device)
+    return flash_device
+end
+
+-- 3. 挂载文件系统
+local function mount_filesystem(flash_device, mount_point)
+    log.info("文件系统", "开始挂载:", mount_point)
+
+    -- 检查是否支持挂载功能
+    if not lf.mount then
+        log.error("文件系统", "lf模块不支持挂载功能")
+        return false
+    end
+
+    -- 尝试挂载
+    local mount_ok = lf.mount(flash_device, mount_point)
+    if not mount_ok then
+        log.warn("文件系统lf", "挂载失败,尝试重新挂载...")
+        mount_ok = lf.mount(flash_device, mount_point)
+        if not mount_ok then
+            log.error("文件系统", "仍挂载失败")
+            return false
+        end
+    end
+
+    log.info("文件系统", "挂载成功:", mount_point)
+    return true
+end
+
+-- 4. 打印文件系统信息
+local function print_filesystem_info(mount_point)
+    log.info("文件系统信息", "开始查询:", mount_point)
+
+    -- 获取文件系统详细信息,总块数/已用块数等
+    local ok, total_blocks, used_blocks, block_size, fs_type = fs.fsstat(mount_point)
+    if ok then
+        log.info("  总block数:", total_blocks)
+        log.info("  已用block数:", used_blocks)
+        log.info("  block大小:", block_size, "字节")
+        log.info("  文件系统类型:", fs_type)
+    else
+        log.warn("  无法获取详细信息")
+    end
+end
+
+-- 5. 执行文件操作测试
+local function test_file_operations(mount_point)
+    log.info("文件操作测试", "开始")
+
+    -- 测试写入文件
+    local test_file = mount_point .. "/test.txt"
+    local f, err = io.open(test_file, "w")
+    if not f then
+        log.error("  写入失败", test_file, "错误:", err)
+        return false
+    end
+    local write_data = "当前时间: " .. os.date()
+    f:write(write_data)
+    f:close()
+    log.info("  写入成功", test_file, "内容:", write_data)
+
+    -- 测试读取文件
+    local read_data, read_err = io.readFile(test_file)
+    if not read_data then
+        log.error("  读取失败", test_file, "错误:", read_err)
+        return false
+    end
+    log.info("  读取成功", test_file, "内容:", read_data)
+
+    -- 验证内容一致性
+    if read_data ~= write_data then
+        log.warn("  内容不一致", "写入:", write_data, "读取:", read_data)
+    end
+
+    -- 测试文件追加
+    local append_file = mount_point .. "/append.txt"
+    os.remove(append_file) -- 清除旧文件
+    io.writeFile(append_file, "LuatOS 测试") -- 初始写入
+
+    local f_append, append_err = io.open(append_file, "a+")
+    if not f_append then
+        log.error("  追加失败", append_file, "错误:", append_err)
+        return false
+    end
+    local append_data = " - 追加时间: " .. os.date()
+    f_append:write(append_data)
+    -- 执行完操作后,一定要关掉文件
+    f_append:close()
+
+    local final_data = io.readFile(append_file)
+    log.info("  追加后内容:", final_data)
+
+    log.info("文件操作测试", "完成")
+
+    return true
+end
+
+-- 7. 关闭SPI设备,成功返回0
+local function spi_close_func()    
+    log.info("关闭spi", spi_device:close())
+end
+
+-- 主任务函数:按流程调用各功能函数
+local function spinand_test_func()
+    --1.判断SPI初始化
+    spi_device = spiDev_init_func()
+    if not spi_device then
+        log.error("主流程", "SPI初始化失败,终止")
+        return
+    end
+
+    -- 流程2:初始化Flash设备
+    local flash_device = init_flash_device(spi_device)
+    if not flash_device then
+        log.error("主流程", "Flash初始化失败,终止")
+        return
+    end
+
+    -- 流程3:挂载文件系统
+    local mount_point = "/little_flash"
+    if not mount_filesystem(flash_device, mount_point) then
+        log.error("主流程", "文件系统挂载失败,终止")
+        return
+    end
+
+    -- 流程4:打印文件系统信息
+    print_filesystem_info(mount_point)
+
+    -- 流程5:执行文件操作测试
+    if not test_file_operations(mount_point) then
+        log.warn("主流程", "文件操作测试部分失败")
+    end
+
+     -- 6.关闭SPI设备
+    spi_close_func()
+end
+
+sys.taskInit(spinand_test_func)

+ 53 - 0
module/Air8000/demo/accessory_board/AIRSPINAND_1000/main.lua

@@ -0,0 +1,53 @@
+PROJECT = "Air8000_SPI_lf_NAND"
+VERSION = "001.000.000"
+
+
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
+end
+
+
+-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
+-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
+-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
+-- 启动errDump日志存储并且上传功能,600秒上传一次
+-- if errDump then
+--     errDump.config(true, 600)
+-- end
+
+
+-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
+-- 可以使用合宙的iot.openluat.com平台进行远程升级
+-- 也可以使用客户自己搭建的平台进行远程升级
+-- 远程升级的详细用法,可以参考fota的demo进行使用
+
+
+-- 启动一个循环定时器
+-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
+-- 方便分析内存使用是否有异常
+-- sys.timerLoopStart(function()
+--     log.info("mem.lua", rtos.meminfo())
+--     log.info("mem.sys", rtos.meminfo("sys"))
+-- end, 3000)
+
+
+
+-- 加载AIRSPINAND_1000功能模块
+require "AIRSPINAND_1000"
+
+
+
+
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

+ 109 - 0
module/Air8000/demo/accessory_board/AIRSPINAND_1000/readme.md

@@ -0,0 +1,109 @@
+## 功能模块介绍:
+
+1. main.lua:主程序入口
+
+2. AIRSPINAND_1000:通过littleFS文件系统,对flash模块以文件系统的方式进行读写数据操作,详细逻辑请看AIRSPINAND_1000.lua 文件
+
+## 演示功能概述:
+
+### AIRSPINAND_1000:
+
+1.以对象的方式配置参数,初始化启用 SPI,返回 SPI 对象
+
+2.用 SPI 对象初始化 flash 设备,返回 flash 设备对象
+
+3.用 lf 库挂载 flash 设备对象为文件系统
+
+4.读取文件系统的信息,以确认内存足够用于文件操作
+
+5.操作文件读写,并验证写入一致性,追加文件等。
+
+## 演示硬件环境:
+
+![](https://docs.openluat.com/accessory/AirSPINORFLASH_1000/image/spi1.jpg)
+
+![](https://docs.openluat.com/accessory/AirSPINAND_1000/image/nand.jpg)
+
+1. 合宙 Air8000 核心板一块
+
+2. 合宙 AIRSPINAND_1000 一块
+
+3. TYPE-C USB 数据线一根 ,Air8000 核心板和数据线的硬件接线方式为:
+- Air8000 核心板通过 TYPE-C USB 口供电;(外部供电/USB 供电 拨动开关 拨到 USB 供电一端)
+
+- TYPE-C USB 数据线直接插到开发板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
+4. 杜邦线 6 根
+
+    Air8000 核心板与 AIRSPINAND_1000 按以下方式接线:
+
+<table>
+<tr>
+<td>Air8000核心板<br/></td><td>AirSPINAND_1000配件版<br/></td></tr>
+<tr>
+<td>GND(任意)          <br/></td><td>GND<br/></td></tr>
+<tr>
+<td>VDD_EXT<br/></td><td>VCC<br/></td></tr>
+<tr>
+<td>GPIO12/<br/>SPI1_CS<br/></td><td>CS<br/></td></tr>
+<tr>
+<td>SPI1_SLK<br/></td><td>SCK<br/></td></tr>
+<tr>
+<td>SPI1_MOSI<br/></td><td>MOSI<br/></td></tr>
+<tr>
+<td>SPI1_MISO<br/></td><td>MISO<br/></td></tr>
+</table>
+
+## 演示软件环境:
+
+1. Luatools 下载调试工具
+
+2. 固件版本:LuatOS-SoC_V2014_Air8000_1,固件地址,如有最新固件请用最新 [https://docs.openluat.com/air8000/luatos/firmware/](https://docs.openluat.com/air8000/luatos/firmware/)
+
+3. pc 系统 win11(win10 及以上)
+
+## 演示核心步骤:
+
+1. 搭建好硬件环境
+
+2. main.lua 中加载需要用的功能模块,两个功能模块同时只能选择一个使用,另一个注释。
+
+3. Luatools 烧录内核固件和修改后的 demo 脚本代码
+
+4. 烧录成功后,代码会自动运行,查看打印日志,如果正常运行,会打印相关信息,spi 初始化,数据读写,文件操作等。
+
+5. AIRSPINAND_1000.lua 如下 log 显示:
+
+```bash
+[2025-09-18 14:50:09.757][000000000.358] I/user.main Air8000_SPI_lf_NAND 001.000.000
+[2025-09-18 14:50:09.763][000000000.368] I/user.lf_fs SPI_ID 1 CS_PIN 12
+[2025-09-18 14:50:09.771][000000000.368] SPI_HWInit 552:spi1 speed 2000000,1994805,154
+[2025-09-18 14:50:09.777][000000000.369] I/user.硬件spi 初始化,波特率: SPI*: 0C7F5B90 2000000
+[2025-09-18 14:50:09.788][000000000.369] I/user.SPI初始化 成功,波特率:20MHz
+[2025-09-18 14:50:09.794][000000000.369] I/user.Flash初始化 开始
+[2025-09-18 14:50:09.807][000000000.370] I/little_flash Welcome to use little flash V0.0.1 .
+[2025-09-18 14:50:09.815][000000000.370] I/little_flash Github Repositories https://github.com/Dozingfiretruck/little_flash .
+[2025-09-18 14:50:09.822][000000000.370] I/little_flash Gitee Repositories https://gitee.com/Dozingfiretruck/little_flash .
+[2025-09-18 14:50:09.831][000000000.371] I/little_flash SFDP header not found.
+[2025-09-18 14:50:09.838][000000000.371] I/little_flash JEDEC ID: manufacturer_id:0xEF device_id:0xAA21 
+[2025-09-18 14:50:09.847][000000000.371] I/little_flash little flash fonud flash W25N01GVZEIG
+[2025-09-18 14:50:09.853][000000000.421] I/user.Flash初始化 成功,设备: userdata: 0C0F9D7C
+[2025-09-18 14:50:09.864][000000000.421] I/user.文件系统 开始挂载: /little_flash
+[2025-09-18 14:50:10.078][000000000.816] D/little_flash lfs_mount 0
+[2025-09-18 14:50:10.086][000000000.816] D/little_flash vfs mount /little_flash ret 0
+[2025-09-18 14:50:10.095][000000000.817] I/user.文件系统 挂载成功: /little_flash
+[2025-09-18 14:50:10.102][000000000.817] I/user.文件系统信息 开始查询: /little_flash
+[2025-09-18 14:50:10.119][000000001.127] I/user.  总block数: 1024
+[2025-09-18 14:50:10.131][000000001.128] I/user.  已用block数: 2
+[2025-09-18 14:50:10.143][000000001.128] I/user.  block大小: 131072 字节
+[2025-09-18 14:50:10.150][000000001.128] I/user.  文件系统类型: lfs
+[2025-09-18 14:50:10.165][000000001.128] I/user.文件操作测试 开始
+[2025-09-18 14:50:10.173][000000001.310] I/user.  写入成功 /little_flash/test.txt 内容: 当前时间: Sun Jan  0 08:00:01 1900
+[2025-09-18 14:50:10.182][000000001.482] I/user.  读取成功 /little_flash/test.txt 内容: 当前时间: Sun Jan  0 08:00:01 1900
+[2025-09-18 14:50:11.057][000000002.366] I/user.  追加后内容: LuatOS 测试 - 追加时间: Sun Jan  0 08:00:02 1900
+[2025-09-18 14:50:11.063][000000002.367] I/user.文件操作测试 完成
+[2025-09-18 14:50:11.068][000000002.367] I/user.关闭spi true
+
+
+```
+
+# 

+ 141 - 0
module/Air8000/demo/accessory_board/AirETH_1000/http/http_app.lua

@@ -0,0 +1,141 @@
+--[[
+@module  http_app
+@summary http应用功能模块
+@version 1.0
+@date    2025.09.17
+@author  王城钧
+@usage
+本文件为http应用功能模块,核心业务逻辑为:基于不同的应用场景,演示http核心库的使用方式;
+本文件没有对外接口,直接在main.lua中require "http_app"就可以加载运行;
+]]
+
+--[[
+此处先详细解释下http.request接口的使用方法
+
+接口定义:
+    http.request(method, url, headers, body, opts, server_ca_cert, client_cert, client_key, client_password)
+
+使用方法:
+    local code, headers, body = http.request(method, url, headers, body, opts, server_ca_cert, client_cert, client_key, client_password).wait()
+    只能在task中使用
+    发送http请求到服务器,等待服务器的http应答,此处会阻塞当前task,等待整个过程成功结束或者出现错误异常结束或者超时结束
+
+参数定义:
+    method,stirng类型,必须包含此参数,表示HTTP请求方法,支持"GET"、"POST"、"HEAD"等所有HTTP请求方法
+    url,string类型,必须包含此参数,表示HTTP请求URL地址,支持HTTP、HTTPS,支持域名、IP地址,支持自定义端口,标准的HTTP URL格式都支持
+    headers,table或者nil类型,可选包含此参数,表示HTTP请求头,例如 {["Content-Type"] = "application/x-www-form-urlencoded", ["self_defined_key"] = "self_defined_value"}
+    body,string或者zbuff或者nil类型,可选包含此参数,表示HTTP请求体,如果请求体是一个文件中的内容,要把文件内容读出来,赋值给body使用
+    opts,table或者nil类型,可选包含此参数,表示HTTP请求的一些额外配置,包含以下内容
+    {
+        timeout    -- -- number或者nil类型,单位毫秒,可选包含此参数,表示从发送请求到读取到服务器响应整个过程的超时时间,如果传入0,表示永久等待;如果没有传入此参数或者传入nil,则使用默认值10分钟
+        dst        -- 下载路径,string类型,当HTTP请求的数据需要保存到文件中时,此处填写完整的文件路径
+        adapter    -- 使用的网卡ID,number类型,例如4G网卡,SPI外挂以太网卡,WIFI网卡等;如果没有传入此参数,内核固件会自动选择当前时间点其他功能模块设置的默认网卡
+                    -- 除非你HTTP请求时,一定要使用某一种网卡,才设置此参数;如果没什么特别要求,不要使用此参数,使用系统中设置的默认网卡即可
+                    -- 这个参数和本demo中的netdrv_device.lua关系比较大,netdrv_device会设置默认网卡,此处http不要设置adapter参数,直接使用netdrv_device设置的默认网卡就行
+        debug      -- 调试开关,bool类型,true表示打开debug调试信息日志,false表示关闭debug调试信息日志,默认为关闭状态
+        ipv6       -- 是否为ipv6,bool类型,true表示使用ipv6,false表示不使用ipv6,默认为false
+        userdata   -- 下载回调函数使用的用户自定义回调参数,做为callback回调函数的第三个参数使用
+        callback   -- 下载回调函数,function类型,当下载数据时,无论是保存到内存中,还是保存到文件系统中,如果设置了callback,内核固件中每收到一包body数据,都会自动执行一次callback回调函数
+                    -- 回调函数的调用形式为callback(content_len, body_len, userdata)
+                    --     content_len:number类型,数据总长度
+                    --     body_len:number类型,已经下载的数据长度
+                    --     userdata:下载回调函数使用的用户自定义回调参数
+    }
+    server_ca_cert,string类型,服务器ca证书数据,可选包含此参数,当客户端需要验证服务器证书时,需要此参数,如果证书数据在一个文件中,要把文件内容读出来,赋值给server_ca_cert
+    client_cert,string类型,客户端证书数据,可选包含此参数,当服务器需要验证客户端证书时,需要此参数,如果证书数据在一个文件中,要把文件内容读出来,赋值给client_cert
+    client_key, string类型,客户端加密后的私钥数据,可选包含此参数,当服务器需要验证客户端证书时,需要此参数,如果加密后的私钥数据在一个文件中,要把文件内容读出来,赋值给client_key
+    client_password,string类型,客户端私钥口令数据,可选包含此参数,当服务器需要验证客户端证书时,需要此参数,如果私钥口令数据在一个文件中,要把文件内容读出来,赋值给client_password
+
+返回值定义:
+
+    http.request().wait()有三个返回值code,headers,body
+    code表示执行结果,number类型,有以下两种含义:
+        1、code大于等于100时,表示服务器返回的HTTP状态码,例如200表示成功,详细说明可以通过搜索引擎搜索“HTTP状态码”自行了解
+        2、code小于0时,表示内核固件中检测到通信异常,有如下几种:
+            -1 HTTP_ERROR_STATE 错误的状态, 一般是底层异常,请报issue
+            -2 HTTP_ERROR_HEADER 错误的响应头部, 通常是服务器问题
+            -3 HTTP_ERROR_BODY 错误的响应体,通常是服务器问题
+            -4 HTTP_ERROR_CONNECT 连接服务器失败, 未联网,地址错误,域名错误
+            -5 HTTP_ERROR_CLOSE 提前断开了连接, 网络或服务器问题
+            -6 HTTP_ERROR_RX 接收数据报错, 网络问题
+            -7 HTTP_ERROR_DOWNLOAD 下载文件过程报错, 网络问题或下载路径问题
+            -8 HTTP_ERROR_TIMEOUT 超时, 包括连接超时,读取数据超时
+            -9 HTTP_ERROR_FOTA fota功能报错,通常是更新包不合法
+    headers有以下两种含义:
+        1、当code的返回值大于等于100时,headers表示服务器返回的应答头,table类型
+        2、当code的返回值小于0时,headers为nil
+    body有以下三种含义
+        1、当code的返回值大于等于100时,如果请求的body数据不需要保存到文件中,而是直接保存到内存中,则body表示请求到的数据内容,string类型
+        2、当code的返回值大于等于100时,如果请求的body数据需要保存到文件中,则body表示保存请求数据后的文件的大小,number类型
+        3、当code的返回值小于0时,body为nil
+]]
+
+
+-- 普通的http get请求功能演示
+-- 请求的body数据保存到内存变量中,在内存够用的情况下,最大支持32KB的数据存储到内存中
+-- timeout可以设置超时时间
+-- callback可以设置回调函数,可用于实时检测body数据的下载进度
+local function http_app_get()
+    -- https get请求https://www.air32.cn/网页内容
+    -- 如果请求成功,请求的数据保存到body中
+    local code, headers, body = http.request("GET", "https://www.air32.cn/").wait()
+    log.info("http_app_get1",
+        code == 200 and "success" or "error",
+        code,
+        json.encode(headers or {}),
+        body and (body:len() > 512 and body:len() or body) or "nil")
+
+    -- https get请求https://www.luatos.com/网页内容,超时时间为10秒
+    -- 请求超时时间为10秒,用户自己写代码时,不要照抄10秒,根据自己业务逻辑的需要设置合适的超时时间
+    -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get2"
+    -- 如果请求成功,请求的数据保存到body中
+    code, headers, body = http.request("GET", "https://www.luatos.com/", nil, nil,
+        { timeout = 10000, userdata = "http_app_get2", callback = http_cbfunc }).wait()
+    log.info("http_app_get2",
+        code == 200 and "success" or "error",
+        code,
+        json.encode(headers or {}),
+        body and (body:len() > 512 and body:len() or body) or "nil")
+
+    -- http get请求http://httpbin.air32.cn/get网页内容,超时时间为3秒
+    -- 请求超时时间为3秒,用户自己写代码时,不要照抄3秒,根据自己业务逻辑的需要设置合适的超时时间
+    -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get3"
+    -- 如果请求成功,请求的数据保存到body中
+    code, headers, body = http.request("GET", "http://httpbin.air32.cn/get", nil, nil,
+        { timeout = 3000, userdata = "http_app_get3", callback = http_cbfunc }).wait()
+    log.info("http_app_get3",
+        code == 200 and "success" or "error",
+        code,
+        json.encode(headers or {}),
+        body and (body:len() > 512 and body:len() or body) or "nil")
+end
+
+-- http app task 的任务处理函数
+local function http_app_task_func()
+    while true do
+        -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
+        while not socket.adapter(socket.dft()) do
+            log.warn("http_app_task_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())
+            sys.waitUntil("IP_READY", 1000)
+        end
+
+        -- 检测到了IP_READY消息
+        log.info("http_app_task_func", "recv IP_READY", socket.dft())
+
+        -- 普通的http get请求功能演示
+        http_app_get()
+
+        -- 50秒之后,循环测试
+        sys.wait(50000)
+    end
+end
+
+--创建并且启动一个task
+--运行这个task的处理函数http_app_task_func
+sys.taskInit(http_app_task_func)

+ 88 - 0
module/Air8000/demo/accessory_board/AirETH_1000/http/netdrv/netdrv_eth_spi.lua

@@ -0,0 +1,88 @@
+--[[
+@module  netdrv_eth_spi
+@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
+@version 1.0
+@date    2025.07.24
+@author  马梦阳
+@usage
+本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块,核心业务逻辑为:
+1、打开CH390H芯片供电开关;
+2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
+3、以太网卡的连接状态发生变化时,在日志中进行打印;
+
+使用Air8000核心板外接AirETH_1000小板测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
+]]
+
+local function ip_ready_func(ip, adapter)
+    if adapter == socket.LWIP_ETH then
+        log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
+    end
+end
+
+local function ip_lose_func(adapter)
+    if adapter == socket.LWIP_ETH then
+        log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
+    end
+end
+
+
+-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
+-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
+-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
+sys.subscribe("IP_READY", ip_ready_func)
+sys.subscribe("IP_LOSE", ip_lose_func)
+
+
+-- 设置默认网卡为socket.LWIP_ETH
+socket.dft(socket.LWIP_ETH)
+
+
+-- 本demo测试使用的是Air8000开发板
+-- GPIO140为CH390H以太网芯片的供电使能控制引脚
+gpio.setup(140, 1, gpio.PULLUP)
+
+-- 这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
+local function netdrv_eth_spi_task_func()
+    -- 初始化SPI1
+    local result = spi.setup(
+        1,--spi_id
+        nil,
+        0,--CPHA
+        0,--CPOL
+        8,--数据宽度
+        25600000--,--频率
+        -- spi.MSB,--高低位顺序    可选,默认高位在前
+        -- spi.master,--主模式     可选,默认主
+        -- spi.full--全双工       可选,默认全双工
+    )
+    log.info("netdrv_eth_spi", "spi open result", result)
+    --返回值为0,表示打开成功
+    if result ~= 0 then
+        log.error("netdrv_eth_spi", "spi open error",result)
+        return
+    end
+
+    -- 初始化以太网卡
+
+    -- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
+    -- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
+    -- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+    -- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
+    -- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
+    -- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功
+
+    -- socket.LWIP_ETH 指定网络适配器编号
+    -- netdrv.CH390外挂CH390
+    -- SPI ID 1, 片选 GPIO12
+    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1, cs=12})
+
+    -- 在以太网上开启动态主机配置协议
+    netdrv.dhcp(socket.LWIP_ETH, true)
+end
+
+-- 创建并且启动一个task
+-- task的处理函数为netdrv_eth_spi_task_func
+sys.taskInit(netdrv_eth_spi_task_func)

+ 94 - 0
module/Air8000/demo/accessory_board/AirETH_1000/http/netdrv/netdrv_multiple.lua

@@ -0,0 +1,94 @@
+--[[
+@module  netdrv_multiple
+@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
+@version 1.0
+@date    2025.07.24
+@author  马梦阳
+@usage
+本文件为多网卡驱动模块,核心业务逻辑为:
+1、调用exnetif.set_priority_order配置多网卡的控制参数以及优先级;
+
+使用Air8000核心板外接AirETH_1000小板测试即可;
+
+本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
+]]
+
+local exnetif = require "exnetif"
+
+-- 网卡状态变化通知回调函数
+-- 当exnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
+-- 当网卡切换切换时:
+--     net_type:string类型,表示当前使用的网卡字符串
+--     adapter:number类型,表示当前使用的网卡id
+-- 当所有网卡断网时:
+--     net_type:为nil
+--     adapter:number类型,为-1
+local function netdrv_multiple_notify_cbfunc(net_type,adapter)
+    if type(net_type)=="string" then
+        log.info("netdrv_multiple_notify_cbfunc", "use new adapter", net_type, adapter)
+    elseif type(net_type)=="nil" then
+        log.warn("netdrv_multiple_notify_cbfunc", "no available adapter", net_type, adapter)
+    else
+        log.warn("netdrv_multiple_notify_cbfunc", "unknown status", net_type, adapter)
+    end
+end
+
+local function netdrv_multiple_task_func()
+    --设置网卡优先级
+    exnetif.set_priority_order(
+        {
+            -- “通过SPI外挂CH390H芯片”的以太网卡,使用Air8000开发板验证
+            {
+                ETHERNET = {
+                    -- 供电使能GPIO
+                    pwrpin = 140,
+                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
+                    -- 如果没有传入此参数,exnetif会使用默认值10秒
+                    ping_time = 3000,
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+
+                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
+                    tp = netdrv.CH390,
+                    opts = {spi=1, cs=12}
+                }
+            },
+
+            -- WIFI STA网卡
+            {
+                WIFI = {
+                    -- 要连接的WIFI路由器名称
+                    ssid = "iPhone",
+                    -- 要连接的WIFI路由器密码
+                    password = "HZ88888888",
+
+                    -- 连通性检测ip(选填参数);
+                    -- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
+                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
+                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
+                }
+            },
+
+            -- 4G网卡
+            {
+                LWIP_GP = true
+            }
+        }
+    )
+end
+
+-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
+exnetif.notify_status(netdrv_multiple_notify_cbfunc)
+
+-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
+-- 可以在判断出现异常的位置,调用一次exnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
+-- 如果存在tcp网络应用,不需要用户调用exnetif.check_network_status()接口去控制,exnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。
+
+
+-- 启动一个task,task的处理函数为netdrv_multiple_task_func
+-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
+-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
+sys.taskInit(netdrv_multiple_task_func)

+ 27 - 0
module/Air8000/demo/accessory_board/AirETH_1000/http/netdrv_device.lua

@@ -0,0 +1,27 @@
+--[[
+@module  netdrv_device
+@summary 网络驱动设备功能模块
+@version 1.0
+@date    2025.07.24
+@author  马梦阳
+@usage
+本文件为网络驱动设备功能模块,核心业务逻辑为:根据项目需求,选择并且配置合适的网卡(网络适配器)
+1、netdrv_4g:socket.LWIP_GP,4G网卡;
+2、netdrv_wifi:socket.LWIP_STA,WIFI STA网卡;
+3、netdrv_ethernet_spi:socket.LWIP_ETH,通过SPI外挂CH390H芯片的以太网卡;
+4、netdrv_multiple:可以配置多种网卡的优先级,按照优先级配置,使用其中一种网卡连接外网;
+
+根据自己的项目需求,只需要require以上其中的一种即可;
+
+
+本文件没有对外接口,直接在main.lua中require "netdrv_device"就可以加载运行;
+]]
+
+
+-- 根据自己的项目需求,只需要require以下其中的一种即可;
+
+-- 加载“通过SPI外挂CH390H芯片的以太网卡”驱动模块
+require "netdrv_eth_spi"
+
+-- 加载“可以配置优先级的多种网卡”驱动模块
+-- require "netdrv_multiple"

+ 0 - 79
module/Air8000/demo/accessory_board/AirETH_1000/lan.lua

@@ -1,79 +0,0 @@
---[[
-@module  lan
-@summary lan 模组连接4G网络通过以太网口传输给其他设备
-@version 1.0
-@date    2025.09.12
-@author  王城钧
-@usage
-本文件为lan网络模块,核心业务逻辑为:
-1.设置模组连接4G网络通过以太网口传输给其他设备
-本文件没有对外接口,直接在main.lua中require "lan"就可以加载运行;
-]]
-
-dhcps = require "dhcpsrv"
-dnsproxy = require "dnsproxy"
-
--- 启动lan网络初始化
-local function lan_init()
-    sys.wait(500)   -- 非必须延时
-    log.info("ch390", "打开LDO供电")
-    gpio.setup(140, 1, gpio.PULLUP)     --打开ch390供电
-    sys.wait(6000)
-    local result = spi.setup(
-        1,--spi_id
-        nil,
-        0,--CPHA
-        0,--CPOL
-        8,--数据宽度
-        25600000--,--频率
-        -- spi.MSB,--高低位顺序    可选,默认高位在前
-        -- spi.master,--主模式     可选,默认主
-        -- spi.full--全双工       可选,默认全双工
-    )
-    log.info("main", "open",result)
-    if result ~= 0 then--返回值为0,表示打开成功
-        log.info("main", "spi open error",result)
-        return
-    end
-
-    -- 初始化指定netdrv设备,
-    -- socket.LWIP_ETH 网络适配器编号
-    -- netdrv.CH390外挂CH390
-    -- SPI ID 1, 片选 GPIO12
-    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1,cs=12})
-    sys.wait(3000)
-    local ipv4,mark, gw = netdrv.ipv4(socket.LWIP_ETH, "192.168.4.1", "255.255.255.0", "192.168.4.1")
-    log.info("ipv4", ipv4,mark, gw)
-    while netdrv.link(socket.LWIP_ETH) ~= true do
-        -- sys.wait(100)   -- 等待以太网口联通
-    end
-    while netdrv.link(socket.LWIP_GP) ~= true do
-        -- sys.wait(100)   -- 等待GP口联通
-    end
-    sys.wait(2000) --非必须延时
-    dhcps.create({adapter=socket.LWIP_ETH})
-    dnsproxy.setup(socket.LWIP_ETH, socket.LWIP_GP)
-    netdrv.napt(socket.LWIP_GP)
-    if iperf then
-        log.info("启动iperf服务器端")
-        iperf.server(socket.LWIP_ETH)
-    end
-end
-
--- 启动lan网络任务
-local function lan_task()
-    -- sys.waitUntil("IP_READY")
-    while 1 do
-        sys.wait(3000) -- 非必须延时, 此处为了方便观察日志
-        -- log.info("http", http.request("GET", "http://httpbin.air32.cn/bytes/4096", nil, nil, {adapter=socket.LWIP_ETH}).wait())
-        log.info("lua", rtos.meminfo())
-        log.info("sys", rtos.meminfo("sys"))
-        -- log.info("psram", rtos.meminfo("psram"))
-    end
-end
-
--- 启动lan网络初始化
-sys.taskInit(lan_init)
-
--- 启动lan网络任务
-sys.taskInit(lan_task)

+ 11 - 8
module/Air8000/demo/accessory_board/AirETH_1000/main.lua

@@ -2,7 +2,7 @@
 @module  main
 @summary LuatOS用户应用脚本文件入口,总体调度应用逻辑 
 @version 1.0
-@date    2025.09.12
+@date    2025.09.17
 @author  王城钧
 @usage
 本demo演示的核心功能为:
@@ -51,16 +51,19 @@ end
 -- 也可以使用客户自己搭建的平台进行远程升级
 -- 远程升级的详细用法,可以参考fota的demo进行使用
 
--- 开启WAN功能(以太网提供网络供模组上网)
--- require "wan"
+-- 加载网络驱动设备功能模块
+-- require "netdrv_device"
 
--- 开启LAN功能(设置模组连接4G网络通过以太网口传输给其他设备)
-require "lan"
+-- 开启多网融合功能,4G提供网络供以太网和wifi设备上网
+require "4g-eth-wifi"
 
--- 开启多网融合功能(以太网提供网络供wifi和以太网设备上网)
--- require "netif_app"
+-- 开启多网融合功能,WIFI提供网络供以太网和wifi设备上网 
+-- require "wifi-eth-wifi"
 
--- 用户代码已结束---------------------------------------------
+-- 加载http应用功能模块
+-- require "http_app"
+
+-- 用户代码已结束
 -- 结尾总是这一句
 sys.run()
 -- sys.run()之后后面不要加任何语句!!!!!

+ 0 - 49
module/Air8000/demo/accessory_board/AirETH_1000/netif_app.lua

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

+ 78 - 0
module/Air8000/demo/accessory_board/AirETH_1000/network_routing/4g_out_ethernet_in_wifi_in/4g-eth-wifi.lua

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

+ 55 - 0
module/Air8000/demo/accessory_board/AirETH_1000/network_routing/wifi_out_ethernet_in_wifi_in/wifi-eth-wifi.lua

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

+ 20 - 64
module/Air8000/demo/accessory_board/AirETH_1000/readme.md

@@ -1,22 +1,24 @@
 ## 功能模块介绍:
 
-1、main.lua:主程序入口,以下三个脚本按自己的需求选择其一使用即可,另外两个注释
+1、main.lua:主程序入口;
 
-2、wan.lua:以太网提供网络供模组上网
+2、netdrv_device.lua:加载网络驱动设备功能模块
 
-3、lan.lua:模组连接4G网络通过以太网口传输给其他设备供网;
+3、4g-eth-wifi.lua:模组通过4G提供网络供以太网和wifi设备上网;
 
-4、netif_app: 网络管理模块,开启多网融合功能,以太网提供网络供wifi和以太网设备上网;
+4、wifi-eth-wifi: 模组通过WIFI提供网络供以太网和wifi设备上网;
 
 ## 演示功能概述
 
-1、模组连接4G网络通过以太网口传输给其他设备供网 
+1、以太网给模组供网,通过连接http测试连通。 
 
 ## 演示硬件环境
 
 1、Air8000核心板一块+可上网的sim卡一张+网线一根+AirETH_1000板子一个;
 
-<img title="" src="https://docs.openLuat.com/cdn//image/AirETH_1000.jpg" alt="lan" style="zoom:50%;" data-align="left">
+[](https://docs.openLuat.com/cdn/image/AirETH_1000.jpg)
+
+![lan](E:\文档池\新建文件夹\luatos-doc-pool\docs\root\docs\air8000\luatos\app\image\lan.jpg)
 
 2、TYPE-C USB数据线一根 + 杜邦线若干;
 
@@ -49,75 +51,29 @@
 
 1、搭建好硬件环境,按接线图连接硬件。
 
-2、烧录内核固件和本项目的Lua脚本:main.lua:主程序入口(需要在main.lua文件中打开require"lan"),lan.lua:模组连接4G网络通过以太网口传输给其他设备供网。
+2、烧录内核固件和本项目的Lua脚本:main.lua:主程序入口(需要在main.lua文件中打开require"netdrv_device"和require"http_app")
 
 3、启动设备,观察日志输出:
 
-```
-[2025-09-12 15:25:44.453][000000006.933] D/ch390h 注册CH390H设备 SPI id 1 cs 12 irq 255
-
-[2025-09-12 15:25:44.512][000000006.934] D/netdrv.ch390x task started
-
-[2025-09-12 15:25:44.560][000000006.934] D/ch390h ch390注册完成
-
-[2025-09-12 15:25:44.595][000000006.979] D/netdrv.ch390x 初始化MAC 701988D3008A
-
-[2025-09-12 15:25:44.627][000000006.980] D/netdrv.ch390x luat_netif_init 执行完成 1
-
-[2025-09-12 15:25:45.993][000000008.574] I/netdrv.ch390x link is up 1 12 100M
-
-[2025-09-12 15:25:46.850][000000009.435] I/user.lua 4194296 54592 54592
-
-[2025-09-12 15:25:46.880][000000009.435] I/user.sys 3208824 303680 305200
-
-[2025-09-12 15:25:47.349][000000009.935] D/net network ready 4, setup dns server
-
-[2025-09-12 15:25:47.445][000000009.935] I/user.ipv4 192.168.4.1 255.255.255.0 192.168.4.1
-
-[2025-09-12 15:25:49.356][000000011.936] I/user.dhcpsrv 自动获取网卡IP作为网关 192.168.4.1
-
-[2025-09-12 15:25:49.399][000000011.937] D/socket connect to 255.255.255.255,0
+出现类似如下打印,就表示成功。
 
-[2025-09-12 15:25:49.451][000000011.937] D/net connect 255.255.255.255:0 UDP
-
-[2025-09-12 15:25:49.482][000000011.939] I/user.dnsproxy 4 1
-
-[2025-09-12 15:25:49.525][000000011.941] I/user.dnsproxy 开始监听
-
-[2025-09-12 15:25:49.568][000000011.944] D/socket connect to 255.255.255.255,0
-
-[2025-09-12 15:25:49.612][000000011.945] D/net connect 255.255.255.255:0 UDP
-
-[2025-09-12 15:25:49.655][000000011.946] D/socket connect to 119.29.29.29,53
-
-[2025-09-12 15:25:49.699][000000011.946] D/netdrv NAPT is enabled gw 1
-
-[2025-09-12 15:25:49.751][000000011.951] I/user.启动iperf服务器端
-
-[2025-09-12 15:25:49.794][000000011.952] D/iperf 启动iperf server 0
-
-[2025-09-12 15:25:49.825][000000011.952] D/lwiperf iperf_malloc 88 88 0xc1703c4
-
-[2025-09-12 15:25:49.852][000000011.952] D/iperf iperf listen 192.168.4.1:5001
-
-[2025-09-12 15:25:49.878][000000012.436] I/user.lua 4194296 58552 58568
+```
+[2025-09-17 14:35:59.777][000000005.878] I/user.netdrv_eth_spi.ip_ready_func IP_READY 192.168.3.99 255.255.255.0 
+192.168.3.1 nil
 
-[2025-09-12 15:25:49.908][000000012.436] I/user.sys 3208824 512576 514616
+[2025-09-17 14:35:59.783][000000005.879] I/user.http_app_task_func recv IP_READY 4 4
 
-[2025-09-12 15:25:50.690][000000013.275] I/user.dhcpsrv 是discover包 E466E52E5EE6 12
+[2025-09-17 14:35:59.786][000000005.883] dns_run 676:www.air32.cn state 0 id 1 ipv6 0 use dns server0, try 0
 
-[2025-09-12 15:25:50.720][000000013.276] I/user.dhcpsrv 分配ip E466E52E5EE6 192.168.4.100
+[2025-09-17 14:35:59.789][000000005.883] D/net adatper 4 dns server 192.168.3.1
 
-[2025-09-12 15:25:50.755][000000013.276] I/user.dhcpsrv send offer
+[2025-09-17 14:35:59.793][000000005.883] D/net dns udp sendto 192.168.3.1:53 from 192.168.3.99
 
-[2025-09-12 15:25:50.791][000000013.289] I/user.dhcpsrv 是request包 E466E52E5EE6 12
+[2025-09-17 14:35:59.799][000000005.891] dns_run 693:dns all done ,now stop
 
-[2025-09-12 15:25:50.828][000000013.289] I/user.dhcpsrv request,发现已经分配的mac地址, send ack E466E52E5EE6 12
+[2025-09-17 14:35:59.802][000000005.891] D/net connect 49.232.89.122:443 TCP
 
-[2025-09-12 15:25:52.852][000000015.437] I/user.lua 4194296 69128 69944
+[2025-09-17 14:36:00.215][000000006.395] I/user.http_app_get1 success 200 {"Transfer-Encoding":"chunked","Date":"Wed, 17 Sep 2025 06:36:02 GMT","Connection":"keep-alive","Server":"openresty\/1.27.1.2","Content-Type":"text\/html"} 2416
 
-[2025-09-12 15:25:52.883][000000015.437] I/user.sys 3208824 514232 515760
 
 ```
-
-4、电脑通过网线与AirETH_1000板子连接若可以正常上网,则表示验证成功。

+ 0 - 60
module/Air8000/demo/accessory_board/AirETH_1000/wan.lua

@@ -1,60 +0,0 @@
---[[
-@module  wan
-@summary wan 以太网提供网络供模组上网
-@version 1.0
-@date    2025.09.12
-@author  王城钧
-@usage
-本文件为lan网络模块,核心业务逻辑为:
-1.以太网提供网络供模组上网
-2.http测试以太网网络
-本文件没有对外接口,直接在main.lua中require "wan"就可以加载运行;
-]]
-
--- 启动WAN网络初始化
-local function wan_init()
-    sys.wait(500)                   --等待500ms,此延时非必须
-    log.info("ch390", "打开LDO供电")
-    gpio.setup(140, 1, gpio.PULLUP) --打开ch390供电
-    sys.wait(6000)                  --等待6000ms,此延时非必须
-    local result = spi.setup(
-        1,                          --spi_id
-        nil,
-        0,                          --CPHA
-        0,                          --CPOL
-        8,                          --数据宽度
-        25600000                    --,--频率
-    -- spi.MSB,--高低位顺序    可选,默认高位在前
-    -- spi.master,--主模式     可选,默认主
-    -- spi.full--全双工       可选,默认全双工
-    )
-    log.info("main", "open", result)
-    if result ~= 0 then --返回值为0,表示打开成功
-        log.info("main", "spi open error", result)
-        return
-    end
-    -- 初始化指定netdrv设备,
-    -- socket.LWIP_ETH 网络适配器编号
-    -- netdrv.CH390外挂CH390
-    -- SPI ID 1, 片选 GPIO12
-    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, { spi = 1, cs = 12 })
-    netdrv.dhcp(socket.LWIP_ETH, true)
-end
-
--- WAN网络测试任务
-local function wan_task()
-    -- sys.waitUntil("IP_READY")
-    while 1 do
-        sys.wait(6000)    --非必须延时,只是为了方便观察日志输出                                                                                                      -- 此处延时非必须,只是为了方便观察日志输出
-        log.info("http",
-            http.request("GET", "http://httpbin.air32.cn/bytes/4096", nil, nil, { adapter = socket.LWIP_ETH }).wait())          --adapter指定为以太网联网方式
-        log.info("lua", rtos.meminfo())
-        log.info("sys", rtos.meminfo("sys"))
-    end
-end
-
--- 启动WAN网络初始化和任务
-sys.taskInit(wan_init)
-
--- 启动WAN联网测试
-sys.taskInit(wan_task)

+ 0 - 339
module/Air8000/demo/airtalk/airtalk_dev_ctrl.lua

@@ -1,339 +0,0 @@
-local g_state = SP_T_NO_READY   --device状态
-local g_mqttc = nil             --mqtt客户端
-local g_local_id                  --本机ID
-local g_remote_id                 --对端ID
-local g_s_type                  --对讲的模式,字符串形式的
-local g_s_topic                 --对讲用的topic
-local g_s_mode                  --对讲的模式
-local g_dev_list                --对讲列表
-
-
-
-
-local function auth()
-    if g_state == SP_T_NO_READY then
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0001", json.encode({["key"] = PRODUCT_KEY, ["device_type"] = 1}))
-    end
-end
-
-local function heart()
-    if g_state == SP_T_CONNECTED then
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0005", json.encode({["from"] = g_local_id, ["to"] = g_remote_id}))
-    end
-end
-
-local function wait_speech_to()
-    log.info("主动请求对讲超时无应答")
-    speech_off(true, false)
-end
-
---对讲开始,topic,ssrc,采样率(8K或者16K)这3个参数都有了之后就能进行对讲了,可以通过其他协议传入
-local function speech_on(ssrc, sample)
-    g_state = SP_T_CONNECTED
-    g_mqttc:subscribe(g_s_topic)
-    airtalk.set_topic(g_s_topic)
-    airtalk.set_ssrc(ssrc)
-    log.info("对讲模式", g_s_mode)
-    airtalk.speech(true, g_s_mode, sample)
-    sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, true) 
-    sys.timerLoopStart(heart, 150000)
-    sys.timerStopAll(wait_speech_to)
-    log.info("对讲接通,可以说话了")
-end
---对讲结束
-local function speech_off(need_upload, need_ind)
-    if g_state ==  SP_T_CONNECTED then
-        g_mqttc:unsubscribe(g_s_topic)
-        airtalk.speech(false)
-        g_s_topic = nil
-    end
-    g_state = SP_T_IDLE
-    sys.timerStopAll(auth)
-    sys.timerStopAll(heart)
-    sys.timerStopAll(wait_speech_to)
-    log.info("对讲断开了")
-    if need_upload then
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0004", json.encode({["to"] = g_remote_id}))
-    end
-    if need_ind then
-        sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_OFF_IND, true)
-    end
-end
-
-
-local function analyze_v1(cmd, topic, obj)
-    if cmd == "8005" or cmd == "8004" then       -- 对讲心跳保持和结束对讲的应答不做处理
-        return
-    end
-    if cmd == "8003" then       -- 请求对讲应答
-        if g_state ~= SP_T_CONNECTING then  --没有发起对讲请求
-            log.error("state", g_state, "need", SP_T_CONNECTING)
-            return
-        else
-            if obj and obj["result"] == SUCC and g_s_topic == obj["topic"]then  --完全正确,开始对讲
-                speech_on(obj["ssrc"], obj["audio_code"] == "amr-nb" and 8000 or 16000)
-                return
-            else
-                log.info(obj["result"], obj["topic"], g_s_topic)
-                sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, false)   --有异常,无法对讲
-            end
-            
-        end
-        g_s_topic = nil
-        g_state = SP_T_IDLE
-        return
-    end
-    local new_obj = nil
-    if cmd == "0102" then       -- 对端打过来
-        if obj and obj["topic"] and obj["ssrc"] and obj["audio_code"] and obj["type"] then
-            if g_state ~= SP_T_IDLE then    -- 空闲状态下才可以进入对讲状态
-                log.error("state", g_state, "need", SP_T_IDLE)
-                new_obj = {["result"] = "failed", ["topic"] = obj["topic"], ["info"] = "device is busy"}
-            else
-                if obj["type"] == "one-on-one" then -- 1对1对讲
-                    local from = string.match(obj["topic"], "audio/.*/(.*)/.*")
-                    if from then
-                        log.info("remote id ", from)
-                        g_s_topic = obj["topic"]
-                        g_remote_id = from
-                        new_obj = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
-                        g_s_type = "one-on-one"
-                        g_s_mode = airtalk.MODE_PERSON
-                        speech_on(obj["ssrc"], obj["audio_code"] == "amr-nb" and 8000 or 16000)
-                    else
-                        new_obj = {["result"] = "failed", ["topic"] = obj["topic"], ["info"] = "topic error"}
-                    end
-                elseif obj["type"] == "broadcast" then  -- 1对多对讲
-                    g_s_topic = obj["topic"]
-                    new_obj = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
-                    g_s_mode = airtalk.MODE_GROUP_LISTENER
-                    g_s_type = "broadcast"
-                    speech_on(obj["ssrc"], obj["audio_code"] == "amr-nb" and 8000 or 16000)
-                end
-            end
-        else
-            new_obj = {["result"] = "failed", ["topic"] = obj["topic"], ["info"] = "json info error"}
-        end
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/8102", json.encode(new_obj))
-        return
-    end
-
-    if cmd == "0103" then   --对端挂断
-        if g_state == SP_T_IDLE then
-            new_obj = {["result"] = "failed", ["info"] = "no speech"}
-        else
-            if obj and obj["type"] == g_s_type then
-                new_obj = {["result"] = SUCC, ["info"] = ""}
-                speech_off(false, true)
-            else
-                new_obj = {["result"] = "failed", ["info"] = "type mismatch"}
-            end
-        end
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/8103", json.encode(new_obj))
-        return
-    end
-
-    if cmd == "0101" then                        --更新设备列表
-        if obj then
-            g_dev_list = obj["dev_list"]
-            -- for i=1,#g_dev_list do
-            --     log.info(g_dev_list[i]["id"],g_dev_list[i]["name"])
-            -- end
-            new_obj = {["result"] = SUCC, ["info"] = ""}
-        else
-            new_obj = {["result"] = "failed", ["info"] = "json info error"}
-        end
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/8101", json.encode(new_obj))
-        return
-    end
-    if cmd == "8001" then
-        if obj and obj["result"] == SUCC then
-            g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0002","")  -- 更新列表
-        else
-            sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "鉴权失败" .. obj["info"]) 
-        end
-        return
-    end
-    if cmd == "8002" then
-        if obj and obj["result"] == SUCC then   --收到设备列表更新应答,才能认为相关网络服务准备好了
-            g_dev_list = obj["dev_list"]
-            -- for i=1,#g_dev_list do
-            --     log.info(g_dev_list[i]["id"],g_dev_list[i]["name"])
-            -- end
-            g_state = SP_T_IDLE
-            sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, true)  --完整登录流程结束
-        else
-            sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "更新设备列表失败") 
-        end
-        return
-    end
-end
-
-local function mqtt_cb(mqttc, event, topic, payload)
-    log.info(event, topic)
-    local msg,data,obj
-    if event == "conack" then
-        sys.sendMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND) --mqtt连上了,开始自定义的鉴权流程
-        g_mqttc:subscribe("ctrl/downlink/" .. g_local_id .. "/#")--单主题订阅
-    elseif event == "suback" then
-        if g_state == SP_T_NO_READY then
-            if topic then
-                auth()
-            else
-                sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "订阅失败" .. "ctrl/downlink/" .. g_local_id .. "/#") 
-            end
-        elseif g_state == SP_T_CONNECTED then
-            if not topic then
-                speech_off(false, true)
-            end
-        end
-    elseif event == "recv" then
-        local result = string.match(topic, g_dl_topic)
-        if result then 
-            local obj,res,err = json.decode(payload)
-            analyze_v1(result, topic, obj)
-        end
-        result = nil
-        data = nil
-        obj = nil
-        
-    elseif event == "sent" then
-        -- log.info("mqtt", "sent", "pkgid", data)
-    elseif event == "disconnect" then
-        speech_off(false, true)
-        g_state = SP_T_NO_READY
-    elseif event == "error" then
-
-    end
-end
-
-local function task_cb(msg)
-    if msg[1] == MSG_SPEECH_CONNECT_TO then
-        speech_off(true,false)
-    else
-        log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
-    end
-end
-
-local function airtalk_event_cb(event, param)
-    log.info("airtalk event", event, param)
-    if event == airtalk.EVENT_ERROR then
-        if param == airtalk.ERROR_NO_DATA then
-            log.error("长时间没有收到音频数据")
-            speech_off(true, true)
-        end
-    end
-end
-
-local function airtalk_mqtt_task()
-    local msg,data,obj,online,num,res
-    --g_local_id也可以自己设置
-    g_local_id = mobile.imei()
-    g_dl_topic = "ctrl/downlink/" .. g_local_id .. "/(%w%w%w%w)"
-    sys.timerLoopStart(next_auth, 900000)
-
-    g_mqttc = mqtt.create(nil, "mqtt.airtalk.luatos.com", 1883, false, {rxSize = 32768})
-    airtalk.config(airtalk.PROTOCOL_MQTT, g_mqttc, 200) -- 缓冲至少200ms播放
-    airtalk.on(airtalk_event_cb)
-    airtalk.start()
-
-    g_mqttc:auth(g_local_id,g_local_id,mobile.muid()) -- g_local_id必填,其余选填
-    g_mqttc:keepalive(240) -- 默认值240s
-    g_mqttc:autoreconn(true, 15000) -- 自动重连机制
-    g_mqttc:debug(false)
-    g_mqttc:on(mqtt_cb)
-    log.info("设备信息", g_local_id, mobile.muid())
-    -- mqttc自动处理重连, 除非自行关闭
-    g_mqttc:connect()
-    online = false
-    while true do
-        msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)   --等服务器连上
-        log.info("connected")
-        while not online do
-            msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, 30000)   --登录流程不应该超过30秒
-            if type(msg) == 'table' then
-                online = msg[2]
-                if online then
-                    sys.timerLoopStart(auth, 3600000) --鉴权通过则60分钟后尝试重新鉴权
-                else
-                    log.info(msg[3])
-                    sys.timerLoopStart(auth, 300000)       --5分钟后重新鉴权
-                end
-            else
-                auth()  --30秒鉴权无效后重新鉴权
-            end
-        end
-        log.info("对讲管理平台已连接")
-        while online do
-            msg = sys.waitMsg(AIRTALK_TASK_NAME)
-            if type(msg) == 'table' and type(msg[1]) == "number" then
-                if msg[1] == MSG_PERSON_SPEECH_TEST_START then
-                    if g_state ~= SP_T_IDLE then
-                        log.info("正在对讲无法开始")
-                    else
-                        log.info("测试一下主动1对1对讲功能,找一个有效的IMEI")
-
-                        for i=1,#g_dev_list do
-                            res = string.match(g_dev_list[i]["id"], "(%w%w%w%w%w%w%w%w%w%w%w%w%w%w%w)")
-                            if res and res ~= g_local_id then
-                                break
-                            end
-                        end
-                        if res then
-                            log.info("向", res, "主动发起对讲")
-                            g_state = SP_T_CONNECTING
-                            g_remote_id = res
-                            g_s_mode = airtalk.MODE_PERSON
-                            g_s_type = "one-on-one"
-                            g_s_topic = "audio/" .. g_local_id .. "/" .. g_remote_id .. "/" .. (string.sub(tostring(mcu.ticks()), -4, -1))
-                            g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0003", json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
-                            sys.timerStart(wait_speech_to, 15000)
-                        else
-                            log.info("找不到有效的设备ID")
-                        end
-                    end
-                elseif msg[1] == MSG_GROUP_SPEECH_TEST_START then
-                    if g_state ~= SP_T_IDLE then
-                        log.info("正在对讲无法开始")
-                    else
-                        log.info("测试一下1对多对讲功能")
-                        g_remote_id = "all"
-                        g_state = SP_T_CONNECTING
-                        g_s_mode = airtalk.MODE_GROUP_SPEAKER
-                        g_s_type = "broadcast"
-                        g_s_topic = "audio/" .. g_local_id .. "/all/" .. (string.sub(tostring(mcu.ticks()), -4, -1))
-                        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0003", json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
-                        sys.timerStart(wait_speech_to, 15000)
-                    end
-                elseif msg[1] == MSG_SPEECH_STOP_TEST_END then
-                    if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
-                        log.info("没有对讲", g_state)
-                    else
-                        log.info("主动断开对讲")
-                        speech_off(true, false)
-                    end
-                elseif msg[1] == MSG_SPEECH_ON_IND then
-                    if msg[2] then
-                        log.info("对讲接通")
-                    else
-                        log.info("对讲断开")
-                    end
-                elseif msg[1] == MSG_CONNECT_OFF_IND then
-                    log.info("connect", msg[2])
-                    online = msg[2]
-                end
-                obj = nil
-            else
-                log.info(type(msg), type(msg[1]))
-            end
-            msg = nil
-        end
-        online = false
-    end
-end
-
-function airtalk_mqtt_init()
-    sys.taskInitEx(airtalk_mqtt_task, AIRTALK_TASK_NAME, task_cb)
-end
-
-

+ 0 - 37
module/Air8000/demo/airtalk/audio_config.lua

@@ -1,37 +0,0 @@
-function audio_init()
-    pm.ioVol(pm.IOVOL_ALL_GPIO, 3300)
-    local multimedia_id = 0
-
-    local i2s_id = 0
-    local i2s_mode = 0
-    local i2s_sample_rate = 16000
-    local i2s_bits_per_sample = 16
-    local i2s_channel_format = i2s.MONO_R
-    local i2s_communication_format = i2s.MODE_LSB
-    local i2s_channel_bits = 16
-    --air8000 core开发版+音频小板配置
-    local voice_vol = 60 --音频小板喇叭太容易失真了,不能太大
-    local i2c_id = 0
-    local pa_pin = 162           -- 喇叭pa功放脚
-    local pa_on_level = 1
-    local pa_delay = 200
-    local dac_power_pin = 164
-    local dac_power_on_level = 1
-    local dac_power_off_delay = 600
-    gpio.setup(24, 1)   --air8000的I2C0需要拉高gpio24才能用
-    gpio.setup(26, 0)
-    i2c.setup(0, i2c.FAST)
-    gpio.setup(24, 1, gpio.PULLUP)          -- i2c工作的电压域
-    sys.wait(100)
-    gpio.setup(dac_power_pin, 1, gpio.PULLUP)   -- 打开音频编解码供电
-    gpio.setup(pa_pin, 1, gpio.PULLUP)      -- 打开音频放大器
-    audio.on(0, audio_callback)
-
-    i2s.setup(i2s_id, i2s_mode, i2s_sample_rate, i2s_bits_per_sample, i2s_channel_format, i2s_communication_format,i2s_channel_bits)
-
-    audio.config(multimedia_id, pa_pin, pa_on_level, 0, pa_delay, dac_power_pin, dac_power_on_level, dac_power_off_delay)
-    audio.setBus(multimedia_id, audio.BUS_I2S,{chip = "es8311",i2cid = i2c_id , i2sid = i2s_id})	--通道0的硬件输出通道设置为I2S
-
-    audio.vol(multimedia_id, voice_vol)
-    audio.micVol(multimedia_id, 75)
-end

+ 0 - 27
module/Air8000/demo/airtalk/demo_define.lua

@@ -1,27 +0,0 @@
-
-AIRTALK_TASK_NAME = "airtalk_task"
-USER_TASK_NAME = "user"
-
-MSG_CONNECT_ON_IND = 0
-MSG_CONNECT_OFF_IND = 1
-MSG_AUTH_IND = 2
-MSG_SPEECH_ON_IND = 3
-MSG_SPEECH_OFF_IND = 4
-MSG_SPEECH_CONNECT_TO = 5
-
-MSG_PERSON_SPEECH_TEST_START = 20
-MSG_GROUP_SPEECH_TEST_START = 21
-MSG_SPEECH_STOP_TEST_END = 22
-
-
-MSG_READY = 10
-MSG_NOT_READY = 11
-MSG_KEY_PRESS = 12
-
-SP_T_NO_READY = 0           -- 离线状态无法对讲
-SP_T_IDLE = 1               -- 对讲空闲状态
-SP_T_CONNECTING = 2         -- 主动发起对讲
-SP_T_CONNECTED = 3          -- 对讲中
-
-
-SUCC = "success"

+ 541 - 0
module/Air8000/demo/airtalk/exaudio.lua

@@ -0,0 +1,541 @@
+--[[
+@module exaudio
+@summary exaudio扩展库
+@version 1.1
+@date    2025.09.01
+@author  梁健
+@usage
+]]
+local exaudio = {}
+
+-- 常量定义
+local I2S_ID = 0
+local I2S_MODE = 0          -- 0:主机 1:从机
+local I2S_SAMPLE_RATE = 16000
+local I2S_CHANNEL_FORMAT = i2s.MONO_R  
+local I2S_COMM_FORMAT = i2s.MODE_LSB   -- 可选MODE_I2S, MODE_LSB, MODE_MSB
+local I2S_CHANNEL_BITS = 16
+local MULTIMEDIA_ID = 0
+local EX_MSG_PLAY_DONE = "playDone"
+local ES8311_ADDR = 0x18    -- 7位地址
+local CHIP_ID_REG = 0x00    -- 芯片ID寄存器地址
+
+-- 模块常量
+exaudio.PLAY_DONE = 1         --   音频播放完毕的事件之一
+exaudio.RECORD_DONE = 1       --   音频录音完毕的事件之一  
+exaudio.AMR_NB = 0
+exaudio.AMR_WB = 1
+exaudio.PCM_8000 = 2
+exaudio.PCM_16000 = 3 
+exaudio.PCM_24000 = 4
+exaudio.PCM_32000 = 5
+
+
+-- 默认配置参数
+local audio_setup_param = {
+    model = "es8311",         -- dac类型: "es8311","es8211"
+    i2c_id = 0,               -- i2c_id: 0,1
+    pa_ctrl = 0,              -- 音频放大器电源控制管脚
+    dac_ctrl = 0,             -- 音频编解码芯片电源控制管脚
+    dac_delay = 3,            -- DAC启动前冗余时间(100ms)
+    pa_delay = 100,           -- DAC启动后延迟打开PA的时间(ms)
+    dac_time_delay = 600,     -- 播放完毕后PA与DAC关闭间隔(ms)
+    bits_per_sample = 16,     -- 采样位数
+    pa_on_level = 1           -- PA打开电平 1:高 0:低        
+}
+
+local audio_play_param = {
+    type = 0,                 -- 0:文件 1:TTS 2:流式
+    content = nil,            -- 播放内容
+    cbfnc = nil,              -- 播放完毕回调
+    priority = 0,             -- 优先级(数值越大越高)
+    sampling_rate = 16000,    -- 采样率(仅流式)
+    sampling_depth = 16,      -- 采样位深(仅流式)
+    signed_or_unsigned = true -- PCM是否有符号(仅流式)
+}
+
+local audio_record_param = {
+    format = 0,               -- 录制格式,支持exaudio.AMR_NB,exaudio.AMR_WB,exaudio.PCM_8000,exaudio.PCM_16000,exaudio.PCM_24000,exaudio.PCM_32000
+    time = 5,                 -- 录制时间(秒)
+    path = nil,               -- 文件路径或流式回调
+    cbfnc = nil               -- 录音完毕回调
+}
+
+-- 内部变量
+local pcm_buff0 = nil
+local pcm_buff1 = nil
+local voice_vol = 55
+local mic_vol = 80
+
+-- 定义全局队列表
+local audio_play_queue = {
+    data = {},       -- 存储字符串的数组
+    sequenceIndex = 1  -- 用于跟踪插入顺序的索引
+}
+
+-- 向队列中添加字符串(按调用顺序插入)
+local function audio_play_queue_push(str)
+    if type(str) == "string" then
+        -- 存储格式: {index = 顺序索引, value = 字符串值}
+        table.insert(audio_play_queue.data, {
+            index = audio_play_queue.sequenceIndex,
+            value = str
+        })
+        audio_play_queue.sequenceIndex = audio_play_queue.sequenceIndex + 1
+        return true
+    end
+    return false
+end
+
+-- 从队列中取出最早插入的字符串(按顺序取出)
+local function audio_play_queue_pop()
+    if #audio_play_queue.data > 0 then
+        -- 取出并移除第一个元素
+        local item = table.remove(audio_play_queue.data, 1)
+        return item.value  -- 返回值
+    end
+    return nil
+end
+-- 清空队列中所有数据
+function audio_queue_clear()
+    -- 清空数组
+    audio_play_queue.data = {}
+    -- 重置顺序索引
+    audio_play_queue.sequenceIndex = 1
+    return true
+end
+
+-- 工具函数:参数检查
+local function check_param(param, expected_type, name)
+    if type(param) ~= expected_type then
+        log.error(string.format("参数错误: %s 应为 %s 类型", name, expected_type))
+        return false
+    end
+    return true
+end
+
+-- 音频回调处理
+local function audio_callback(id, event, point)
+    -- log.info("audio_callback", "event:", event, 
+    --         "MORE_DATA:", audio.MORE_DATA, 
+    --         "DONE:", audio.DONE,
+    --         "RECORD_DATA:", audio.RECORD_DATA,
+    --         "RECORD_DONE:", audio.RECORD_DONE)
+
+    if event == audio.MORE_DATA then
+        audio.write(MULTIMEDIA_ID,audio_play_queue_pop())
+    elseif event == audio.DONE then
+        if type(audio_play_param.cbfnc) == "function" then
+            audio_play_param.cbfnc(exaudio.PLAY_DONE)
+        end
+        audio_queue_clear()  -- 清空流式播放数据队列
+        sys.publish(EX_MSG_PLAY_DONE)
+        
+    elseif event == audio.RECORD_DATA then
+        if type(audio_record_param.path) == "function" then
+            local buff, len = point == 0 and pcm_buff0 or pcm_buff1,
+                             point == 0 and pcm_buff0:used() or pcm_buff1:used()
+            audio_record_param.path(buff, len)
+        end
+        
+    elseif event == audio.RECORD_DONE then
+        if type(audio_record_param.cbfnc) == "function" then
+            audio_record_param.cbfnc(exaudio.RECORD_DONE)
+        end
+    end
+end
+
+-- 读取ES8311芯片ID
+local function read_es8311_id()
+
+
+    -- 发送读取请求
+    local send_ok = i2c.send(audio_setup_param.i2c_id, ES8311_ADDR, CHIP_ID_REG)
+    if not send_ok then
+        log.error("发送芯片ID读取请求失败")
+        return false
+    end
+
+    -- 读取数据
+    local data = i2c.recv(audio_setup_param.i2c_id, ES8311_ADDR, 1)
+    if data and #data == 1 then
+        return true
+    end
+
+    log.error("读取ES8311芯片ID失败")
+    return false
+end
+
+-- 音频硬件初始化
+local function audio_setup()
+    -- I2C配置
+    if not i2c.setup(audio_setup_param.i2c_id, i2c.FAST) then
+        log.error("I2C初始化失败")
+        return false
+    end
+    -- 初始化I2S
+    local result, data = i2s.setup(
+        I2S_ID, 
+        I2S_MODE, 
+        I2S_SAMPLE_RATE, 
+        audio_setup_param.bits_per_sample, 
+        I2S_CHANNEL_FORMAT, 
+        I2S_COMM_FORMAT,
+        I2S_CHANNEL_BITS
+    )
+
+    if not result then
+        log.error("I2S设置失败")
+        return false
+    end
+    -- 配置音频通道
+    audio.config(
+        MULTIMEDIA_ID, 
+        audio_setup_param.pa_ctrl, 
+        audio_setup_param.pa_on_level, 
+        audio_setup_param.dac_delay, 
+        audio_setup_param.pa_delay, 
+        audio_setup_param.dac_ctrl, 
+        1,  -- power_on_level
+        audio_setup_param.dac_time_delay
+    )
+    -- 设置总线
+    audio.setBus(
+        MULTIMEDIA_ID, 
+        audio.BUS_I2S,
+        {
+            chip = audio_setup_param.model,
+            i2cid = audio_setup_param.i2c_id,
+            i2sid = I2S_ID,
+            voltage = audio.VOLTAGE_1800
+        }
+    )
+
+  
+    -- 设置音量
+    audio.vol(MULTIMEDIA_ID, voice_vol)
+    audio.micVol(MULTIMEDIA_ID, mic_vol)
+    audio.pm(MULTIMEDIA_ID, audio.RESUME)
+    
+    -- 检查芯片连接
+    if audio_setup_param.model == "es8311" and not read_es8311_id() then
+        log.error("ES8311通讯失败,请检查硬件")
+        return false
+    end
+
+    -- 注册回调
+    audio.on(MULTIMEDIA_ID, audio_callback)
+    return true
+end
+
+-- 模块接口:初始化
+function exaudio.setup(audioConfigs)
+    -- 检查必要参数
+    if not  audio  then
+        log.error("不支持audio 库,请选择支持audio 的core")
+        return false
+    end
+    if not audioConfigs or type(audioConfigs) ~= "table" then
+        log.error("配置参数必须为table类型")
+        return false
+    end
+    -- 检查codec型号
+    if not audioConfigs.model or 
+       (audioConfigs.model ~= "es8311" and audioConfigs.model ~= "es8211") then
+        log.error("请指定正确的codec型号(es8311或es8211)")
+        return false
+    end
+    audio_setup_param.model = audioConfigs.model
+    -- 针对ES8311的特殊检查
+    if audioConfigs.model == "es8311" then
+        if not check_param(audioConfigs.i2c_id, "number", "i2c_id") then
+            return false
+        end
+        audio_setup_param.i2c_id = audioConfigs.i2c_id
+    end
+
+    -- 检查功率放大器控制管脚
+    if audioConfigs.pa_ctrl == nil then
+        log.warn("pa_ctrl(功率放大器控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
+    end
+    audio_setup_param.pa_ctrl = audioConfigs.pa_ctrl
+
+    -- 检查功率放大器控制管脚
+    if audioConfigs.dac_ctrl == nil then
+        log.warn("dac_ctrl(音频编解码控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
+    end
+    audio_setup_param.dac_ctrl = audioConfigs.dac_ctrl
+
+
+    -- 处理可选参数
+    local optional_params = {
+        {name = "dac_delay", type = "number"},
+        {name = "pa_delay", type = "number"},
+        {name = "dac_time_delay", type = "number"},
+        {name = "bits_per_sample", type = "number"},
+        {name = "pa_on_level", type = "number"}
+    }
+
+    for _, param in ipairs(optional_params) do
+        if audioConfigs[param.name] ~= nil then
+            if check_param(audioConfigs[param.name], param.type, param.name) then
+                audio_setup_param[param.name] = audioConfigs[param.name]
+            else
+                return false
+            end
+        end
+    end
+
+    -- 确保采样位数有默认值
+    audio_setup_param.bits_per_sample = audio_setup_param.bits_per_sample or 16
+    return audio_setup()
+end
+
+-- 模块接口:开始播放
+function exaudio.play_start(playConfigs)
+    if not playConfigs or type(playConfigs) ~= "table" then
+        log.error("播放配置必须为table类型")
+        return false
+    end
+
+    -- 检查播放类型
+    if not check_param(playConfigs.type, "number", "type") then
+        log.error("type必须为数值(0:文件,1:TTS,2:流式)")
+        return false
+    end
+    audio_play_param.type = playConfigs.type
+
+    -- 处理优先级
+    if playConfigs.priority ~= nil then
+        if check_param(playConfigs.priority, "number", "priority") then
+            if playConfigs.priority > audio_play_param.priority then
+                log.error("是否完成播放",audio.isEnd(MULTIMEDIA_ID))
+                if not audio.isEnd(MULTIMEDIA_ID) then
+                    if audio.play(MULTIMEDIA_ID) ~= true then
+                        return false
+                    end
+                    sys.waitUntil(EX_MSG_PLAY_DONE)
+                end
+                audio_play_param.priority = playConfigs.priority
+            end
+        else
+            return false
+        end
+    end
+
+    -- 处理不同播放类型
+    local play_type = audio_play_param.type
+    if play_type == 0 then  -- 文件播放
+        if not playConfigs.content then
+            log.error("文件播放需要指定content(文件路径或路径表)")
+            return false
+        end
+
+        local content_type = type(playConfigs.content)
+        if content_type == "table" then
+            for _, path in ipairs(playConfigs.content) do
+                if type(path) ~= "string" then
+                    log.error("播放列表元素必须为字符串路径")
+                    return false
+                end
+            end
+        elseif content_type ~= "string" then
+            log.error("文件播放content必须为字符串或路径表")
+            return false
+        end
+
+        audio_play_param.content = playConfigs.content
+        if audio.play(MULTIMEDIA_ID, audio_play_param.content) ~= true then
+            return false
+        end
+
+    elseif play_type == 1 then  -- TTS播放
+        if not audio.tts then
+            log.error("本固件不支持TTS,请更换支持TTS 的固件")
+            return false
+        end
+        if not check_param(playConfigs.content, "string", "content") then
+            log.error("TTS播放content必须为字符串")
+            return false
+        end
+        audio_play_param.content = playConfigs.content
+        if audio.tts(MULTIMEDIA_ID, audio_play_param.content)  ~= true  then
+            return false
+        end
+
+    elseif play_type == 2 then  -- 流式播放
+        if not check_param(playConfigs.sampling_rate, "number", "sampling_rate") then
+            return false
+        end
+        if not check_param(playConfigs.sampling_depth, "number", "sampling_depth") then
+            return false
+        end
+
+        audio_play_param.content = playConfigs.content
+        audio_play_param.sampling_rate = playConfigs.sampling_rate
+        audio_play_param.sampling_depth = playConfigs.sampling_depth
+        
+        if playConfigs.signed_or_unsigned ~= nil then
+            audio_play_param.signed_or_unsigned = playConfigs.signed_or_unsigned
+        end
+
+        audio.start(
+            MULTIMEDIA_ID, 
+            audio.PCM, 
+            1, 
+            playConfigs.sampling_rate, 
+            playConfigs.sampling_depth, 
+            audio_play_param.signed_or_unsigned
+        )
+        -- 发送初始数据
+        if audio.write(MULTIMEDIA_ID, string.rep("\0", 512)) ~= true then
+            return false
+        end
+    end
+
+    -- 处理回调函数
+    if playConfigs.cbfnc ~= nil then
+        if check_param(playConfigs.cbfnc, "function", "cbfnc") then
+            audio_play_param.cbfnc = playConfigs.cbfnc
+        else
+            return false
+        end
+    else
+        audio_play_param.cbfnc = nil
+    end
+    return true
+end
+
+-- 模块接口:流式播放数据写入
+function exaudio.play_stream_write(data)
+    audio_play_queue_push(data)
+    return true
+end
+
+-- 模块接口:停止播放
+function exaudio.play_stop()
+    return audio.play(MULTIMEDIA_ID)
+end
+
+-- 模块接口:检查播放是否结束
+function exaudio.is_end()
+    return audio.isEnd(MULTIMEDIA_ID)
+end
+
+-- 模块接口:获取错误信息
+function exaudio.get_error()
+    return audio.getError(MULTIMEDIA_ID)
+end
+
+-- 模块接口:开始录音
+function exaudio.record_start(recodConfigs)
+    if not recodConfigs or type(recodConfigs) ~= "table" then
+        log.error("录音配置必须为table类型")
+        return false
+    end
+    -- 检查录音格式
+    if recodConfigs.format == nil or type(recodConfigs.format) ~= "number" or recodConfigs.format > 5 then
+        log.error("请指定正确的录音格式")
+        return false
+    end
+    audio_record_param.format = recodConfigs.format
+
+    -- 处理录音时间
+    if recodConfigs.time ~= nil then
+        if check_param(recodConfigs.time, "number", "time") then
+            audio_record_param.time = recodConfigs.time
+        else
+            return false
+        end
+    else
+        audio_record_param.time = 0
+    end
+
+    -- 处理存储路径/回调
+    if not recodConfigs.path then
+        log.error("必须指定录音路径或流式回调函数")
+        return false
+    end
+    audio_record_param.path = recodConfigs.path
+
+    -- 转换录音格式
+    local recod_format, amr_quailty
+    if audio_record_param.format == exaudio.AMR_NB then
+        recod_format = audio.AMR_NB
+        amr_quailty = 7
+    elseif audio_record_param.format == exaudio.AMR_WB then
+        recod_format = audio.AMR_WB
+        amr_quailty = 8
+    elseif audio_record_param.format == exaudio.PCM_8000 then
+        recod_format = 8000
+    elseif audio_record_param.format == exaudio.PCM_16000 then
+        recod_format = 16000
+    elseif audio_record_param.format == exaudio.PCM_24000 then
+        recod_format = 24000
+    elseif audio_record_param.format == exaudio.PCM_32000 then
+        recod_format = 32000
+    end
+
+    -- 处理回调函数
+    if recodConfigs.cbfnc ~= nil then
+        if check_param(recodConfigs.cbfnc, "function", "cbfnc") then
+            audio_record_param.cbfnc = recodConfigs.cbfnc
+        else
+            return false
+        end
+    else
+        audio_record_param.cbfnc = nil
+    end
+    -- 开始录音
+    local path_type = type(audio_record_param.path)
+    if path_type == "string" then
+        return audio.record(
+            MULTIMEDIA_ID, 
+            recod_format, 
+            audio_record_param.time, 
+            amr_quailty, 
+            audio_record_param.path
+        )
+    elseif path_type == "function" then
+        -- 初始化缓冲区
+        if not pcm_buff0 or not pcm_buff1 then
+            pcm_buff0 = zbuff.create(16000)
+            pcm_buff1 = zbuff.create(16000)
+        end
+        return audio.record(
+            MULTIMEDIA_ID, 
+            recod_format, 
+            audio_record_param.time, 
+            amr_quailty, 
+            nil, 
+            3,
+            pcm_buff0,
+            pcm_buff1
+        )
+    end
+    log.error("录音路径必须为字符串或函数")
+    return false
+end
+
+-- 模块接口:停止录音
+function exaudio.record_stop()
+    return audio.recordStop(MULTIMEDIA_ID)
+end
+
+-- 模块接口:设置音量
+function exaudio.vol(play_volume)
+    if check_param(play_volume, "number", "音量值") then
+        return audio.vol(MULTIMEDIA_ID, play_volume)
+    end
+    return false
+end
+
+-- 模块接口:设置麦克风音量
+function exaudio.mic_vol(record_volume)
+    if check_param(record_volume, "number", "麦克风音量值") then
+        return audio.micVol(MULTIMEDIA_ID, record_volume)  
+    end
+    return false
+end
+
+return exaudio

+ 561 - 0
module/Air8000/demo/airtalk/extalk.lua

@@ -0,0 +1,561 @@
+--[[
+@module extalk
+@summary extalk扩展库
+@version 1.1.1
+@date    2025.09.18
+@author  梁健
+@usage
+    local extalk = require "extalk"
+    -- 配置并初始化
+    extalk.setup({
+        key = "your_product_key",
+        heart_break_time = 30,
+        contact_list_cbfnc = function(dev_list) end,
+        state_cbfnc = function(state) end
+    })
+    -- 发起对讲
+    extalk.start("remote_device_id")
+    -- 结束对讲
+    extalk.stop()
+]]
+
+local extalk = {}
+
+-- 模块常量(保留原始数据结构)
+extalk.START = 1     -- 通话开始
+extalk.STOP = 2      -- 通话结束
+extalk.UNRESPONSIVE = 3  -- 未响应
+extalk.ONE_ON_ONE = 5  -- 一对一来电
+extalk.BROADCAST = 6 -- 广播
+
+local AIRTALK_TASK_NAME = "airtalk_task"
+
+-- 消息类型常量(保留原始数据结构)
+local MSG_CONNECT_ON_IND = 0
+local MSG_CONNECT_OFF_IND = 1
+local MSG_AUTH_IND = 2
+local MSG_SPEECH_ON_IND = 3
+local MSG_SPEECH_OFF_IND = 4
+local MSG_SPEECH_CONNECT_TO = 5
+local MSG_SPEECH_STOP_TEST_END = 22
+
+-- 设备状态常量(保留原始数据结构)
+local SP_T_NO_READY = 0           -- 离线状态无法对讲
+local SP_T_IDLE = 1               -- 对讲空闲状态
+local SP_T_CONNECTING = 2         -- 主动发起对讲
+local SP_T_CONNECTED = 3          -- 对讲中
+
+local SUCC = "success"
+
+-- 全局状态变量(保留原始数据结构)
+local g_state = SP_T_NO_READY   -- 设备状态
+local g_mqttc = nil             -- mqtt客户端
+local g_local_id                -- 本机ID
+local g_remote_id               -- 对端ID
+local g_s_type                  -- 对讲的模式,字符串形式
+local g_s_topic                 -- 对讲用的topic
+local g_s_mode                  -- 对讲的模式
+local g_dev_list                -- 对讲列表
+local g_dl_topic                -- 下行消息topic模板
+
+-- 配置参数
+local extalk_configs_local = {
+    key = 0,               -- 项目key,一般需要和main的PRODUCT_KEY保持一致
+    heart_break_time = 0,  -- 心跳间隔(单位秒)
+    contact_list_cbfnc = nil, -- 联系人回调函数,含设备号和昵称
+    state_cbfnc = nil,  -- 状态回调,分为对讲开始,对讲结束,未响应
+}
+
+-- 工具函数:参数检查
+local function check_param(param, expected_type, name)
+    if type(param) ~= expected_type then
+        log.error(string.format("参数错误: %s 应为 %s 类型,实际为 %s", 
+            name, expected_type, type(param)))
+        return false
+    end
+    return true
+end
+
+-- 发送鉴权消息
+local function auth()
+    if g_state == SP_T_NO_READY and g_mqttc then
+        local topic = string.format("ctrl/uplink/%s/0001", g_local_id)
+        local payload = json.encode({
+            ["key"] = extalk_configs_local.key, 
+            ["device_type"] = 1
+        })
+        g_mqttc:publish(topic, payload)
+    end
+end
+
+-- 发送心跳消息
+local function heart()
+    if g_state == SP_T_CONNECTED and g_mqttc then
+        local topic = string.format("ctrl/uplink/%s/0005", g_local_id)
+        local payload = json.encode({
+            ["from"] = g_local_id, 
+            ["to"] = g_remote_id
+        })
+        g_mqttc:publish(topic, payload)
+    end
+end
+
+-- 开始对讲
+local function speech_on(ssrc, sample)
+    g_state = SP_T_CONNECTED
+    g_mqttc:subscribe(g_s_topic)
+    airtalk.set_topic(g_s_topic)
+    airtalk.set_ssrc(ssrc)
+    log.info("对讲模式", g_s_mode)
+    airtalk.speech(true, g_s_mode, sample)
+    sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, true) 
+    sys.timerLoopStart(heart, extalk_configs_local.heart_break_time * 1000)
+    sys.timerStopAll(wait_speech_to)
+end
+
+-- 结束对讲
+local function speech_off(need_upload, need_ind)
+    if g_state == SP_T_CONNECTED then
+        g_mqttc:unsubscribe(g_s_topic)
+        airtalk.speech(false)
+        g_s_topic = nil
+    end
+    
+    g_state = SP_T_IDLE
+    sys.timerStopAll(auth)
+    sys.timerStopAll(heart)
+    sys.timerStopAll(wait_speech_to)
+    
+    if need_upload and g_mqttc then
+        local topic = string.format("ctrl/uplink/%s/0004", g_local_id)
+        g_mqttc:publish(topic, json.encode({["to"] = g_remote_id}))
+    end
+
+    if need_ind then
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_OFF_IND, true)
+    end
+end
+
+-- 对讲超时处理
+local function wait_speech_to()
+    log.info("主动请求对讲超时无应答")
+    speech_off(true, false)
+end
+
+-- 命令处理:请求对讲应答
+local function handle_speech_response(obj)
+    if g_state ~= SP_T_CONNECTING then
+        log.error("state", g_state, "need", SP_T_CONNECTING)
+        return
+    end
+
+    if obj and obj["result"] == SUCC and g_s_topic == obj["topic"] then
+        -- 开始对讲
+        local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
+        speech_on(obj["ssrc"], sample_rate)
+        return
+    else
+        log.info(obj["result"], obj["topic"], g_s_topic)
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, false)
+    end
+    
+    g_s_topic = nil
+    g_state = SP_T_IDLE
+end
+
+-- 命令处理:对端来电
+local function handle_incoming_call(obj)
+    if not obj or not obj["topic"] or not obj["ssrc"] or not obj["audio_code"] or not obj["type"] then
+        local response = {
+            ["result"] = "failed", 
+            ["topic"] = obj and obj["topic"] or "", 
+            ["info"] = "无效的请求参数"
+        }
+        g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+        return
+    end
+
+    -- 非空闲状态无法接收来电
+    if g_state ~= SP_T_IDLE then
+        log.error("state", g_state, "need", SP_T_IDLE)
+        local response = {
+            ["result"] = "failed", 
+            ["topic"] = obj["topic"], 
+            ["info"] = "device is busy"
+        }
+        g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+        return
+    end
+
+    local response, from = {}, nil
+    
+    -- 提取对端ID
+    from = string.match(obj["topic"], "audio/(.*)/.*/.*")
+    if not from then
+        response = {
+            ["result"] = "failed", 
+            ["topic"] = obj["topic"], 
+            ["info"] = "topic error"
+        }
+        g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+        return
+    end
+
+    -- 处理一对一通话
+    if obj["type"] == "one-on-one" then
+        g_s_topic = obj["topic"]
+        g_remote_id = from
+        g_s_type = "one-on-one"
+        g_s_mode = airtalk.MODE_PERSON
+        
+        -- 触发回调
+        if extalk_configs_local.state_cbfnc then
+            extalk_configs_local.state_cbfnc({
+                state = extalk.ONE_ON_ONE, 
+                id = from 
+            })
+        end
+        
+        response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
+        local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
+        speech_on(obj["ssrc"], sample_rate)
+    end
+
+    -- 处理广播
+    if obj["type"] == "broadcast" then
+        g_s_topic = obj["topic"]
+        g_remote_id = from
+        g_s_mode = airtalk.MODE_GROUP_LISTENER
+        g_s_type = "broadcast"
+        
+        -- 触发回调
+        if extalk_configs_local.state_cbfnc then
+            extalk_configs_local.state_cbfnc({
+                state = extalk.BROADCAST, 
+                id = from 
+            })
+        end
+        
+        response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
+        local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
+        speech_on(obj["ssrc"], sample_rate)
+    end
+
+    -- 发送响应
+    g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+end
+
+-- 命令处理:对端挂断
+local function handle_remote_hangup(obj)
+    local response = {}
+    
+    if g_state == SP_T_IDLE then
+        response = {["result"] = "failed", ["info"] = "no speech"}
+    else
+        log.info("0103", obj, obj["type"], g_s_type)
+        if obj and obj["type"] == g_s_type then
+            response = {["result"] = SUCC, ["info"] = ""}
+            speech_off(false, true)
+        else
+            response = {["result"] = "failed", ["info"] = "type mismatch"}
+        end
+    end
+    
+    g_mqttc:publish(string.format("ctrl/uplink/%s/8103", g_local_id), json.encode(response))
+end
+
+-- 命令处理:更新设备列表
+local function handle_device_list_update(obj)
+    local response = {}
+    if obj then
+        g_dev_list = obj["dev_list"]
+        response = {["result"] = SUCC, ["info"] = ""}
+    else
+        response = {["result"] = "failed", ["info"] = "json info error"}
+    end
+    
+    g_mqttc:publish(string.format("ctrl/uplink/%s/8101", g_local_id), json.encode(response))
+end
+
+-- 命令处理:鉴权结果
+local function handle_auth_result(obj)
+    if obj and obj["result"] == SUCC then
+        g_mqttc:publish(string.format("ctrl/uplink/%s/0002", g_local_id), "")  -- 更新列表
+    else
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, 
+            "鉴权失败" .. (obj and obj["info"] or "")) 
+    end
+end
+
+-- 命令处理:设备列表更新应答
+local function handle_device_list_response(obj)
+    if obj and obj["result"] == SUCC then
+        g_dev_list = obj["dev_list"]
+        if extalk_configs_local.contact_list_cbfnc then
+            extalk_configs_local.contact_list_cbfnc(g_dev_list)
+        end
+        g_state = SP_T_IDLE
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, true)  -- 完整登录流程结束
+    else
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "更新设备列表失败") 
+    end
+end
+
+-- 命令解析路由表
+local cmd_handlers = {
+    ["8003"] = handle_speech_response,  -- 请求对讲应答
+    ["0102"] = handle_incoming_call,    -- 平台通知对端对讲开始
+    ["0103"] = handle_remote_hangup,    -- 平台通知终端对讲结束
+    ["0101"] = handle_device_list_update,-- 平台通知终端更新对讲设备列表
+    ["8001"] = handle_auth_result,      -- 平台对鉴权应答
+    ["8002"] = handle_device_list_response -- 平台对终端获取终端列表应答
+}
+
+-- 解析接收到的消息
+local function analyze_v1(cmd, topic, obj)
+    -- 忽略心跳和结束对讲的应答
+    if cmd == "8005" or cmd == "8004" then
+        return
+    end
+    
+    -- 查找并执行对应的命令处理器
+    local handler = cmd_handlers[cmd]
+    if handler then
+        handler(obj)
+    else
+        log.warn("未处理的命令", cmd)
+    end
+end
+
+-- MQTT回调处理
+local function mqtt_cb(mqttc, event, topic, payload)
+    log.info(event, topic or "")
+    
+    if event == "conack" then
+        -- MQTT连接成功,开始自定义鉴权流程
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
+        g_mqttc:subscribe("ctrl/downlink/" .. g_local_id .. "/#")
+    elseif event == "suback" then
+        if g_state == SP_T_NO_READY then
+            if topic then
+                auth()
+            else
+                sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, 
+                    "订阅失败" .. "ctrl/downlink/" .. g_local_id .. "/#") 
+            end
+        elseif g_state == SP_T_CONNECTED and not topic then
+            speech_off(false, true)
+        end
+    elseif event == "recv" then
+        local result = string.match(topic, g_dl_topic)
+        if result then 
+            local obj = json.decode(payload)
+            analyze_v1(result, topic, obj)
+        end
+    elseif event == "disconnect" then
+        speech_off(false, true)
+        g_state = SP_T_NO_READY
+    elseif event == "error" then
+        log.error("MQTT错误发生")
+    end
+end
+
+-- 任务消息处理
+local function task_cb(msg)
+    if msg[1] == MSG_SPEECH_CONNECT_TO then
+        speech_off(true, false)
+    else
+        log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
+    end
+end
+
+-- 对讲事件回调
+local function airtalk_event_cb(event, param)
+    log.info("airtalk event", event, param)
+    if event == airtalk.EVENT_ERROR then
+        if param == airtalk.ERROR_NO_DATA then
+            log.error("长时间没有收到音频数据")
+            speech_off(true, true)
+        end
+    end
+end
+
+-- MQTT任务主循环
+local function airtalk_mqtt_task()
+    local msg, online = nil, false
+    
+    -- 初始化本地ID
+    g_local_id = mobile.imei()
+    g_dl_topic = "ctrl/downlink/" .. g_local_id .. "/(%w%w%w%w)"
+    
+    -- 创建MQTT客户端
+    g_mqttc = mqtt.create(nil, "mqtt.airtalk.luatos.com", 1883, false, {rxSize = 32768})
+    
+    -- 配置对讲参数
+    airtalk.config(airtalk.PROTOCOL_MQTT, g_mqttc, 200) -- 缓冲至少200ms播放
+    airtalk.on(airtalk_event_cb)
+    airtalk.start()
+    
+    -- 配置MQTT客户端
+    g_mqttc:auth(g_local_id, g_local_id, mobile.muid())
+    g_mqttc:keepalive(240) -- 默认值240s
+    g_mqttc:autoreconn(true, 15000) -- 自动重连机制
+    g_mqttc:debug(false)
+    g_mqttc:on(mqtt_cb)
+    
+    log.info("设备信息", g_local_id, mobile.muid())
+    
+    -- 开始连接
+    g_mqttc:connect()
+    online = false
+    
+    while true do
+        -- 等待MQTT连接成功
+        msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
+        log.info("connected")
+        
+        -- 处理登录流程
+        while not online do
+            msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, 30000) -- 30秒超时
+            
+            if type(msg) == 'table' then
+                online = msg[2]
+                if online then
+                    -- 鉴权通过,60分钟后重新鉴权
+                    sys.timerLoopStart(auth, 3600000)
+                else
+                    log.info(msg[3])
+                    -- 鉴权失败,5分钟后重试
+                    sys.timerLoopStart(auth, 300000)
+                end
+            else
+                -- 超时未收到鉴权结果,重新发送
+                auth()
+            end
+        end
+        
+        log.info("对讲管理平台已连接")
+        
+        -- 处理在线状态下的消息
+        while online do
+            msg = sys.waitMsg(AIRTALK_TASK_NAME)
+            
+            if type(msg) == 'table' and type(msg[1]) == "number" then
+                if msg[1] == MSG_SPEECH_STOP_TEST_END then
+                    if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
+                        log.info("没有对讲", g_state)
+                    else
+                        speech_off(true, false)
+                    end
+                elseif msg[1] == MSG_SPEECH_ON_IND then
+                    if extalk_configs_local.state_cbfnc then
+                        local state = msg[2] and extalk.START or extalk.UNRESPONSIVE
+                        extalk_configs_local.state_cbfnc({state = state})
+                    end
+                elseif msg[1] == MSG_SPEECH_OFF_IND then
+                    if extalk_configs_local.state_cbfnc then
+                        extalk_configs_local.state_cbfnc({state = extalk.STOP})
+                    end
+                elseif msg[1] == MSG_CONNECT_OFF_IND then
+                    log.info("connect", msg[2])
+                    online = msg[2]
+                end
+            else
+                log.info(type(msg), type(msg and msg[1]))
+            end
+            
+            msg = nil -- 清理引用
+        end
+        
+        online = false -- 重置在线状态
+    end
+end
+
+-- 模块初始化
+function extalk.setup(extalk_configs)
+    if not extalk_configs or type(extalk_configs) ~= "table" then
+        log.error("AirTalk配置必须为table类型")
+        return false
+    end
+
+    -- 检查配置参数
+    if not check_param(extalk_configs.key, "string", "key") then
+        return false
+    end
+    extalk_configs_local.key = extalk_configs.key
+
+    if not check_param(extalk_configs.heart_break_time, "number", "heart_break_time") then
+        return false
+    end
+    extalk_configs_local.heart_break_time = extalk_configs.heart_break_time
+
+    if not check_param(extalk_configs.contact_list_cbfnc, "function", "contact_list_cbfnc") then
+        return false
+    end
+    extalk_configs_local.contact_list_cbfnc = extalk_configs.contact_list_cbfnc
+
+    if not check_param(extalk_configs.state_cbfnc, "function", "state_cbfnc") then
+        return false
+    end
+    extalk_configs_local.state_cbfnc = extalk_configs.state_cbfnc
+
+    -- 启动任务
+    sys.taskInitEx(airtalk_mqtt_task, AIRTALK_TASK_NAME, task_cb)
+    return true
+end
+
+-- 开始对讲
+function extalk.start(id)
+    if g_state ~= SP_T_IDLE then
+        log.warn("正在对讲无法开始,当前状态:", g_state)
+        return false
+    end
+
+    if id == nil then
+        -- 广播模式
+        g_remote_id = "all"
+        g_state = SP_T_CONNECTING
+        g_s_mode = airtalk.MODE_GROUP_SPEAKER
+        g_s_type = "broadcast"
+        g_s_topic = string.format("audio/%s/all/%s", 
+            g_local_id, string.sub(tostring(mcu.ticks()), -4, -1))
+        
+        g_mqttc:publish(string.format("ctrl/uplink/%s/0003", g_local_id), 
+            json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
+        sys.timerStart(wait_speech_to, 15000)
+    else
+        -- 一对一模式
+        log.info("向", id, "主动发起对讲")
+        if id == g_local_id then
+            log.error("不允许本机给本机拨打电话")
+            return false
+        end
+        
+        g_state = SP_T_CONNECTING
+        g_remote_id = id
+        g_s_mode = airtalk.MODE_PERSON
+        g_s_type = "one-on-one"
+        g_s_topic = string.format("audio/%s/%s/%s", 
+            g_local_id, id, string.sub(tostring(mcu.ticks()), -4, -1))
+        
+        g_mqttc:publish(string.format("ctrl/uplink/%s/0003", g_local_id), 
+            json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
+        sys.timerStart(wait_speech_to, 15000)
+    end
+    
+    return true
+end
+
+-- 结束对讲
+function extalk.stop()
+    if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
+        log.info("没有对讲,当前状态:", g_state)
+        return false
+    end
+
+    log.info("主动断开对讲")
+    speech_off(true, false)
+    return true
+end
+
+return extalk

+ 52 - 62
module/Air8000/demo/airtalk/main.lua

@@ -1,68 +1,58 @@
---演示airtalk基本功能
---按一次boot,开始1对1对讲,再按一次boot,结束对讲
---按一次powerkey,开始1对多对讲,再按一次powerkey或者boot,结束对讲
-PROJECT = "airtalk_demo"
-VERSION = "1.0.1"
-PRODUCT_KEY = "s1uUnY6KA06ifIjcutm5oNbG3MZf5aUv" -- 到 iot.openluat.com 创建项目,获取正确的项目id
-_G.sys=require"sys"
-log.style(1)
-require "demo_define"
-require "airtalk_dev_ctrl"
-require "audio_config"
-
---errDump.config(true, 600, "airtalk_test")
-mcu.hardfault(0)
-local function boot_key_cb()
-    sys.sendMsg(USER_TASK_NAME, MSG_KEY_PRESS, false)
-end
-
-local function power_key_cb()
-    sys.sendMsg(USER_TASK_NAME, MSG_KEY_PRESS, true)
+--[[
+@module  main
+@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑
+@version 1.0
+@date    2025.09.19
+@author  梁健
+@usage
+本demo演示的核心功能为:
+
+airtalk.lua: 进行airtalk 对讲业务,相关使用说明,请见https://docs.openluat.com/value/airtalk/
+
+更多说明参考本目录下的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进行远程升级,根据自己项目的需求,自定义格式即可
+]]
+
+--[[
+本demo可直接在Air8000整机开发板上运行
+]]
+
+PROJECT = "audio"
+VERSION = "1.0.0"
+-- 在日志中打印项目名和项目版本号
+log.info("main", PROJECT, VERSION)
+
+
+-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
+-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
+if wdt then
+    --配置喂狗超时时间为9秒钟
+    wdt.init(9000)
+    --启动一个循环定时器,每隔3秒钟喂一次狗
+    sys.timerLoopStart(wdt.feed, 3000)
 end
 
---按下boot开始上传,再按下停止,加入了软件去抖,不需要长按了
-gpio.setup(0, boot_key_cb, gpio.PULLDOWN, gpio.RISING)
-gpio.debounce(0, 200, 1)
-gpio.setup(gpio.PWR_KEY, power_key_cb, gpio.PULLUP, gpio.FALLING)
-gpio.debounce(gpio.PWR_KEY, 200, 1)
 
-local test_ready = false
-local function task_cb(msg)
-    log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
-    if msg[1] == MSG_SPEECH_IND then
+require "talk"            --  启动airtalk
 
-    elseif msg[1] == MSG_NOT_READY then
-        test_ready = false
-        msg = sys.waitMsg(USER_TASK_NAME, MSG_KEY_PRESS)
-    end
-end
-
-local function user_task()
-    audio_init()
-    airtalk_mqtt_init()
-    local msg
-    while true do
-        msg = sys.waitMsg(USER_TASK_NAME, MSG_KEY_PRESS)
-        if msg[2] then  -- true powerkey false boot key
-            sys.sendMsg(AIRTALK_TASK_NAME, MSG_GROUP_SPEECH_TEST_START)   --测试阶段自动给一个device打
-        else
-            sys.sendMsg(AIRTALK_TASK_NAME, MSG_PERSON_SPEECH_TEST_START)   --测试阶段自动给一个device打
-        end 
-        msg = sys.waitMsg(USER_TASK_NAME, MSG_KEY_PRESS)
-        sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_STOP_TEST_END)        --再按一次就自动挂断
-    end
-end
+-- 音频对内存影响较大,不断的打印内存,用于判断是否异常
+sys.timerLoopStart(function()
+    log.info("mem.lua", rtos.meminfo())
+    log.info("mem.sys", rtos.meminfo("sys"))
+ end, 3000)
 
-sys.taskInitEx(user_task, USER_TASK_NAME, task_cb)
+-- 用户代码已结束---------------------------------------------
+-- 结尾总是这一句
+sys.run()
+-- sys.run()之后后面不要加任何语句!!!!!
 
---定期检查ram使用情况,及时发现内存泄露
-sys.taskInit(function()
-    while true do
-        sys.wait(60000)
-        log.info("time", os.time())
-        log.info("lua", rtos.meminfo("lua"))
-        log.info("sys", rtos.meminfo("sys"))
-        log.info("psram", rtos.meminfo("psram"))
-    end
-end)
-sys.run()

+ 112 - 0
module/Air8000/demo/airtalk/readme.md

@@ -0,0 +1,112 @@
+## 总体设计框图
+
+ 
+
+
+
+## 功能模块介绍
+
+1、main.lua:主程序入口;
+
+2、play_file.lua: 播放音频文件,可支持wav,amr,mp3 格式音频
+
+3、play_tts: 支持文字转普通话输出需要固件支持
+
+4、play_stream: 流式播放音频,仅支持PCM 格式,可以将音频推流到云端,用来对接大模型或者流式录音的应用。
+
+5、record_file: 录音到文件,仅支持PCM 格式
+
+6、record_stream:  流式录音,仅支持PCM,可以将音频流不断的拉取,可用来对接大模型
+
+7、1.mp3: 用于测试本地mp3文件播放
+
+8、test.pcm: 用于测试pcm 流式播放(实际可以云端下载)
+
+
+
+
+
+## 常量的介绍
+
+1、exaudio.PLAY_DONE : 当播放音频结束时,会在回调函数返回播放完成的时间
+
+2、exaudio.RECORD_DONE : 当录音结束时,会在回调函数返回播放完成的时间
+
+3、exaudio.AMR_NB : 仅录音时有用,表示使用AMR_NB 方式录音
+
+4、exaudio.AMR_WB : 仅录音时有用,表示使用AMR_WB 方式录音
+
+5、exaudio.PCM_8000/exaudio.PCM_16000/exaudio.PCM_24000/exaudio.PCM_32000 :  仅录音时有用,表示使用8000/16000/24000/32000/秒 的速度对音频进行采样
+
+
+## 演示功能概述
+
+1、play_flie.lua 自动播放一个1.mp3音乐,点powerkey 按键进行音频切换,点击boot 按键停止音频播放
+
+2、play_tts.lua 播放一个TTS,点powerkey 按键进行tts 的音色切换,点击boot 按键停止音频播放
+
+3、play_stream.lua 流式播放PCM,使用test.pcm 模拟音频来源,通过流式传输不断填入播放的音频,使用powerkey 按键进行音量减小,点击boot 按键进行音量增加
+
+4、record_file.lua 录音到文件,演示了pcm 录音到文件,使用powerkey 按键进行录音音量减小,点击boot 按键进行录音音量增加
+
+5、record_stream.lua 流式录音(仅支持PCM),不断输出录音的数据地址和录音长度,供给应用层调用
+
+
+## 演示硬件环境
+
+![](https://docs.openluat.com/air8000/luatos/app/image/netdrv_multi.jpg)
+
+1、Air8000开发板一块
+
+2、喇叭一个
+
+2、插入喇叭到开发板中
+
+
+## 演示软件环境
+
+1、Luatools下载调试工具
+
+2、[Air8000 V2014版本固件](https://docs.openluat.com/air8000/luatos/firmware/)(理论上,2025年7月26日之后发布的固件都可以)
+
+
+## 演示核心步骤
+
+1、搭建好硬件环境
+
+2、demo脚本代码main.lua中,按照自己的需求选择对应的功能
+
+- 如果需要测试播放音频文件,则选择play_file 文件
+
+- 如果需要测试播放tts,则选择play_tts 文件
+
+- 如果需要测试流式播放音频,则选择play_stream 文件
+
+- 如果需要测试录音频到文件,则选择record_file 文件
+
+- 如果需要测试流式录音,则选择record_stream 文件
+
+
+3、Luatools烧录内核固件和修改后的demo脚本代码
+
+4、烧录成功后,自动开机运行,如果出现以下日志,播放或者或者录音完成
+
+``` lua
+I/user.播放完成 true
+I/user.录音完成 
+I/user.录音后文件大小 
+```
+
+5、 在测试播放音频文件的时候,点powerkey 按键进行音频切换,切换内容是MP3,AMR格式,切换是通过播放优先级进行区分的,注意音频格式仅仅支持:MP3,WAV,AMR,点击boot 按键停止音频播放
+
+6、 在测试播放TTS的时候,点powerkey 按键进行TTS 音色切换,点击boot 按键停止音频播放,注意:仅支持中文TTS。
+
+
+7、在进行流式播放测试的时候,使用test.pcm 模拟音频来源,通过流式传输不断填入播放的音频,使用powerkey 按键进行音量减小,点击boot 按键进行音量增加,注意流式播放目前仅支持PCM 格式音频,可选择不同的采样率,以及位深
+
+8、在测试录音到文件(仅支持PCM),演示了pcm 录音到文件,使用powerkey 按键进行录音音量减小,点击boot 按键进行录音音量增加
+
+9、在测试流式录音(仅支持PCM),不断输出录音的数据地址和录音长度,供给应用层调用
+
+
+

+ 196 - 0
module/Air8000/demo/airtalk/talk.lua

@@ -0,0 +1,196 @@
+--[[
+    演示airtalk基本功能
+    按一次boot,开始1对1对讲,再按一次boot,结束对讲
+    按一次powerkey,开始1对多对讲,再按一次powerkey或者boot,结束对讲
+]]
+
+-- 引入必要模块
+
+local extalk = require "extalk"
+local exaudio = require "exaudio"
+
+-- 配置日志格式
+log.style(1)
+
+-- 常量定义
+local USER_TASK_NAME = "user_task"
+local MSG_KEY_PRESS = 12  -- 按键消息
+
+-- 全局状态变量
+local g_dev_list = nil    -- 设备列表
+local g_speech_active = false  -- 对讲状态标记
+
+-- 音频初始化参数
+local audio_setup_param = {
+    model = "es8311",       -- 音频编解码类型,可填入"es8311","es8211"
+    i2c_id = 0,             -- i2c_id,可填入0,1 并使用pins工具配置对应的管脚
+    pa_ctrl = 162,          -- 音频放大器电源控制管脚
+    dac_ctrl = 164,         -- 音频编解码芯片电源控制管脚    
+}
+
+-- 联系人列表回调函数
+local function contact_list_callback(dev_list)
+    g_dev_list = dev_list
+    if dev_list and #dev_list > 0 then
+        log.info("联系人列表更新:")
+        for i = 1, #dev_list do
+            log.info(string.format("  %d. ID: %s, 名称: %s", 
+                i, dev_list[i]["id"], dev_list[i]["name"] or "未知"))
+        end
+    else
+        log.info("联系人列表为空")
+    end
+end
+
+-- 对讲状态回调函数
+local function speech_state_callback(event_table)
+    if not event_table then return end
+    
+    if event_table.state == extalk.START then
+        log.info("对讲开始,可以说话了")
+        g_speech_active = true
+    elseif event_table.state == extalk.STOP then
+        log.info("对讲结束")
+        g_speech_active = false
+    elseif event_table.state == extalk.UNRESPONSIVE then
+        log.info("对端未响应")
+        g_speech_active = false
+    elseif event_table.state == extalk.ONE_ON_ONE then
+        g_speech_active = true
+        
+        local dev_name = "未知设备"
+        if g_dev_list then
+            for i = 1, #g_dev_list do
+                if g_dev_list[i]["id"] == event_table.id then
+                    dev_name = g_dev_list[i]["name"] or "未知设备"
+                    break
+                end
+            end
+        end
+        log.info(string.format("%s 来电", dev_name))
+    elseif event_table.state == extalk.BROADCAST then
+        g_speech_active = true
+        
+        local dev_name = "未知设备"
+        if g_dev_list then
+            for i = 1, #g_dev_list do
+                if g_dev_list[i]["id"] == event_table.id then
+                    dev_name = g_dev_list[i]["name"] or "未知设备"
+                    break
+                end
+            end
+        end
+        log.info(string.format("%s 开始广播", dev_name))
+    end
+end
+
+-- extalk配置参数
+local extalk_configs = {
+    key = PRODUCT_KEY,
+    heart_break_time = 120,  -- 心跳间隔(单位秒)
+    contact_list_cbfnc = contact_list_callback,
+    state_cbfnc = speech_state_callback,
+}
+
+-- 按键回调函数 - Boot键
+local function boot_key_callback()
+    sys.sendMsg(USER_TASK_NAME, MSG_KEY_PRESS, false)  -- false表示Boot键
+end
+
+-- 按键回调函数 - Power键
+local function power_key_callback()
+    sys.sendMsg(USER_TASK_NAME, MSG_KEY_PRESS, true)   -- true表示Power键
+end
+
+-- 初始化按键
+local function init_buttons()
+    -- 配置Boot键 (GPIO0)
+    gpio.setup(0, boot_key_callback, gpio.PULLDOWN, gpio.RISING)
+    gpio.debounce(0, 200, 1)  -- 200ms去抖
+    
+    -- 配置Power键
+    gpio.setup(gpio.PWR_KEY, power_key_callback, gpio.PULLUP, gpio.FALLING)
+    gpio.debounce(gpio.PWR_KEY, 200, 1)  -- 200ms去抖
+end
+
+-- 查找第一个可用的对端设备ID
+local function find_first_remote_device()
+    if not g_dev_list or #g_dev_list == 0 then
+        log.warn("没有找到可用的设备")
+        return nil
+    end
+    
+    local local_id = mobile.imei()
+    for i = 1, #g_dev_list do
+        local dev_id = g_dev_list[i]["id"]
+        if dev_id and dev_id ~= local_id then
+            return dev_id
+        end
+    end
+    
+    log.warn("没有找到其他可用设备")
+    return nil
+end
+
+-- 处理按键消息
+local function handle_key_press(is_power_key)
+    if g_speech_active then
+        -- 当前正在对讲,按任何键都结束对讲
+        log.info("结束当前对讲")
+        extalk.stop()
+        g_speech_active = false
+    else
+        -- 当前未在对讲,根据按键类型开始不同对讲
+        if is_power_key then
+            -- Power键:开始一对多广播
+            log.info("开始一对多广播")
+            extalk.start()  -- 不带参数表示广播
+        else
+            -- Boot键:开始一对一对讲
+            log.info("开始一对一对讲")
+            local remote_id = find_first_remote_device()
+            if remote_id then
+                extalk.start(remote_id)
+            else
+                log.error("无法开始一对一对讲,没有找到可用设备")
+            end
+        end
+    end
+end
+
+
+
+-- 用户主任务
+local function user_main_task()
+    -- 初始化音频
+    local audio_init_ok = exaudio.setup(audio_setup_param)
+    if not audio_init_ok then
+        log.error("音频初始化失败")
+        return
+    end
+    log.info("音频初始化成功")
+    
+    -- 初始化extalk
+    local extalk_init_ok = extalk.setup(extalk_configs)
+    if not extalk_init_ok then
+        log.error("extalk初始化失败")
+        return
+    end
+    log.info("extalk初始化成功")
+    
+    -- 等待按键消息并处理
+    while true do
+        local msg = sys.waitMsg(USER_TASK_NAME, MSG_KEY_PRESS)
+        if msg and msg[1] == MSG_KEY_PRESS then
+            handle_key_press(msg[2])  -- msg[2]区分是Power键(true)还是Boot键(false)
+        end
+    end
+end
+
+-- 初始化按键
+init_buttons()
+
+-- 启动用户任务
+sys.taskInitEx(user_main_task, USER_TASK_NAME)
+
+

+ 1 - 1
module/Air8000/demo/audio/exaudio.lua

@@ -111,7 +111,7 @@ local function check_param(param, expected_type, name)
         log.error(string.format("参数错误: %s 应为 %s 类型", name, expected_type))
         return false
     end
-    return true
+    return true 
 end
 
 -- 音频回调处理

+ 9 - 55
module/Air8000/project/整机开发板出厂工程/user/airaudio.lua

@@ -1,66 +1,20 @@
 local airaudio = {}
 
-local i2c_id = 0            -- i2c_id 0
+exaudio = require("exaudio")
 
-
-local pa_pin = 162           -- 喇叭pa功放脚
-local power_pin = 164         -- es8311电源脚
-
-
-local i2s_id = 0            -- i2s_id 0
-local i2s_mode = 0          -- i2s模式 0 主机 1 从机
-local i2s_sample_rate = 16000   -- 采样率
-local i2s_bits_per_sample = 16  -- 数据位数
-local i2s_channel_format = i2s.MONO_R   -- 声道, 0 左声道, 1 右声道, 2 立体声
-local i2s_communication_format = i2s.MODE_LSB   -- 格式, 可选MODE_I2S, MODE_LSB, MODE_MSB
-local i2s_channel_bits = 16     -- 声道的BCLK数量
-
-local multimedia_id = 0         -- 音频通道 0
-local pa_on_level = 1           -- PA打开电平 1 高电平 0 低电平
-local power_delay = 3           -- 在DAC启动前插入的冗余时间,单位100ms
-local pa_delay = 100            -- 在DAC启动后,延迟多长时间打开PA,单位1ms
-local power_on_level = 1        -- 电源控制IO的电平,默认拉高
-local power_time_delay = 600    -- 音频播放完毕时,PA与DAC关闭的时间间隔,单位1ms
-local taskName = "task_tts"
-
-local play_string = "降功耗,找合宙"
-local voice_vol = 55        -- 喇叭音量
-local mic_vol = 80          -- 麦克风音量
-
-function audio_setup()
-    sys.wait(100)
-
-    i2c.setup(i2c_id,i2c.FAST)
-    i2s.setup(i2s_id, i2s_mode, i2s_sample_rate, i2s_bits_per_sample, i2s_channel_format, i2s_communication_format,i2s_channel_bits)
-
-    audio.config(multimedia_id, pa_pin, pa_on_level, power_delay, pa_delay, power_pin, power_on_level, power_time_delay)
-    audio.setBus(multimedia_id, audio.BUS_I2S,{chip = "es8311",i2cid = i2c_id , i2sid = i2s_id, voltage = audio.VOLTAGE_1800})	--通道0的硬件输出通道设置为I2S
-
-    audio.vol(multimedia_id, voice_vol)
-    audio.micVol(multimedia_id, mic_vol)
-
-end
-
-local function audio_callback(id, event)
-    local succ,stop,file_cnt = audio.getError(0)
-    if not succ then
-        if stop then
-            log.info("用户停止播放")
-        else
-            log.info("第", file_cnt, "个文件解码失败")
-        end
-    end
-    sysplus.sendMsg(taskName, MSG_PD)
-end
+-- 音频初始化设置参数,exaudio.setup 传入参数
+local audio_setup_param ={
+    model= "es8311",          -- 音频编解码类型,可填入"es8311","es8211"
+    i2c_id = 0,          -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚
+    pa_ctrl = 162,         -- 音频放大器电源控制管脚
+    dac_ctrl = 164,        --  音频编解码芯片电源控制管脚    
+}
 
 
 function airaudio.init()
     gpio.setup(24, 1, gpio.PULLUP)          -- i2c工作的电压域
     sys.wait(100)
-    gpio.setup(power_pin, 1, gpio.PULLUP)   -- 打开音频编解码供电
-    gpio.setup(pa_pin, 1, gpio.PULLUP)      -- 打开音频放大器
-    audio_setup()
-    audio.on(0, audio_callback)
+    exaudio.setup(audio_setup_param)
 end
 
 

+ 541 - 0
module/Air8000/project/整机开发板出厂工程/user/exaudio.lua

@@ -0,0 +1,541 @@
+--[[
+@module exaudio
+@summary exaudio扩展库
+@version 1.1
+@date    2025.09.01
+@author  梁健
+@usage
+]]
+local exaudio = {}
+
+-- 常量定义
+local I2S_ID = 0
+local I2S_MODE = 0          -- 0:主机 1:从机
+local I2S_SAMPLE_RATE = 16000
+local I2S_CHANNEL_FORMAT = i2s.MONO_R  
+local I2S_COMM_FORMAT = i2s.MODE_LSB   -- 可选MODE_I2S, MODE_LSB, MODE_MSB
+local I2S_CHANNEL_BITS = 16
+local MULTIMEDIA_ID = 0
+local EX_MSG_PLAY_DONE = "playDone"
+local ES8311_ADDR = 0x18    -- 7位地址
+local CHIP_ID_REG = 0x00    -- 芯片ID寄存器地址
+
+-- 模块常量
+exaudio.PLAY_DONE = 1         --   音频播放完毕的事件之一
+exaudio.RECORD_DONE = 1       --   音频录音完毕的事件之一  
+exaudio.AMR_NB = 0
+exaudio.AMR_WB = 1
+exaudio.PCM_8000 = 2
+exaudio.PCM_16000 = 3 
+exaudio.PCM_24000 = 4
+exaudio.PCM_32000 = 5
+
+
+-- 默认配置参数
+local audio_setup_param = {
+    model = "es8311",         -- dac类型: "es8311","es8211"
+    i2c_id = 0,               -- i2c_id: 0,1
+    pa_ctrl = 0,              -- 音频放大器电源控制管脚
+    dac_ctrl = 0,             -- 音频编解码芯片电源控制管脚
+    dac_delay = 3,            -- DAC启动前冗余时间(100ms)
+    pa_delay = 100,           -- DAC启动后延迟打开PA的时间(ms)
+    dac_time_delay = 600,     -- 播放完毕后PA与DAC关闭间隔(ms)
+    bits_per_sample = 16,     -- 采样位数
+    pa_on_level = 1           -- PA打开电平 1:高 0:低        
+}
+
+local audio_play_param = {
+    type = 0,                 -- 0:文件 1:TTS 2:流式
+    content = nil,            -- 播放内容
+    cbfnc = nil,              -- 播放完毕回调
+    priority = 0,             -- 优先级(数值越大越高)
+    sampling_rate = 16000,    -- 采样率(仅流式)
+    sampling_depth = 16,      -- 采样位深(仅流式)
+    signed_or_unsigned = true -- PCM是否有符号(仅流式)
+}
+
+local audio_record_param = {
+    format = 0,               -- 录制格式,支持exaudio.AMR_NB,exaudio.AMR_WB,exaudio.PCM_8000,exaudio.PCM_16000,exaudio.PCM_24000,exaudio.PCM_32000
+    time = 5,                 -- 录制时间(秒)
+    path = nil,               -- 文件路径或流式回调
+    cbfnc = nil               -- 录音完毕回调
+}
+
+-- 内部变量
+local pcm_buff0 = nil
+local pcm_buff1 = nil
+local voice_vol = 55
+local mic_vol = 80
+
+-- 定义全局队列表
+local audio_play_queue = {
+    data = {},       -- 存储字符串的数组
+    sequenceIndex = 1  -- 用于跟踪插入顺序的索引
+}
+
+-- 向队列中添加字符串(按调用顺序插入)
+local function audio_play_queue_push(str)
+    if type(str) == "string" then
+        -- 存储格式: {index = 顺序索引, value = 字符串值}
+        table.insert(audio_play_queue.data, {
+            index = audio_play_queue.sequenceIndex,
+            value = str
+        })
+        audio_play_queue.sequenceIndex = audio_play_queue.sequenceIndex + 1
+        return true
+    end
+    return false
+end
+
+-- 从队列中取出最早插入的字符串(按顺序取出)
+local function audio_play_queue_pop()
+    if #audio_play_queue.data > 0 then
+        -- 取出并移除第一个元素
+        local item = table.remove(audio_play_queue.data, 1)
+        return item.value  -- 返回值
+    end
+    return nil
+end
+-- 清空队列中所有数据
+function audio_queue_clear()
+    -- 清空数组
+    audio_play_queue.data = {}
+    -- 重置顺序索引
+    audio_play_queue.sequenceIndex = 1
+    return true
+end
+
+-- 工具函数:参数检查
+local function check_param(param, expected_type, name)
+    if type(param) ~= expected_type then
+        log.error(string.format("参数错误: %s 应为 %s 类型", name, expected_type))
+        return false
+    end
+    return true
+end
+
+-- 音频回调处理
+local function audio_callback(id, event, point)
+    -- log.info("audio_callback", "event:", event, 
+    --         "MORE_DATA:", audio.MORE_DATA, 
+    --         "DONE:", audio.DONE,
+    --         "RECORD_DATA:", audio.RECORD_DATA,
+    --         "RECORD_DONE:", audio.RECORD_DONE)
+
+    if event == audio.MORE_DATA then
+        audio.write(MULTIMEDIA_ID,audio_play_queue_pop())
+    elseif event == audio.DONE then
+        if type(audio_play_param.cbfnc) == "function" then
+            audio_play_param.cbfnc(exaudio.PLAY_DONE)
+        end
+        audio_queue_clear()  -- 清空流式播放数据队列
+        sys.publish(EX_MSG_PLAY_DONE)
+        
+    elseif event == audio.RECORD_DATA then
+        if type(audio_record_param.path) == "function" then
+            local buff, len = point == 0 and pcm_buff0 or pcm_buff1,
+                             point == 0 and pcm_buff0:used() or pcm_buff1:used()
+            audio_record_param.path(buff, len)
+        end
+        
+    elseif event == audio.RECORD_DONE then
+        if type(audio_record_param.cbfnc) == "function" then
+            audio_record_param.cbfnc(exaudio.RECORD_DONE)
+        end
+    end
+end
+
+-- 读取ES8311芯片ID
+local function read_es8311_id()
+
+
+    -- 发送读取请求
+    local send_ok = i2c.send(audio_setup_param.i2c_id, ES8311_ADDR, CHIP_ID_REG)
+    if not send_ok then
+        log.error("发送芯片ID读取请求失败")
+        return false
+    end
+
+    -- 读取数据
+    local data = i2c.recv(audio_setup_param.i2c_id, ES8311_ADDR, 1)
+    if data and #data == 1 then
+        return true
+    end
+
+    log.error("读取ES8311芯片ID失败")
+    return false
+end
+
+-- 音频硬件初始化
+local function audio_setup()
+    -- I2C配置
+    if not i2c.setup(audio_setup_param.i2c_id, i2c.FAST) then
+        log.error("I2C初始化失败")
+        return false
+    end
+    -- 初始化I2S
+    local result, data = i2s.setup(
+        I2S_ID, 
+        I2S_MODE, 
+        I2S_SAMPLE_RATE, 
+        audio_setup_param.bits_per_sample, 
+        I2S_CHANNEL_FORMAT, 
+        I2S_COMM_FORMAT,
+        I2S_CHANNEL_BITS
+    )
+
+    if not result then
+        log.error("I2S设置失败")
+        return false
+    end
+    -- 配置音频通道
+    audio.config(
+        MULTIMEDIA_ID, 
+        audio_setup_param.pa_ctrl, 
+        audio_setup_param.pa_on_level, 
+        audio_setup_param.dac_delay, 
+        audio_setup_param.pa_delay, 
+        audio_setup_param.dac_ctrl, 
+        1,  -- power_on_level
+        audio_setup_param.dac_time_delay
+    )
+    -- 设置总线
+    audio.setBus(
+        MULTIMEDIA_ID, 
+        audio.BUS_I2S,
+        {
+            chip = audio_setup_param.model,
+            i2cid = audio_setup_param.i2c_id,
+            i2sid = I2S_ID,
+            voltage = audio.VOLTAGE_1800
+        }
+    )
+
+  
+    -- 设置音量
+    audio.vol(MULTIMEDIA_ID, voice_vol)
+    audio.micVol(MULTIMEDIA_ID, mic_vol)
+    audio.pm(MULTIMEDIA_ID, audio.RESUME)
+    
+    -- 检查芯片连接
+    if audio_setup_param.model == "es8311" and not read_es8311_id() then
+        log.error("ES8311通讯失败,请检查硬件")
+        return false
+    end
+
+    -- 注册回调
+    audio.on(MULTIMEDIA_ID, audio_callback)
+    return true
+end
+
+-- 模块接口:初始化
+function exaudio.setup(audioConfigs)
+    -- 检查必要参数
+    if not  audio  then
+        log.error("不支持audio 库,请选择支持audio 的core")
+        return false
+    end
+    if not audioConfigs or type(audioConfigs) ~= "table" then
+        log.error("配置参数必须为table类型")
+        return false
+    end
+    -- 检查codec型号
+    if not audioConfigs.model or 
+       (audioConfigs.model ~= "es8311" and audioConfigs.model ~= "es8211") then
+        log.error("请指定正确的codec型号(es8311或es8211)")
+        return false
+    end
+    audio_setup_param.model = audioConfigs.model
+    -- 针对ES8311的特殊检查
+    if audioConfigs.model == "es8311" then
+        if not check_param(audioConfigs.i2c_id, "number", "i2c_id") then
+            return false
+        end
+        audio_setup_param.i2c_id = audioConfigs.i2c_id
+    end
+
+    -- 检查功率放大器控制管脚
+    if audioConfigs.pa_ctrl == nil then
+        log.warn("pa_ctrl(功率放大器控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
+    end
+    audio_setup_param.pa_ctrl = audioConfigs.pa_ctrl
+
+    -- 检查功率放大器控制管脚
+    if audioConfigs.dac_ctrl == nil then
+        log.warn("dac_ctrl(音频编解码控制管脚)是控制pop 音的重要管脚,建议硬件设计加上")
+    end
+    audio_setup_param.dac_ctrl = audioConfigs.dac_ctrl
+
+
+    -- 处理可选参数
+    local optional_params = {
+        {name = "dac_delay", type = "number"},
+        {name = "pa_delay", type = "number"},
+        {name = "dac_time_delay", type = "number"},
+        {name = "bits_per_sample", type = "number"},
+        {name = "pa_on_level", type = "number"}
+    }
+
+    for _, param in ipairs(optional_params) do
+        if audioConfigs[param.name] ~= nil then
+            if check_param(audioConfigs[param.name], param.type, param.name) then
+                audio_setup_param[param.name] = audioConfigs[param.name]
+            else
+                return false
+            end
+        end
+    end
+
+    -- 确保采样位数有默认值
+    audio_setup_param.bits_per_sample = audio_setup_param.bits_per_sample or 16
+    return audio_setup()
+end
+
+-- 模块接口:开始播放
+function exaudio.play_start(playConfigs)
+    if not playConfigs or type(playConfigs) ~= "table" then
+        log.error("播放配置必须为table类型")
+        return false
+    end
+
+    -- 检查播放类型
+    if not check_param(playConfigs.type, "number", "type") then
+        log.error("type必须为数值(0:文件,1:TTS,2:流式)")
+        return false
+    end
+    audio_play_param.type = playConfigs.type
+
+    -- 处理优先级
+    if playConfigs.priority ~= nil then
+        if check_param(playConfigs.priority, "number", "priority") then
+            if playConfigs.priority > audio_play_param.priority then
+                log.error("是否完成播放",audio.isEnd(MULTIMEDIA_ID))
+                if not audio.isEnd(MULTIMEDIA_ID) then
+                    if audio.play(MULTIMEDIA_ID) ~= true then
+                        return false
+                    end
+                    sys.waitUntil(EX_MSG_PLAY_DONE)
+                end
+                audio_play_param.priority = playConfigs.priority
+            end
+        else
+            return false
+        end
+    end
+
+    -- 处理不同播放类型
+    local play_type = audio_play_param.type
+    if play_type == 0 then  -- 文件播放
+        if not playConfigs.content then
+            log.error("文件播放需要指定content(文件路径或路径表)")
+            return false
+        end
+
+        local content_type = type(playConfigs.content)
+        if content_type == "table" then
+            for _, path in ipairs(playConfigs.content) do
+                if type(path) ~= "string" then
+                    log.error("播放列表元素必须为字符串路径")
+                    return false
+                end
+            end
+        elseif content_type ~= "string" then
+            log.error("文件播放content必须为字符串或路径表")
+            return false
+        end
+
+        audio_play_param.content = playConfigs.content
+        if audio.play(MULTIMEDIA_ID, audio_play_param.content) ~= true then
+            return false
+        end
+
+    elseif play_type == 1 then  -- TTS播放
+        if not audio.tts then
+            log.error("本固件不支持TTS,请更换支持TTS 的固件")
+            return false
+        end
+        if not check_param(playConfigs.content, "string", "content") then
+            log.error("TTS播放content必须为字符串")
+            return false
+        end
+        audio_play_param.content = playConfigs.content
+        if audio.tts(MULTIMEDIA_ID, audio_play_param.content)  ~= true  then
+            return false
+        end
+
+    elseif play_type == 2 then  -- 流式播放
+        if not check_param(playConfigs.sampling_rate, "number", "sampling_rate") then
+            return false
+        end
+        if not check_param(playConfigs.sampling_depth, "number", "sampling_depth") then
+            return false
+        end
+
+        audio_play_param.content = playConfigs.content
+        audio_play_param.sampling_rate = playConfigs.sampling_rate
+        audio_play_param.sampling_depth = playConfigs.sampling_depth
+        
+        if playConfigs.signed_or_unsigned ~= nil then
+            audio_play_param.signed_or_unsigned = playConfigs.signed_or_unsigned
+        end
+
+        audio.start(
+            MULTIMEDIA_ID, 
+            audio.PCM, 
+            1, 
+            playConfigs.sampling_rate, 
+            playConfigs.sampling_depth, 
+            audio_play_param.signed_or_unsigned
+        )
+        -- 发送初始数据
+        if audio.write(MULTIMEDIA_ID, string.rep("\0", 512)) ~= true then
+            return false
+        end
+    end
+
+    -- 处理回调函数
+    if playConfigs.cbfnc ~= nil then
+        if check_param(playConfigs.cbfnc, "function", "cbfnc") then
+            audio_play_param.cbfnc = playConfigs.cbfnc
+        else
+            return false
+        end
+    else
+        audio_play_param.cbfnc = nil
+    end
+    return true
+end
+
+-- 模块接口:流式播放数据写入
+function exaudio.play_stream_write(data)
+    audio_play_queue_push(data)
+    return true
+end
+
+-- 模块接口:停止播放
+function exaudio.play_stop()
+    return audio.play(MULTIMEDIA_ID)
+end
+
+-- 模块接口:检查播放是否结束
+function exaudio.is_end()
+    return audio.isEnd(MULTIMEDIA_ID)
+end
+
+-- 模块接口:获取错误信息
+function exaudio.get_error()
+    return audio.getError(MULTIMEDIA_ID)
+end
+
+-- 模块接口:开始录音
+function exaudio.record_start(recodConfigs)
+    if not recodConfigs or type(recodConfigs) ~= "table" then
+        log.error("录音配置必须为table类型")
+        return false
+    end
+    -- 检查录音格式
+    if recodConfigs.format == nil or type(recodConfigs.format) ~= "number" or recodConfigs.format > 5 then
+        log.error("请指定正确的录音格式")
+        return false
+    end
+    audio_record_param.format = recodConfigs.format
+
+    -- 处理录音时间
+    if recodConfigs.time ~= nil then
+        if check_param(recodConfigs.time, "number", "time") then
+            audio_record_param.time = recodConfigs.time
+        else
+            return false
+        end
+    else
+        audio_record_param.time = 0
+    end
+
+    -- 处理存储路径/回调
+    if not recodConfigs.path then
+        log.error("必须指定录音路径或流式回调函数")
+        return false
+    end
+    audio_record_param.path = recodConfigs.path
+
+    -- 转换录音格式
+    local recod_format, amr_quailty
+    if audio_record_param.format == exaudio.AMR_NB then
+        recod_format = audio.AMR_NB
+        amr_quailty = 7
+    elseif audio_record_param.format == exaudio.AMR_WB then
+        recod_format = audio.AMR_WB
+        amr_quailty = 8
+    elseif audio_record_param.format == exaudio.PCM_8000 then
+        recod_format = 8000
+    elseif audio_record_param.format == exaudio.PCM_16000 then
+        recod_format = 16000
+    elseif audio_record_param.format == exaudio.PCM_24000 then
+        recod_format = 24000
+    elseif audio_record_param.format == exaudio.PCM_32000 then
+        recod_format = 32000
+    end
+
+    -- 处理回调函数
+    if recodConfigs.cbfnc ~= nil then
+        if check_param(recodConfigs.cbfnc, "function", "cbfnc") then
+            audio_record_param.cbfnc = recodConfigs.cbfnc
+        else
+            return false
+        end
+    else
+        audio_record_param.cbfnc = nil
+    end
+    -- 开始录音
+    local path_type = type(audio_record_param.path)
+    if path_type == "string" then
+        return audio.record(
+            MULTIMEDIA_ID, 
+            recod_format, 
+            audio_record_param.time, 
+            amr_quailty, 
+            audio_record_param.path
+        )
+    elseif path_type == "function" then
+        -- 初始化缓冲区
+        if not pcm_buff0 or not pcm_buff1 then
+            pcm_buff0 = zbuff.create(16000)
+            pcm_buff1 = zbuff.create(16000)
+        end
+        return audio.record(
+            MULTIMEDIA_ID, 
+            recod_format, 
+            audio_record_param.time, 
+            amr_quailty, 
+            nil, 
+            3,
+            pcm_buff0,
+            pcm_buff1
+        )
+    end
+    log.error("录音路径必须为字符串或函数")
+    return false
+end
+
+-- 模块接口:停止录音
+function exaudio.record_stop()
+    return audio.recordStop(MULTIMEDIA_ID)
+end
+
+-- 模块接口:设置音量
+function exaudio.vol(play_volume)
+    if check_param(play_volume, "number", "音量值") then
+        return audio.vol(MULTIMEDIA_ID, play_volume)
+    end
+    return false
+end
+
+-- 模块接口:设置麦克风音量
+function exaudio.mic_vol(record_volume)
+    if check_param(record_volume, "number", "麦克风音量值") then
+        return audio.micVol(MULTIMEDIA_ID, record_volume)  
+    end
+    return false
+end
+
+return exaudio

+ 561 - 0
module/Air8000/project/整机开发板出厂工程/user/extalk.lua

@@ -0,0 +1,561 @@
+--[[
+@module extalk
+@summary extalk扩展库
+@version 1.1.1
+@date    2025.09.18
+@author  梁健
+@usage
+    local extalk = require "extalk"
+    -- 配置并初始化
+    extalk.setup({
+        key = "your_product_key",
+        heart_break_time = 30,
+        contact_list_cbfnc = function(dev_list) end,
+        state_cbfnc = function(state) end
+    })
+    -- 发起对讲
+    extalk.start("remote_device_id")
+    -- 结束对讲
+    extalk.stop()
+]]
+
+local extalk = {}
+
+-- 模块常量(保留原始数据结构)
+extalk.START = 1     -- 通话开始
+extalk.STOP = 2      -- 通话结束
+extalk.UNRESPONSIVE = 3  -- 未响应
+extalk.ONE_ON_ONE = 5  -- 一对一来电
+extalk.BROADCAST = 6 -- 广播
+
+local AIRTALK_TASK_NAME = "airtalk_task"
+
+-- 消息类型常量(保留原始数据结构)
+local MSG_CONNECT_ON_IND = 0
+local MSG_CONNECT_OFF_IND = 1
+local MSG_AUTH_IND = 2
+local MSG_SPEECH_ON_IND = 3
+local MSG_SPEECH_OFF_IND = 4
+local MSG_SPEECH_CONNECT_TO = 5
+local MSG_SPEECH_STOP_TEST_END = 22
+
+-- 设备状态常量(保留原始数据结构)
+local SP_T_NO_READY = 0           -- 离线状态无法对讲
+local SP_T_IDLE = 1               -- 对讲空闲状态
+local SP_T_CONNECTING = 2         -- 主动发起对讲
+local SP_T_CONNECTED = 3          -- 对讲中
+
+local SUCC = "success"
+
+-- 全局状态变量(保留原始数据结构)
+local g_state = SP_T_NO_READY   -- 设备状态
+local g_mqttc = nil             -- mqtt客户端
+local g_local_id                -- 本机ID
+local g_remote_id               -- 对端ID
+local g_s_type                  -- 对讲的模式,字符串形式
+local g_s_topic                 -- 对讲用的topic
+local g_s_mode                  -- 对讲的模式
+local g_dev_list                -- 对讲列表
+local g_dl_topic                -- 下行消息topic模板
+
+-- 配置参数
+local extalk_configs_local = {
+    key = 0,               -- 项目key,一般需要和main的PRODUCT_KEY保持一致
+    heart_break_time = 0,  -- 心跳间隔(单位秒)
+    contact_list_cbfnc = nil, -- 联系人回调函数,含设备号和昵称
+    state_cbfnc = nil,  -- 状态回调,分为对讲开始,对讲结束,未响应
+}
+
+-- 工具函数:参数检查
+local function check_param(param, expected_type, name)
+    if type(param) ~= expected_type then
+        log.error(string.format("参数错误: %s 应为 %s 类型,实际为 %s", 
+            name, expected_type, type(param)))
+        return false
+    end
+    return true
+end
+
+-- 发送鉴权消息
+local function auth()
+    if g_state == SP_T_NO_READY and g_mqttc then
+        local topic = string.format("ctrl/uplink/%s/0001", g_local_id)
+        local payload = json.encode({
+            ["key"] = extalk_configs_local.key, 
+            ["device_type"] = 1
+        })
+        g_mqttc:publish(topic, payload)
+    end
+end
+
+-- 发送心跳消息
+local function heart()
+    if g_state == SP_T_CONNECTED and g_mqttc then
+        local topic = string.format("ctrl/uplink/%s/0005", g_local_id)
+        local payload = json.encode({
+            ["from"] = g_local_id, 
+            ["to"] = g_remote_id
+        })
+        g_mqttc:publish(topic, payload)
+    end
+end
+
+-- 开始对讲
+local function speech_on(ssrc, sample)
+    g_state = SP_T_CONNECTED
+    g_mqttc:subscribe(g_s_topic)
+    airtalk.set_topic(g_s_topic)
+    airtalk.set_ssrc(ssrc)
+    log.info("对讲模式", g_s_mode)
+    airtalk.speech(true, g_s_mode, sample)
+    sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, true) 
+    sys.timerLoopStart(heart, extalk_configs_local.heart_break_time * 1000)
+    sys.timerStopAll(wait_speech_to)
+end
+
+-- 结束对讲
+local function speech_off(need_upload, need_ind)
+    if g_state == SP_T_CONNECTED then
+        g_mqttc:unsubscribe(g_s_topic)
+        airtalk.speech(false)
+        g_s_topic = nil
+    end
+    
+    g_state = SP_T_IDLE
+    sys.timerStopAll(auth)
+    sys.timerStopAll(heart)
+    sys.timerStopAll(wait_speech_to)
+    
+    if need_upload and g_mqttc then
+        local topic = string.format("ctrl/uplink/%s/0004", g_local_id)
+        g_mqttc:publish(topic, json.encode({["to"] = g_remote_id}))
+    end
+
+    if need_ind then
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_OFF_IND, true)
+    end
+end
+
+-- 对讲超时处理
+local function wait_speech_to()
+    log.info("主动请求对讲超时无应答")
+    speech_off(true, false)
+end
+
+-- 命令处理:请求对讲应答
+local function handle_speech_response(obj)
+    if g_state ~= SP_T_CONNECTING then
+        log.error("state", g_state, "need", SP_T_CONNECTING)
+        return
+    end
+
+    if obj and obj["result"] == SUCC and g_s_topic == obj["topic"] then
+        -- 开始对讲
+        local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
+        speech_on(obj["ssrc"], sample_rate)
+        return
+    else
+        log.info(obj["result"], obj["topic"], g_s_topic)
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, false)
+    end
+    
+    g_s_topic = nil
+    g_state = SP_T_IDLE
+end
+
+-- 命令处理:对端来电
+local function handle_incoming_call(obj)
+    if not obj or not obj["topic"] or not obj["ssrc"] or not obj["audio_code"] or not obj["type"] then
+        local response = {
+            ["result"] = "failed", 
+            ["topic"] = obj and obj["topic"] or "", 
+            ["info"] = "无效的请求参数"
+        }
+        g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+        return
+    end
+
+    -- 非空闲状态无法接收来电
+    if g_state ~= SP_T_IDLE then
+        log.error("state", g_state, "need", SP_T_IDLE)
+        local response = {
+            ["result"] = "failed", 
+            ["topic"] = obj["topic"], 
+            ["info"] = "device is busy"
+        }
+        g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+        return
+    end
+
+    local response, from = {}, nil
+    
+    -- 提取对端ID
+    from = string.match(obj["topic"], "audio/(.*)/.*/.*")
+    if not from then
+        response = {
+            ["result"] = "failed", 
+            ["topic"] = obj["topic"], 
+            ["info"] = "topic error"
+        }
+        g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+        return
+    end
+
+    -- 处理一对一通话
+    if obj["type"] == "one-on-one" then
+        g_s_topic = obj["topic"]
+        g_remote_id = from
+        g_s_type = "one-on-one"
+        g_s_mode = airtalk.MODE_PERSON
+        
+        -- 触发回调
+        if extalk_configs_local.state_cbfnc then
+            extalk_configs_local.state_cbfnc({
+                state = extalk.ONE_ON_ONE, 
+                id = from 
+            })
+        end
+        
+        response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
+        local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
+        speech_on(obj["ssrc"], sample_rate)
+    end
+
+    -- 处理广播
+    if obj["type"] == "broadcast" then
+        g_s_topic = obj["topic"]
+        g_remote_id = from
+        g_s_mode = airtalk.MODE_GROUP_LISTENER
+        g_s_type = "broadcast"
+        
+        -- 触发回调
+        if extalk_configs_local.state_cbfnc then
+            extalk_configs_local.state_cbfnc({
+                state = extalk.BROADCAST, 
+                id = from 
+            })
+        end
+        
+        response = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
+        local sample_rate = obj["audio_code"] == "amr-nb" and 8000 or 16000
+        speech_on(obj["ssrc"], sample_rate)
+    end
+
+    -- 发送响应
+    g_mqttc:publish(string.format("ctrl/uplink/%s/8102", g_local_id), json.encode(response))
+end
+
+-- 命令处理:对端挂断
+local function handle_remote_hangup(obj)
+    local response = {}
+    
+    if g_state == SP_T_IDLE then
+        response = {["result"] = "failed", ["info"] = "no speech"}
+    else
+        log.info("0103", obj, obj["type"], g_s_type)
+        if obj and obj["type"] == g_s_type then
+            response = {["result"] = SUCC, ["info"] = ""}
+            speech_off(false, true)
+        else
+            response = {["result"] = "failed", ["info"] = "type mismatch"}
+        end
+    end
+    
+    g_mqttc:publish(string.format("ctrl/uplink/%s/8103", g_local_id), json.encode(response))
+end
+
+-- 命令处理:更新设备列表
+local function handle_device_list_update(obj)
+    local response = {}
+    if obj then
+        g_dev_list = obj["dev_list"]
+        response = {["result"] = SUCC, ["info"] = ""}
+    else
+        response = {["result"] = "failed", ["info"] = "json info error"}
+    end
+    
+    g_mqttc:publish(string.format("ctrl/uplink/%s/8101", g_local_id), json.encode(response))
+end
+
+-- 命令处理:鉴权结果
+local function handle_auth_result(obj)
+    if obj and obj["result"] == SUCC then
+        g_mqttc:publish(string.format("ctrl/uplink/%s/0002", g_local_id), "")  -- 更新列表
+    else
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, 
+            "鉴权失败" .. (obj and obj["info"] or "")) 
+    end
+end
+
+-- 命令处理:设备列表更新应答
+local function handle_device_list_response(obj)
+    if obj and obj["result"] == SUCC then
+        g_dev_list = obj["dev_list"]
+        if extalk_configs_local.contact_list_cbfnc then
+            extalk_configs_local.contact_list_cbfnc(g_dev_list)
+        end
+        g_state = SP_T_IDLE
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, true)  -- 完整登录流程结束
+    else
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "更新设备列表失败") 
+    end
+end
+
+-- 命令解析路由表
+local cmd_handlers = {
+    ["8003"] = handle_speech_response,  -- 请求对讲应答
+    ["0102"] = handle_incoming_call,    -- 平台通知对端对讲开始
+    ["0103"] = handle_remote_hangup,    -- 平台通知终端对讲结束
+    ["0101"] = handle_device_list_update,-- 平台通知终端更新对讲设备列表
+    ["8001"] = handle_auth_result,      -- 平台对鉴权应答
+    ["8002"] = handle_device_list_response -- 平台对终端获取终端列表应答
+}
+
+-- 解析接收到的消息
+local function analyze_v1(cmd, topic, obj)
+    -- 忽略心跳和结束对讲的应答
+    if cmd == "8005" or cmd == "8004" then
+        return
+    end
+    
+    -- 查找并执行对应的命令处理器
+    local handler = cmd_handlers[cmd]
+    if handler then
+        handler(obj)
+    else
+        log.warn("未处理的命令", cmd)
+    end
+end
+
+-- MQTT回调处理
+local function mqtt_cb(mqttc, event, topic, payload)
+    log.info(event, topic or "")
+    
+    if event == "conack" then
+        -- MQTT连接成功,开始自定义鉴权流程
+        sys.sendMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
+        g_mqttc:subscribe("ctrl/downlink/" .. g_local_id .. "/#")
+    elseif event == "suback" then
+        if g_state == SP_T_NO_READY then
+            if topic then
+                auth()
+            else
+                sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, 
+                    "订阅失败" .. "ctrl/downlink/" .. g_local_id .. "/#") 
+            end
+        elseif g_state == SP_T_CONNECTED and not topic then
+            speech_off(false, true)
+        end
+    elseif event == "recv" then
+        local result = string.match(topic, g_dl_topic)
+        if result then 
+            local obj = json.decode(payload)
+            analyze_v1(result, topic, obj)
+        end
+    elseif event == "disconnect" then
+        speech_off(false, true)
+        g_state = SP_T_NO_READY
+    elseif event == "error" then
+        log.error("MQTT错误发生")
+    end
+end
+
+-- 任务消息处理
+local function task_cb(msg)
+    if msg[1] == MSG_SPEECH_CONNECT_TO then
+        speech_off(true, false)
+    else
+        log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
+    end
+end
+
+-- 对讲事件回调
+local function airtalk_event_cb(event, param)
+    log.info("airtalk event", event, param)
+    if event == airtalk.EVENT_ERROR then
+        if param == airtalk.ERROR_NO_DATA then
+            log.error("长时间没有收到音频数据")
+            speech_off(true, true)
+        end
+    end
+end
+
+-- MQTT任务主循环
+local function airtalk_mqtt_task()
+    local msg, online = nil, false
+    
+    -- 初始化本地ID
+    g_local_id = mobile.imei()
+    g_dl_topic = "ctrl/downlink/" .. g_local_id .. "/(%w%w%w%w)"
+    
+    -- 创建MQTT客户端
+    g_mqttc = mqtt.create(nil, "mqtt.airtalk.luatos.com", 1883, false, {rxSize = 32768})
+    
+    -- 配置对讲参数
+    airtalk.config(airtalk.PROTOCOL_MQTT, g_mqttc, 200) -- 缓冲至少200ms播放
+    airtalk.on(airtalk_event_cb)
+    airtalk.start()
+    
+    -- 配置MQTT客户端
+    g_mqttc:auth(g_local_id, g_local_id, mobile.muid())
+    g_mqttc:keepalive(240) -- 默认值240s
+    g_mqttc:autoreconn(true, 15000) -- 自动重连机制
+    g_mqttc:debug(false)
+    g_mqttc:on(mqtt_cb)
+    
+    log.info("设备信息", g_local_id, mobile.muid())
+    
+    -- 开始连接
+    g_mqttc:connect()
+    online = false
+    
+    while true do
+        -- 等待MQTT连接成功
+        msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)
+        log.info("connected")
+        
+        -- 处理登录流程
+        while not online do
+            msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, 30000) -- 30秒超时
+            
+            if type(msg) == 'table' then
+                online = msg[2]
+                if online then
+                    -- 鉴权通过,60分钟后重新鉴权
+                    sys.timerLoopStart(auth, 3600000)
+                else
+                    log.info(msg[3])
+                    -- 鉴权失败,5分钟后重试
+                    sys.timerLoopStart(auth, 300000)
+                end
+            else
+                -- 超时未收到鉴权结果,重新发送
+                auth()
+            end
+        end
+        
+        log.info("对讲管理平台已连接")
+        
+        -- 处理在线状态下的消息
+        while online do
+            msg = sys.waitMsg(AIRTALK_TASK_NAME)
+            
+            if type(msg) == 'table' and type(msg[1]) == "number" then
+                if msg[1] == MSG_SPEECH_STOP_TEST_END then
+                    if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
+                        log.info("没有对讲", g_state)
+                    else
+                        speech_off(true, false)
+                    end
+                elseif msg[1] == MSG_SPEECH_ON_IND then
+                    if extalk_configs_local.state_cbfnc then
+                        local state = msg[2] and extalk.START or extalk.UNRESPONSIVE
+                        extalk_configs_local.state_cbfnc({state = state})
+                    end
+                elseif msg[1] == MSG_SPEECH_OFF_IND then
+                    if extalk_configs_local.state_cbfnc then
+                        extalk_configs_local.state_cbfnc({state = extalk.STOP})
+                    end
+                elseif msg[1] == MSG_CONNECT_OFF_IND then
+                    log.info("connect", msg[2])
+                    online = msg[2]
+                end
+            else
+                log.info(type(msg), type(msg and msg[1]))
+            end
+            
+            msg = nil -- 清理引用
+        end
+        
+        online = false -- 重置在线状态
+    end
+end
+
+-- 模块初始化
+function extalk.setup(extalk_configs)
+    if not extalk_configs or type(extalk_configs) ~= "table" then
+        log.error("AirTalk配置必须为table类型")
+        return false
+    end
+
+    -- 检查配置参数
+    if not check_param(extalk_configs.key, "string", "key") then
+        return false
+    end
+    extalk_configs_local.key = extalk_configs.key
+
+    if not check_param(extalk_configs.heart_break_time, "number", "heart_break_time") then
+        return false
+    end
+    extalk_configs_local.heart_break_time = extalk_configs.heart_break_time
+
+    if not check_param(extalk_configs.contact_list_cbfnc, "function", "contact_list_cbfnc") then
+        return false
+    end
+    extalk_configs_local.contact_list_cbfnc = extalk_configs.contact_list_cbfnc
+
+    if not check_param(extalk_configs.state_cbfnc, "function", "state_cbfnc") then
+        return false
+    end
+    extalk_configs_local.state_cbfnc = extalk_configs.state_cbfnc
+
+    -- 启动任务
+    sys.taskInitEx(airtalk_mqtt_task, AIRTALK_TASK_NAME, task_cb)
+    return true
+end
+
+-- 开始对讲
+function extalk.start(id)
+    if g_state ~= SP_T_IDLE then
+        log.warn("正在对讲无法开始,当前状态:", g_state)
+        return false
+    end
+
+    if id == nil then
+        -- 广播模式
+        g_remote_id = "all"
+        g_state = SP_T_CONNECTING
+        g_s_mode = airtalk.MODE_GROUP_SPEAKER
+        g_s_type = "broadcast"
+        g_s_topic = string.format("audio/%s/all/%s", 
+            g_local_id, string.sub(tostring(mcu.ticks()), -4, -1))
+        
+        g_mqttc:publish(string.format("ctrl/uplink/%s/0003", g_local_id), 
+            json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
+        sys.timerStart(wait_speech_to, 15000)
+    else
+        -- 一对一模式
+        log.info("向", id, "主动发起对讲")
+        if id == g_local_id then
+            log.error("不允许本机给本机拨打电话")
+            return false
+        end
+        
+        g_state = SP_T_CONNECTING
+        g_remote_id = id
+        g_s_mode = airtalk.MODE_PERSON
+        g_s_type = "one-on-one"
+        g_s_topic = string.format("audio/%s/%s/%s", 
+            g_local_id, id, string.sub(tostring(mcu.ticks()), -4, -1))
+        
+        g_mqttc:publish(string.format("ctrl/uplink/%s/0003", g_local_id), 
+            json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
+        sys.timerStart(wait_speech_to, 15000)
+    end
+    
+    return true
+end
+
+-- 结束对讲
+function extalk.stop()
+    if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
+        log.info("没有对讲,当前状态:", g_state)
+        return false
+    end
+
+    log.info("主动断开对讲")
+    speech_off(true, false)
+    return true
+end
+
+return extalk

+ 5 - 3
module/Air8000/project/整机开发板出厂工程/user/main.lua

@@ -352,9 +352,11 @@ local function draw_main3()
       x = 64 + (i-1)*128 + 24
       y = (j-1)*106 + 13
       sel = j+(i-1)*3
-      fname = "/luadb/" .. "D" .. sel .. ".jpg"
-      -- log.info("fname:", fname, x,y,sel,cur_sel)
-      lcd.showImage(y,x,fname)
+      if sel == 1 or sel == 7 or sel == 9 then
+        fname = "/luadb/" .. "D" .. sel .. ".jpg"
+        -- log.info("fname:", fname, x,y,sel,cur_sel)
+        lcd.showImage(y,x,fname)
+      end
     end
   end
 end

+ 61 - 439
module/Air8000/project/整机开发板出厂工程/user/talk.lua

@@ -6,65 +6,22 @@ dhcpsrv = require("dhcpsrv")
 httpplus = require("httpplus")
 
 local run_state = false
-local airaudio  = require "airaudio"
 local input_method = require "InputMethod" 
 local input_key = false
+local g_dev_list = {}
+local airaudio  = require "airaudio"
+extalk = require("extalk")
+local local_id 
 
 -- 初始化fskv
 AIRTALK_TASK_NAME = "airtalk_task"
 USER_TASK_NAME = "user"
 
-MSG_CONNECT_ON_IND = 0
-MSG_CONNECT_OFF_IND = 1
-MSG_AUTH_IND = 2
-MSG_SPEECH_ON_IND = 3
-MSG_SPEECH_OFF_IND = 4
-MSG_SPEECH_CONNECT_TO = 5
-
-MSG_PERSON_SPEECH_TEST_START = 20
-MSG_GROUP_SPEECH_TEST_START = 21
-MSG_SPEECH_STOP_TEST_END = 22
-
-
-MSG_READY = 10
-MSG_NOT_READY = 11
-MSG_TYPE = 12
-
--- 新增消息类型
-MSG_ADDRESS_LIST_OPEN = 30
-MSG_ADDRESS_LIST_BACK = 31
-MSG_ADDRESS_LIST_PREV = 32
-MSG_ADDRESS_LIST_NEXT = 33
-MSG_ADDRESS_LIST_SELECT = 34
-
-
-SP_T_NO_READY = 0           -- 离线状态无法对讲
-SP_T_IDLE = 1               -- 对讲空闲状态
-SP_T_CONNECTING = 2         -- 主动发起对讲
-SP_T_CONNECTED = 3          -- 对讲中
-
-
 SUCC = "success"
 local speech_topic = nil
-local mqtt_host = "lbsmqtt.openluat.com"
-local mqtt_port = 1886
-local mqtt_isssl = false
-local client_id = nil
-local user_name = "mqtt_hz_test_1"
-local password = "Ck8WpNCp"
-local mqttc = nil
-local message = ""
 local event = ""
 local talk_state = ""
-local mqttc = nil
-local g_state = SP_T_NO_READY   --device状态
-local g_mqttc = nil             --mqtt客户端
-local g_local_id                  --本机ID
-local g_remote_id                 --对端ID
-local g_s_type                  --对讲的模式,字符串形式的
-local g_s_topic                 --对讲用的topic
-local g_s_mode                  --对讲的模式
-local g_dev_list = {}           --对讲列表
+
 
 -- 新增通讯录相关变量
 local address_list_page = 1     -- 通讯录当前页码
@@ -72,397 +29,61 @@ local address_list_max_page = 1 -- 通讯录最大页码
 local current_page = "main"     -- 当前页面状态
 local contacts_per_page = 8     -- 每页显示的联系人数量
 
-local function auth()
-    if g_state == SP_T_NO_READY then
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0001", json.encode({["key"] = PRODUCT_KEY, ["device_type"] = 1}))
-    end
-end
-
-local function heart()
-    -- if g_state == SP_T_CONNECTED then
-        log.info("心跳上报")
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0005", json.encode({["csq"] = mobile.csq(), ["battery"] = 100}))
-    -- end
-end
-
-
---对讲开始,topic,ssrc,采样率(8K或者16K)这3个参数都有了之后就能进行对讲了,可以通过其他协议传入
-local function speech_on(ssrc, sample)
-    g_state = SP_T_CONNECTED
-    g_mqttc:subscribe(g_s_topic)
-    airtalk.set_topic(g_s_topic)
-    airtalk.set_ssrc(ssrc)
-    log.info("对讲模式", g_s_mode)
-    airtalk.speech(true, g_s_mode, sample)
-    sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, true) 
-    
-    sys.timerStopAll(wait_speech_to)
-    log.info("对讲接通,可以说话了")
-end
---对讲结束
-local function speech_off(need_upload, need_ind)
-    if g_state ==  SP_T_CONNECTED then
-        g_mqttc:unsubscribe(g_s_topic)
-        airtalk.speech(false)
-        g_s_topic = nil
-    end
-    g_state = SP_T_IDLE
-    sys.timerStopAll(auth)
-    sys.timerStopAll(heart)
-    sys.timerStopAll(wait_speech_to)
-    log.info("对讲断开了")
-    if need_upload then
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0004", json.encode({["to"] = g_remote_id}))
-    end
-    if need_ind then
-        sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_OFF_IND, true)
-    end
-end
-
-function wait_speech_to()
-    log.info("主动请求对讲超时无应答")
-    speech_off(true, false)
-end
-
-local function analyze_v1(cmd, topic, obj)
-    if cmd == "8005" or cmd == "8004" then       -- 对讲心跳保持和结束对讲的应答不做处理
-        return
-    end
-    if cmd == "8003" then       -- 请求对讲应答
-        if g_state ~= SP_T_CONNECTING then  --没有发起对讲请求
-            log.error("state", g_state, "need", SP_T_CONNECTING)
-            return
-        else
-            if obj and obj["result"] == SUCC and g_s_topic == obj["topic"]then  --完全正确,开始对讲
-                speech_on(obj["ssrc"], obj["audio_code"] == "amr-nb" and 8000 or 16000)
-                return
-            else
-                log.info(obj["result"], obj["topic"], g_s_topic)
-                sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_ON_IND, false)   --有异常,无法对讲
-            end
-            
-        end
-        g_s_topic = nil
-        g_state = SP_T_IDLE
-        return
-    end
-    local new_obj = nil
-    if cmd == "0102" then       -- 对端打过来
-        if obj and obj["topic"] and obj["ssrc"] and obj["audio_code"] and obj["type"] then
-            if g_state ~= SP_T_IDLE then    -- 空闲状态下才可以进入对讲状态
-                log.error("state", g_state, "need", SP_T_IDLE)
-                new_obj = {["result"] = "failed", ["topic"] = obj["topic"], ["info"] = "device is busy"}
-            else
-                if obj["type"] == "one-on-one" then -- 1对1对讲
-                    local from = string.match(obj["topic"], "audio/.*/(.*)/.*")
-                    if from then
-                        log.info("remote id ", from)
-                        g_s_topic = obj["topic"]
-                        g_remote_id = from
-                        new_obj = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
-                        g_s_type = "one-on-one"
-                        g_s_mode = airtalk.MODE_PERSON
-                        speech_on(obj["ssrc"], obj["audio_code"] == "amr-nb" and 8000 or 16000)
-                    else
-                        new_obj = {["result"] = "failed", ["topic"] = obj["topic"], ["info"] = "topic error"}
-                    end
-                elseif obj["type"] == "broadcast" then  -- 1对多对讲
-                    g_s_topic = obj["topic"]
-                    new_obj = {["result"] = SUCC, ["topic"] = obj["topic"], ["info"] = ""}
-                    g_s_mode = airtalk.MODE_GROUP_LISTENER
-                    g_s_type = "broadcast"
-                    speech_on(obj["ssrc"], obj["audio_code"] == "amr-nb" and 8000 or 16000)
-                end
-            end
-        else
-            new_obj = {["result"] = "failed", ["topic"] = obj["topic"], ["info"] = "json info error"}
-        end
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/8102", json.encode(new_obj))
-        return
-    end
-
-    if cmd == "0103" then   --对端挂断
-        if g_state == SP_T_IDLE then
-            new_obj = {["result"] = "failed", ["info"] = "no speech"}
-        else
-            if obj and obj["type"] == g_s_type then
-                new_obj = {["result"] = SUCC, ["info"] = ""}
-                speech_off(false, true)
-            else
-                new_obj = {["result"] = "failed", ["info"] = "type mismatch"}
-            end
-        end
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/8103", json.encode(new_obj))
-        return
-    end
-
-    if cmd == "0101" then                        --更新设备列表
-        if obj then
-            g_dev_list = obj["dev_list"]
-            -- 计算通讯录最大页码
-            address_list_max_page = math.ceil(#g_dev_list / contacts_per_page)
-            if address_list_max_page == 0 then
-                address_list_max_page = 1
-            end
-            -- for i=1,#g_dev_list do
-            --     log.info(g_dev_list[i]["id"],g_dev_list[i]["name"])
-            -- end
-            new_obj = {["result"] = SUCC, ["info"] = ""}
-        else
-            new_obj = {["result"] = "failed", ["info"] = "json info error"}
-        end
-        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/8101", json.encode(new_obj))
-        return
-    end
-    if cmd == "8001" then
-        if obj and obj["result"] == SUCC then
-            g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0002","")  -- 更新列表
-        else
-            sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "鉴权失败" .. obj["info"]) 
-        end
-        return
-    end
-    if cmd == "8002" then
-        if obj and obj["result"] == SUCC then   --收到设备列表更新应答,才能认为相关网络服务准备好了
-            g_dev_list = obj["dev_list"]
-            -- 计算通讯录最大页码
-            address_list_max_page = math.ceil(#g_dev_list / contacts_per_page)
-            if address_list_max_page == 0 then
-                address_list_max_page = 1
-            end
-            for i=1,#g_dev_list do
-                log.info(g_dev_list[i]["id"],g_dev_list[i]["name"])
-            end
-            g_state = SP_T_IDLE
-            sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, true)  --完整登录流程结束
-        else
-            sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "更新设备列表失败") 
-        end
-        return
-    end
-end
-
-local function mqtt_cb(mqttc, event, topic, payload)
-    log.info(event, topic)
-    local msg,data,obj
-    if event == "conack" then
-        sys.sendMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND) --mqtt连上了,开始自定义的鉴权流程
-        g_mqttc:subscribe("ctrl/downlink/" .. g_local_id .. "/#")--单主题订阅
-    elseif event == "suback" then
-        if g_state == SP_T_NO_READY then
-            if topic then
-                auth()
-            else
-                sys.sendMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, false, "订阅失败" .. "ctrl/downlink/" .. g_local_id .. "/#") 
-            end
-        elseif g_state == SP_T_CONNECTED then
-            if not topic then
-                speech_off(false, true)
-            end
-        end
-    elseif event == "recv" then
-        local result = string.match(topic, g_dl_topic)
-        if result then 
-            local obj,res,err = json.decode(payload)
-            analyze_v1(result, topic, obj)
-        end
-        result = nil
-        data = nil
-        obj = nil
-        
-    elseif event == "sent" then
-        -- log.info("mqtt", "sent", "pkgid", data)
-    elseif event == "disconnect" then
-        speech_off(false, true)
-        g_state = SP_T_NO_READY
-    elseif event == "error" then
-
-    end
-end
-
-local function task_cb(msg)
-    if msg[1] == MSG_SPEECH_CONNECT_TO then
-        speech_off(true,false)
-    else
-        log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
+local function contact_list(dev_list)
+    g_dev_list = dev_list
+    for i=1,#dev_list do
+        log.info("联系人ID:",dev_list[i]["id"],"名称:",dev_list[i]["name"])
     end
 end
 
-local function airtalk_event_cb(event, param)
-    log.info("airtalk event", event, param)
-    if event == airtalk.EVENT_ERROR then
-        if param == airtalk.ERROR_NO_DATA then
-            -- log.error("长时间没有收到音频数据")
-            -- speech_off(true, true)
-        end
-    end
-end
-
-local function airtalk_mqtt_task()
-    local msg,data,obj,online,num,res
-    --g_local_id也可以自己设置
-    
-    g_dl_topic = "ctrl/downlink/" .. g_local_id .. "/(%w%w%w%w)"
-    sys.timerLoopStart(next_auth, 900000)
-
-    g_mqttc = mqtt.create(nil, "mqtt.airtalk.luatos.com", 1883, false, {rxSize = 32768})
-    airtalk.config(airtalk.PROTOCOL_MQTT, g_mqttc, 200) -- 缓冲至少200ms播放
-    airtalk.on(airtalk_event_cb)
-    airtalk.start()
-
-    g_mqttc:auth(g_local_id,g_local_id,mobile.muid()) -- g_local_id必填,其余选填
-    g_mqttc:keepalive(240) -- 默认值240s
-    g_mqttc:autoreconn(true, 15000) -- 自动重连机制
-    g_mqttc:debug(false)
-    g_mqttc:on(mqtt_cb)
-    log.info("设备信息", g_local_id, mobile.muid())
-    -- mqttc自动处理重连, 除非自行关闭
-    g_mqttc:connect()
-    online = false
-    while true do
-        msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_CONNECT_ON_IND)   --等服务器连上
-        log.info("connected")
-        while not online do
-            msg = sys.waitMsg(AIRTALK_TASK_NAME, MSG_AUTH_IND, 30000)   --登录流程不应该超过30秒
-            if type(msg) == 'table' then
-                online = msg[2]
-                if online then
-                    sys.timerLoopStart(auth, 3600000) --鉴权通过则60分钟后尝试重新鉴权
-                else
-                    log.info(msg[3])
-                    sys.timerLoopStart(auth, 300000)       --5分钟后重新鉴权
-                end
-            else
-                auth()  --30秒鉴权无效后重新鉴权
+local function state(event_table)
+    if event_table.state  == extalk.START then
+        log.info("对讲开始,可以说话了")
+        talk_state = "对讲开始,可以说话了"
+    elseif  event_table.state  == extalk.STOP then
+        log.info("对讲结束")
+        talk_state = "对讲结束"
+    elseif  event_table.state  == extalk.UNRESPONSIVE then
+        log.info("对端未响应")
+        talk_state = "对端未响应"
+    elseif  event_table.state  == extalk.ONE_ON_ONE then
+        for i=1,#g_dev_list do
+            if g_dev_list[i]["id"] == event_table.id then
+                log.info(g_dev_list[i]["name"],",来电话了")
+                talk_state = g_dev_list[i]["name"]..",来电话了"
+                break
             end
         end
-        log.info("对讲管理平台已连接")
-        while online do
-            msg = sys.waitMsg(AIRTALK_TASK_NAME)
-            if type(msg) == 'table' and type(msg[1]) == "number" then
-                if msg[1] == MSG_PERSON_SPEECH_TEST_START then
-                    if g_state ~= SP_T_IDLE then
-                        log.info("正在对讲无法开始")
-                    else
-                        log.info("匹配输入的设备号是在设备列表中")
-
-                        res = false
-                        for i=1,#g_dev_list do
-                            res = string.match(g_dev_list[i]["id"], "(%w%w%w%w%w%w%w%w%w%w%w%w%w%w%w)")
-                            if res and res == speech_topic then
-                                res = true          
-                                break
-                            end
-                        end
-                        if res then
-                            log.info("向", speech_topic, "主动发起对讲")
-                            g_state = SP_T_CONNECTING
-                            g_remote_id = speech_topic
-                            g_s_mode = airtalk.MODE_PERSON
-                            g_s_type = "one-on-one"
-                            g_s_topic = "audio/" .. g_local_id .. "/" .. g_remote_id .. "/" .. (string.sub(tostring(mcu.ticks()), -4, -1))
-                            g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0003", json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
-                            sys.timerStart(wait_speech_to, 15000)
-                        else
-                            log.info("找不到有效的设备ID")
-                        end
-                    end
-                elseif msg[1] == MSG_GROUP_SPEECH_TEST_START then
-                    if g_state ~= SP_T_IDLE then
-                        log.info("正在对讲无法开始")
-                    else
-                        g_remote_id = "all"
-                        g_state = SP_T_CONNECTING
-                        g_s_mode = airtalk.MODE_GROUP_SPEAKER
-                        g_s_type = "broadcast"
-                        g_s_topic = "audio/" .. g_local_id .. "/all/" .. (string.sub(tostring(mcu.ticks()), -4, -1))
-                        g_mqttc:publish("ctrl/uplink/" .. g_local_id .."/0003", json.encode({["topic"] = g_s_topic, ["type"] = g_s_type}))
-                        sys.timerStart(wait_speech_to, 15000)
-                    end
-                elseif msg[1] == MSG_SPEECH_STOP_TEST_END then
-                    if g_state ~= SP_T_CONNECTING and g_state ~= SP_T_CONNECTED then
-                        log.info("没有对讲", g_state)
-                    else
-                        log.info("主动断开对讲")
-                        speech_off(true, false)
-                    end
-                elseif msg[1] == MSG_SPEECH_ON_IND then
-                    if msg[2] then
-                        log.info("对讲接通")
-                    else
-                        log.info("对讲断开")
-                    end
-                elseif msg[1] == MSG_CONNECT_OFF_IND then
-                    log.info("connect", msg[2])
-                    online = msg[2]
-                end
-                obj = nil
-            else
-                log.info(type(msg), type(msg[1]))
+    elseif  event_table.state  == extalk.BROADCAST then
+        for i=1,#g_dev_list do
+            if g_dev_list[i]["id"] == event_table.id then
+                log.info(g_dev_list[i]["name"],"开始广播")
+                talk_state = g_dev_list[i]["name"].."开始广播"
+                break
             end
-            msg = nil
         end
-        online = false
     end
 end
 
-function airtalk_mqtt_init()
-    sys.taskInitEx(airtalk_mqtt_task, AIRTALK_TASK_NAME, task_cb)
-end
-
-
-local function airtalk_event_cb(event, param)
-    log.info("talk event", event, param)
-    event  = event
-end
+local extalk_configs = {
+    key = PRODUCT_KEY,               -- 项目key,一般来说需要和main 的PRODUCT_KEY保持一致
+    heart_break_time = 120,  -- 心跳间隔(单位秒)
+    contact_list_cbfnc = contact_list, -- 联系人回调函数,回调信息含设备号和昵称
+    state_cbfnc = state,  --状态回调,分为对讲开始,对讲结束,未响应
+}
 
 
--- MQTT回调函数
-local function mqtt_cb(mqtt_client, event, data, payload)
-    log.info("mqtt", "event", event, mqtt_client, data, payload)
-    -- 连接成功时订阅主题
-end
 
-local function task_cb(msg)
-    log.info("未处理消息", msg[1], msg[2], msg[3], msg[4])
-    if msg[1] == MSG_SPEECH_IND then
 
-    elseif msg[1] == MSG_NOT_READY then
-        test_ready = false
-        msg = sys.waitMsg(USER_TASK_NAME, MSG_TYPE)
-    end
-end
 local function init_talk()
     log.info("init_call")
-    airaudio.init() 
-    airtalk_mqtt_init()
-    sys.timerLoopStart(heart, 10000)
-    local msg
-    while true do
-        msg = sys.waitMsg(USER_TASK_NAME, MSG_TYPE)
-        if msg[2] then  -- true powerkey false boot key
-            sys.sendMsg(AIRTALK_TASK_NAME, MSG_GROUP_SPEECH_TEST_START)   
-        else
-            sys.sendMsg(AIRTALK_TASK_NAME, MSG_PERSON_SPEECH_TEST_START)   
-        end 
-        msg = sys.waitMsg(USER_TASK_NAME, MSG_TYPE)
-        sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_STOP_TEST_END)        
-    end
-
+    airaudio.init()                           -- 初始化音频
+    extalk.setup(extalk_configs)              -- airtalk 初始化
 end
 
 
 
--- 输入法回调函数
-local function submit_callback(input_text)
-    if input_text and #input_text > 0 then
-        speech_topic = input_text
-        fskv.set("talk_number", input_text)  -- 保存对讲号码到fskv
-        log.info("talk", "对讲号码:", fskv.get("talk_number"))
-        input_key = false
 
-    end
-end
 
 -- 绘制通讯录页面
 local function draw_address_list()
@@ -518,12 +139,12 @@ local function draw_address_list()
 end
 
 function talk.run()
-    log.info("talk.run",airtalk.PROTOCOL_DEMO_MQTT_16K)
+    log.info("talk.run",airtalk.PROTOCOL_DEMO_MQTT_16K,mobile.imei())
     lcd.setFont(lcd.font_opposansm12_chinese)
     
     run_state = true
-    g_local_id = mobile.imei()
-    sys.taskInitEx(init_talk, USER_TASK_NAME, task_cb)
+    local_id = mobile.imei()
+    sys.taskInitEx(init_talk, USER_TASK_NAME)
     speech_topic = fskv.get("talk_number")
     log.info("get  speech_topic",speech_topic)
 
@@ -535,26 +156,25 @@ function talk.run()
             if current_page == "main" then
                 lcd.clear(_G.bkcolor) 
                 if  speech_topic  == nil then
-                    lcd.drawStr(0, 80, "输入设备号,并保证所有终端/平台一致")
+                    lcd.drawStr(0, 80, "所有要对讲的设备,要保持在线")
                     lcd.drawStr(0, 100, "方案介绍:airtalk.luatos.com")
                     lcd.drawStr(0, 120, "平台端网址:airtalk.openluat.com/talk/")
-                    lcd.drawStr(0, 140, "本机ID:" .. g_local_id)
+                    lcd.drawStr(0, 140, "本机ID:" .. local_id)
                     lcd.showImage(32, 250, "/luadb/input_topic.jpg")
                     lcd.showImage(32, 300, "/luadb/broadcast.jpg")
                     lcd.showImage(104, 400, "/luadb/stop.jpg")
                     
                 else
-                    lcd.drawStr(0, 80, "对端ID:"..speech_topic )
+                    -- lcd.drawStr(0, 80, "对端ID:"..speech_topic )
                     lcd.drawStr(0, 100, "方案介绍:airtalk.luatos.com")
                     lcd.drawStr(0, 120, "平台端网址:airtalk.openluat.com/talk/")
-                    lcd.drawStr(0, 140, "所有终端或者网页都要使用同一个topic")
+                    lcd.drawStr(0, 140, "所有要对讲的设备,要保持在线")
                     lcd.drawStr(0, 160, talk_state)
                     lcd.drawStr(0, 180, "事件:" .. event)
-                    lcd.drawStr(0, 200, "本机ID:" .. g_local_id)
+                    lcd.drawStr(0, 200, "本机ID:" .. local_id)
                     lcd.drawQrcode(185, 148, "https://airtalk.openluat.com/talk/", 82)
                     lcd.drawStr(185, 242, "扫码进入网页端",0x0000)
                     -- 显示输入法入口按钮
-                    lcd.showImage(32, 250, "/luadb/input_topic.jpg")
                     lcd.showImage(175, 300, "/luadb/datacall.jpg")
                     lcd.showImage(32, 300, "/luadb/broadcast.jpg")
                     lcd.showImage(104, 400, "/luadb/stop.jpg")
@@ -580,30 +200,34 @@ end
 
 local function stop_talk()
     talk_state = "停止对讲"
-    sys.sendMsg(AIRTALK_TASK_NAME, MSG_SPEECH_STOP_TEST_END)  -- 停止对讲
+    extalk.stop()     --   停止对讲
 end
 
 
 local function start_talk()
-    talk_state = "一对一通话开始"
-    sys.sendMsg(AIRTALK_TASK_NAME, MSG_PERSON_SPEECH_TEST_START)  --  开始一对一流量电话
+    if extalk.start(speech_topic) then
+        talk_state = "发起一对一通话中...."
+    end
 end
 
 local function start_broadcast()
     talk_state = "语音采集上传中,正在广播"
-    sys.sendMsg(AIRTALK_TASK_NAME, MSG_GROUP_SPEECH_TEST_START)     --   开始广播
+    extalk.start()     --   开始广播
 end
 
 
-local function start_input()
-    input_key = true
-    input_method.init(false, "talk", submit_callback)  -- 直接传递函数
-end
+
 
 -- 打开通讯录
 local function open_address_list()
-    current_page = "address_list"
-    address_list_page = 1
+    if g_dev_list == nil or  #g_dev_list  == 0 then
+        talk_state = "联系人列表获取失败,检测key和群组是否建立"
+        log.info("联系人列表获取失败,检测key和群组是否建立")
+        return false
+    else 
+        current_page = "address_list"
+        address_list_page = 1
+    end
 end
 
 -- 返回主页面
@@ -668,8 +292,6 @@ function talk.tp_handal(x, y, event)
         if current_page == "main" then
             if x > 0 and x < 80 and y > 0 and y < 80 then
                 run_state = false 
-            elseif x > 32 and x < 133 and y > 250 and y < 295 then
-                sysplus.taskInitEx(start_input,"start_input")
             elseif x > 173 and x < 284 and y > 300 and y < 345 then
                 sysplus.taskInitEx(start_talk, "start_talk")
             elseif x > 32 and x < 133 and y > 300 and y < 345 then

+ 2 - 2
module/Air8101/demo/tf_card/tfcard_app.lua

@@ -302,11 +302,11 @@ local function tfcard_main_task()
     log.info("文件操作", "===== 文件操作完成 =====")
 
     -- ########## 功能: 收尾功能演示##########
-    -- 卸载文件系统和关闭SPI
+    -- 卸载文件系统
     ::resource_cleanup::
 
     log.info("结束", "开始执行关闭操作...")  
-    -- 如已挂载需先卸载文件系统,未挂载直接关闭SPI
+    -- 如已挂载需先卸载文件系统
     if mount_ok then
         if fatfs.unmount("/sd") then
             log.info("文件系统", "卸载成功")

+ 52 - 0
module/spec/合宙IOT通用报文协议AirCloud_1.0.md

@@ -223,6 +223,10 @@ unique_id:muc 通过 mcu.unique_id()获取的唯一 id,需要转为十六进
 
 20 - 控制回应 - 上行, 用于对服务器发送的控制命令的回应
 
+21 - iRTU 下行命令,
+
+22 - iRTU 上行回复,
+
 ### 3.2.2 业务字段类型
 
 #### 3.2.2.1 传感类 256-511
@@ -317,6 +321,50 @@ unique_id:muc 通过 mcu.unique_id()获取的唯一 id,需要转为十六进
 
 1281 - 无意义数据
 
+### 3.2.3 控制信令的详细定义
+
+#### 3.2.3.1 鉴权请求-16 - 上行
+
+#### 3.2.3.2 鉴权回复-17 - 下行
+
+#### 3.2.3.3 上报回应-18 - 下行
+
+#### 3.2.3.4 控制命令-19 - 下行
+
+#### 3.2.3.5 控制回应-20 - 上行
+
+#### 3.2.3.6 iRTU 下行命令-21
+
+T: 21
+
+L: 消息的长度
+
+V:实际的 iRTU 下行命令的全文
+
+举例:
+
+T: 0x0017      ----- 信令类型 21
+
+L:   0x000B       ---- 11 个字节
+
+V: 字符串: “rrpc,getcsq”
+
+#### 3.2.3.7 iRTU 上行回复-22
+
+T: 22
+
+L: 消息的长度
+
+V:实际的 iRTU 上行命令全文
+
+举例:
+
+T: 0x0017      ----- 信令类型 22
+
+L:   0x000B       ---- 14 个字节
+
+V: 字符串: “rrpc,getcsq,17”
+
 # 四、airCloud 和遥测的关系
 
 遥测的目的是检测设备的 mobile 信息,证明设备还活着,还具备通信能力;
@@ -411,6 +459,10 @@ airCloud 当前不支持 HTTP UDP 协议。
 
 合宙的量产小板,用 airCloud 批量挂测,做压力测试。
 
+### 7.1.3 iRTU 内置支持
+
+IRTU 取消对第三方云平台的支持, 只内置合宙 airCloud 协议。
+
 ## 7.2 客户收费标准
 
 客户可以用这个协议,搭建自己的后台;

+ 60 - 88
script/libs/exfotawifi.lua

@@ -1,40 +1,14 @@
 --[[
 @module exfotawifi
 @summary 用于Air8000/8000A/8000W型号模组自动升级WIFI
-@version 1.0.1
-@date    2025.6.26
-@author  tuoyiheng
+@version 1.0.2
+@date    2025.9.16
+@author  拓毅恒
 @usage
-注:使用时在创建的一个task处理函数中直接调用exfotawifi.request()即可开始执行WiFi升级任务
+注:使用时在中直接调用 require"exfotawifi" 即可开始执行WiFi升级任务
+
 -- 用法实例
 local exfotawifi = require("exfotawifi")
-
-local function wifi_fota_task_func()
-    -- ...此处省略很多代码
-
-    local result = exfotawifi.request()
-    if result then
-        log.info("exfotawifi", "升级任务执行成功")
-    else
-        log.info("exfotawifi", "升级任务执行失败")
-    end
-
-    -- ...此处省略很多代码
-end
-
--- 判断网络是否正常
-local function wait_ip_ready()
-    local result, ip, adapter = sys.waitUntil("IP_READY", 30000)
-    if result then
-        log.info("exfotawifi", "开始执行升级任务")
-        sys.taskInit(wifi_fota_task_func)
-    else
-        log.error("当前正在升级WIFI&蓝牙固件,请插入可以上网的SIM卡")
-    end
-end
-
--- 在设备启动时检查SIM卡状态
-sys.taskInit(wait_ip_ready)
 ]]
 local exfotawifi = {}
 local is_request = false -- 标记是否正在执行request任务
@@ -160,71 +134,66 @@ local function fota_start(file_path)
 end
 
 
---[[
-Air8000系列模组自动升级wifi
-@api exfotawifi.request()
-@return bool 成功返回true
-@usage
-local result = exfotawifi.request()
-if result then
-    log.info("exfotawifi", "升级任务执行成功")
-else
-    log.info("exfotawifi", "升级任务执行失败")
-end
-]]
 function exfotawifi.request()
-    if is_request then
-        log.warn("exfotawifi", "升级任务正在执行中,请勿重复调用")
-        return false
-    end
+    local result, ip, adapter = sys.waitUntil("IP_READY", 30000)
+    if result then
+        log.info("exfotawifi", "开始执行升级任务")
 
-    is_request = true
-    fota_result = false
-
-    -- 构建请求URL
-    local url = "http://wififota.openluat.com/air8000/update.json"
-    local imei = is_nil(mobile.imei()) and "未知imei" or mobile.imei()
-    local version = is_nil(airlink.sver()) and "未知版本" or airlink.sver()
-    local muid = is_nil(mobile.muid()) and "未知muid" or mobile.muid()
-    local hw = is_nil(hmeta.hwver()) and "未知硬件版本" or hmeta.hwver()
-    local request_url = string.format("%s?imei=%s&version=%s&muid=%s&hw=%s", url, imei, version, muid, hw)
-
-    log.info("exfotawifi", "正在请求升级信息, URL:", request_url)
-
-    -- 发送HTTP请求获取服务器响应
-    local code, headers, body = http.request("GET", request_url, {}, nil, {timeout = 30000}).wait()
-    if code == 200 then
-        log.info("exfotawifi", "获取服务器响应成功")
-        -- 打印返回的body内容
-        -- log.info("exfotawifi", "body:", body)
-        -- 解析服务器响应的json数据
-        local response = parse_response(body)
-        if response then
-            -- 获取服务器返回的版本号和下载链接
-            local server_version = response.version
-            local download_url = response.url
-
-            -- 获取本地版本号
-            local local_version = airlink.sver()
-
-            -- 判断是否需要升级
-            if need_fota(local_version, server_version) then
-                log.info("exfotawifi", "需要升级, 本地版本:", local_version, "服务器版本:", server_version)
-                -- 下载升级文件
-                local file_path = download_file(download_url)
-                if file_path then
-                    -- 开始升级
-                    fota_result = fota_start(file_path)
+        if is_request then
+            log.warn("exfotawifi", "升级任务正在执行中,请勿重复调用")
+            return false
+        end
+        
+        is_request = true
+        fota_result = false
+
+        -- 构建请求URL
+        local url = "http://wififota.openluat.com/air8000/update.json"
+        local imei = is_nil(mobile.imei()) and "未知imei" or mobile.imei()
+        local version = is_nil(airlink.sver()) and "未知版本" or airlink.sver()
+        local muid = is_nil(mobile.muid()) and "未知muid" or mobile.muid()
+        local hw = is_nil(hmeta.hwver()) and "未知硬件版本" or hmeta.hwver()
+        local request_url = string.format("%s?imei=%s&version=%s&muid=%s&hw=%s", url, imei, version, muid, hw)
+
+        log.info("exfotawifi", "正在请求升级信息, URL:", request_url)
+
+        -- 发送HTTP请求获取服务器响应
+        local code, headers, body = http.request("GET", request_url, {}, nil, {timeout = 30000}).wait()
+        if code == 200 then
+            log.info("exfotawifi", "获取服务器响应成功")
+            -- 打印返回的body内容
+            -- log.info("exfotawifi", "body:", body)
+            -- 解析服务器响应的json数据
+            local response = parse_response(body)
+            if response then
+                -- 获取服务器返回的版本号和下载链接
+                local server_version = response.version
+                local download_url = response.url
+
+                -- 获取本地版本号
+                local local_version = airlink.sver()
+
+                -- 判断是否需要升级
+                if need_fota(local_version, server_version) then
+                    log.info("exfotawifi", "需要升级, 本地版本:", local_version, "服务器版本:", server_version)
+                    -- 下载升级文件
+                    local file_path = download_file(download_url)
+                    if file_path then
+                        -- 开始升级
+                        fota_result = fota_start(file_path)
+                    end
+                else
+                    log.info("exfotawifi", "当前已是最新WIFI固件")
+                    fota_result = true
                 end
             else
-                log.info("exfotawifi", "当前已是最新WIFI固件")
-                fota_result = true
+                log.error("exfotawifi", "解析服务器响应失败")
             end
         else
-            log.error("exfotawifi", "解析服务器响应失败")
+            log.error("exfotawifi", "获取服务器响应失败,状态码:", code)
         end
     else
-        log.error("exfotawifi", "获取服务器响应失败,状态码:", code)
+        log.error("当前正在升级WIFI&蓝牙固件,请插入可以上网的SIM卡并重新启动")
     end
 
     -- 释放请求标记
@@ -232,4 +201,7 @@ function exfotawifi.request()
     return fota_result
 end
 
+-- 启动WiFi fota任务
+sys.taskInit(exfotawifi.request)
+
 return exfotawifi

+ 10 - 0
script/libs/httpdns.lua

@@ -31,6 +31,11 @@ log.info("httpdns", "air32.cn", ip)
 ]]
 function httpdns.ali(n, opts)
     if n == nil then return end
+    if opts == nil then
+        opts = {timeout=3000}
+    elseif opts.timeout == nil then
+        opts.timeout = 3000
+    end
     local code, _, body = http.request("GET", "http://223.5.5.5/resolve?short=1&name=" .. tostring(n), nil, nil, opts).wait()
     if code == 200 and body and #body > 2 then
         local jdata = json.decode(body)
@@ -57,6 +62,11 @@ log.info("httpdns", "air32.cn", ip)
 ]]
 function httpdns.tx(n, opts)
     if n == nil then return end
+    if opts == nil then
+        opts = {timeout=3000}
+    elseif opts.timeout == nil then
+        opts.timeout = 3000
+    end
     local code, _, body = http.request("GET", "http://119.29.29.29/d?dn=" .. tostring(n), nil, nil, opts).wait()
     if code == 200 and body and #body > 2 then
         local tmp = body:split(",")