Эх сурвалжийг харах

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

alienwalker 1 сар өмнө
parent
commit
d05d5b26f1
50 өөрчлөгдсөн 5242 нэмэгдсэн , 44 устгасан
  1. 18 0
      components/airlink/src/task/luat_airlink_spi_master_task.c
  2. 10 10
      components/lcd/luat_lib_lcd_jpg.c
  3. 4 0
      components/little_flash/luat_lib_little_flash.c
  4. 4 0
      components/sfud/luat_lib_sfud.c
  5. 2 2
      components/tjpgd/tjpgd.c
  6. 2 2
      components/tjpgd/tjpgd.h
  7. 0 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/eink_drv.lua
  8. 0 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/key_drv.lua
  9. 2 1
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/main.lua
  10. 0 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/readme.md
  11. 0 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/ui/eink_page.lua
  12. 0 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/ui/home_page.lua
  13. 1 1
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/ui/time_page.lua
  14. 0 0
      module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/ui/ui_main.lua
  15. 59 0
      module/Air780EPM/demo/ui/eink/eink_drv.lua
  16. 61 0
      module/Air780EPM/demo/ui/eink/key_drv.lua
  17. 87 0
      module/Air780EPM/demo/ui/eink/main.lua
  18. 158 0
      module/Air780EPM/demo/ui/eink/readme.md
  19. 142 0
      module/Air780EPM/demo/ui/eink/ui/eink_page.lua
  20. 147 0
      module/Air780EPM/demo/ui/eink/ui/home_page.lua
  21. 189 0
      module/Air780EPM/demo/ui/eink/ui/time_page.lua
  22. 167 0
      module/Air780EPM/demo/ui/eink/ui/ui_main.lua
  23. 50 0
      module/Air780EPM/demo/ui/ht1621/ht1621_drv.lua
  24. 61 0
      module/Air780EPM/demo/ui/ht1621/key_drv.lua
  25. 83 0
      module/Air780EPM/demo/ui/ht1621/main.lua
  26. 213 0
      module/Air780EPM/demo/ui/ht1621/readme.md
  27. 339 0
      module/Air780EPM/demo/ui/ht1621/ui_main.lua
  28. 59 0
      module/Air8000/demo/ui/eink/eink_drv.lua
  29. 61 0
      module/Air8000/demo/ui/eink/key_drv.lua
  30. 87 0
      module/Air8000/demo/ui/eink/main.lua
  31. 172 0
      module/Air8000/demo/ui/eink/readme.md
  32. 142 0
      module/Air8000/demo/ui/eink/ui/eink_page.lua
  33. 147 0
      module/Air8000/demo/ui/eink/ui/home_page.lua
  34. 197 0
      module/Air8000/demo/ui/eink/ui/time_page.lua
  35. 167 0
      module/Air8000/demo/ui/eink/ui/ui_main.lua
  36. 50 0
      module/Air8000/demo/ui/ht1621/ht1621_drv.lua
  37. 61 0
      module/Air8000/demo/ui/ht1621/key_drv.lua
  38. 83 0
      module/Air8000/demo/ui/ht1621/main.lua
  39. 213 0
      module/Air8000/demo/ui/ht1621/readme.md
  40. 339 0
      module/Air8000/demo/ui/ht1621/ui_main.lua
  41. 59 0
      module/Air8101/demo/ui/eink/eink_drv.lua
  42. 61 0
      module/Air8101/demo/ui/eink/key_drv.lua
  43. 87 0
      module/Air8101/demo/ui/eink/main.lua
  44. 175 0
      module/Air8101/demo/ui/eink/readme.md
  45. 198 0
      module/Air8101/demo/ui/eink/ui/eink_page.lua
  46. 145 0
      module/Air8101/demo/ui/eink/ui/home_page.lua
  47. 197 0
      module/Air8101/demo/ui/eink/ui/time_page.lua
  48. 168 0
      module/Air8101/demo/ui/eink/ui/ui_main.lua
  49. 102 28
      script/libs/excloud.lua
  50. 473 0
      script/libs/exremotecam.lua

+ 18 - 0
components/airlink/src/task/luat_airlink_spi_master_task.c

@@ -113,6 +113,23 @@ void luat_airlink_spi_master_pin_setup(void)
     if (spi_rdy)
         return;
     spi_rdy = 1;
+#ifdef __BK72XX__
+    if (g_airlink_spi_conf.cs_pin == 0)
+    {
+        // if (g_airlink_spi_conf.spi_id == 0) {
+        g_airlink_spi_conf.cs_pin = 15;
+        // }
+        // else {
+        //     g_airlink_spi_conf.cs_pin = 8;
+        // }
+    }
+    if (g_airlink_spi_conf.rdy_pin == 0)
+    {
+        // if (g_airlink_spi_conf.spi_id == 0) {
+        g_airlink_spi_conf.rdy_pin = 48;
+        // }
+    }
+#else
     if (g_airlink_spi_conf.cs_pin == 0)
     {
         // if (g_airlink_spi_conf.spi_id == 0) {
@@ -128,6 +145,7 @@ void luat_airlink_spi_master_pin_setup(void)
         g_airlink_spi_conf.rdy_pin = 22;
         // }
     }
+#endif
     if (g_airlink_spi_conf.irq_pin == 0)
     {
         g_airlink_spi_conf.irq_pin = 255; // 默认禁用irq脚

+ 10 - 10
components/lcd/luat_lib_lcd_jpg.c

@@ -45,7 +45,7 @@ typedef struct {
 } IODEV;
 
 static unsigned int file_in_func (JDEC* jd, uint8_t* buff, unsigned int nbyte){
-    IODEV *dev = (IODEV*)jd->device;   /* Device identifier for the session (5th argument of jd_prepare function) */
+    IODEV *dev = (IODEV*)jd->device;   /* Device identifier for the session (5th argument of luat_jd_prepare function) */
     if (buff) {
         /* Read bytes from input stream */
         return luat_fs_fread(buff, 1, nbyte, dev->fp);
@@ -99,22 +99,22 @@ int lcd_draw_jpeg_default(luat_lcd_conf_t* conf, const char* path, int16_t x, in
         LLOGE("out of memory when malloc jpeg decode workbuff");
         return -3;
     }
-    res = jd_prepare(&jdec, file_in_func, work, sz_work, &devid);
+    res = luat_jd_prepare(&jdec, file_in_func, work, sz_work, &devid);
     if (res != JDR_OK) {
         luat_heap_free(work);
         luat_fs_fclose(fd);
-        LLOGW("jd_prepare file %s error %d", path, res);
+        LLOGW("luat_jd_prepare file %s error %d", path, res);
         return -2;
     }
     devid.x = x;
     devid.y = y;
     // devid.width = jdec.width;
     // devid.height = jdec.height;
-    res = jd_decomp(&jdec, lcd_out_func, 0);
+    res = luat_jd_decomp(&jdec, lcd_out_func, 0);
     luat_heap_free(work);
     luat_fs_fclose(fd);
     if (res != JDR_OK) {
-        LLOGW("jd_decomp file %s error %d", path, res);
+        LLOGW("luat_jd_decomp file %s error %d", path, res);
         return -2;
     }else {
         lcd_auto_flush(lcd_dft_conf);
@@ -123,7 +123,7 @@ int lcd_draw_jpeg_default(luat_lcd_conf_t* conf, const char* path, int16_t x, in
 }
 
 static unsigned int decode_file_in_func (JDEC* jd, uint8_t* buff, unsigned int nbyte){
-    luat_lcd_buff_info_t *buff_info = (luat_lcd_buff_info_t*)jd->device;   /* Device identifier for the session (5th argument of jd_prepare function) */
+    luat_lcd_buff_info_t *buff_info = (luat_lcd_buff_info_t*)jd->device;   /* Device identifier for the session (5th argument of luat_jd_prepare function) */
     if (buff) {
         /* Read bytes from input stream */
         return luat_fs_fread(buff, 1, nbyte, (FILE*)(buff_info->userdata));
@@ -175,18 +175,18 @@ int lcd_jpeg_decode_default(luat_lcd_conf_t* conf, const char* path, luat_lcd_bu
 		LLOGE("out of memory when malloc jpeg decode workbuff");
 		goto error;
 	}
-    res = jd_prepare(&jdec, decode_file_in_func, work, sz_work, buff_info);
+    res = luat_jd_prepare(&jdec, decode_file_in_func, work, sz_work, buff_info);
     if (res != JDR_OK) {
-        LLOGW("jd_prepare file %s error %d", path, res);
+        LLOGW("luat_jd_prepare file %s error %d", path, res);
         goto error;
     }
     buff_info->width = jdec.width;
     buff_info->height = jdec.height;
 	buff_info->len = jdec.width*jdec.height*sizeof(luat_color_t);
 	buff_info->buff = luat_heap_malloc(buff_info->len);
-    res = jd_decomp(&jdec, decode_out_func, 0);
+    res = luat_jd_decomp(&jdec, decode_out_func, 0);
     if (res != JDR_OK) {
-        LLOGW("jd_decomp file %s error %d", path, res);
+        LLOGW("luat_jd_decomp file %s error %d", path, res);
         goto error;
     }
     luat_heap_free(work);

+ 4 - 0
components/little_flash/luat_lib_little_flash.c

@@ -30,6 +30,10 @@ static int luat_little_flash_init(lua_State *L){
     little_flash_t* lf_flash = NULL;
     if (lua_type(L, 1) == LUA_TUSERDATA){
         little_flash_spi_device = (luat_spi_device_t*)lua_touserdata(L, 1);
+        if (little_flash_spi_device->spi_config.mode == 1){
+            LLOGW("flash need half mode, spi_device mode is full mode, change to half mode");
+            little_flash_spi_device->spi_config.mode = 0;
+        }
         lf_flash = luat_heap_malloc(sizeof(little_flash_t));
         memset(lf_flash, 0, sizeof(little_flash_t));
         lf_flash->spi.user_data = little_flash_spi_device;

+ 4 - 0
components/sfud/luat_lib_sfud.c

@@ -49,6 +49,10 @@ static int luat_sfud_init(lua_State *L){
     }else if (lua_type(L, 1) == LUA_TUSERDATA){
         sfud_flash_tables[0].luat_sfud.luat_spi = LUAT_TYPE_SPI_DEVICE;
         sfud_spi_device_flash = (luat_spi_device_t*)lua_touserdata(L, 1);
+        if (sfud_spi_device_flash->spi_config.mode == 1){
+            LLOGW("flash need half mode, spi_device mode is full mode, change to half mode");
+            sfud_spi_device_flash->spi_config.mode = 0;
+        }
         sfud_flash_tables[0].luat_sfud.user_data = sfud_spi_device_flash;
     }
     

+ 2 - 2
components/tjpgd/tjpgd.c

@@ -961,7 +961,7 @@ static JRESULT mcu_output (
 #define	LDB_WORD(ptr)		(uint16_t)(((uint16_t)*((uint8_t*)(ptr))<<8)|(uint16_t)*(uint8_t*)((ptr)+1))
 
 
-JRESULT jd_prepare (
+JRESULT luat_jd_prepare (
 	JDEC* jd,				/* Blank decompressor object */
 	size_t (*infunc)(JDEC*, uint8_t*, size_t),	/* JPEG strem input function */
 	void* pool,				/* Working buffer for the decompression session */
@@ -1118,7 +1118,7 @@ JRESULT jd_prepare (
 /* Start to decompress the JPEG picture                                  */
 /*-----------------------------------------------------------------------*/
 
-JRESULT jd_decomp (
+JRESULT luat_jd_decomp (
 	JDEC* jd,								/* Initialized decompression object */
 	int (*outfunc)(JDEC*, void*, JRECT*),	/* RGB output function */
 	uint8_t scale							/* Output de-scaling factor (0 to 3) */

+ 2 - 2
components/tjpgd/tjpgd.h

@@ -91,8 +91,8 @@ struct JDEC {
 
 
 /* TJpgDec API functions */
-JRESULT jd_prepare (JDEC* jd, size_t (*infunc)(JDEC*,uint8_t*,size_t), void* pool, size_t sz_pool, void* dev);
-JRESULT jd_decomp (JDEC* jd, int (*outfunc)(JDEC*,void*,JRECT*), uint8_t scale);
+JRESULT luat_jd_prepare (JDEC* jd, size_t (*infunc)(JDEC*,uint8_t*,size_t), void* pool, size_t sz_pool, void* dev);
+JRESULT luat_jd_decomp (JDEC* jd, int (*outfunc)(JDEC*,void*,JRECT*), uint8_t scale);
 
 
 #ifdef __cplusplus

+ 0 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/eink/eink_drv.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/eink_drv.lua


+ 0 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/eink/key_drv.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/key_drv.lua


+ 2 - 1
module/Air780EHM_Air780EHV_Air780EGH/demo/eink/main.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/main.lua

@@ -8,7 +8,8 @@
 
 本demo演示的核心功能为:
 1、初始化墨水屏
-2、通过屏幕显示图片、字符、色块
+2、通过按键切换页面显示
+3、显示eink核心库图形和文字
 
 更多说明参考本目录下的readme.md文件
 ]]

+ 0 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/eink/readme.md → module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/readme.md


+ 0 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/eink/ui/eink_page.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/ui/eink_page.lua


+ 0 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/eink/ui/home_page.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/ui/home_page.lua


+ 1 - 1
module/Air780EHM_Air780EHV_Air780EGH/demo/eink/ui/time_page.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/ui/time_page.lua

@@ -1,7 +1,7 @@
 --[[
 @module  time_page
 @summary 时间显示演示模块
-@version 1.1
+@version 1.0
 @date    2025.12.18
 @author  江访  
 @usage

+ 0 - 0
module/Air780EHM_Air780EHV_Air780EGH/demo/eink/ui/ui_main.lua → module/Air780EHM_Air780EHV_Air780EGH/demo/ui/eink/ui/ui_main.lua


+ 59 - 0
module/Air780EPM/demo/ui/eink/eink_drv.lua

@@ -0,0 +1,59 @@
+--[[
+@module  eink_drv
+@summary eink墨水屏显示驱动模块,基于eink核心库
+@version 1.0
+@date    2025.12.18
+@author  江访
+@usage
+本模块为eink墨水屏显示驱动功能模块,主要功能包括:
+1、初始化微雪1.54寸墨水屏(eink.MODEL_1in54_V2);
+2、配置SPI通信参数和设备对象;
+
+本文件无对外接口,require "eink_drv"即可加载运行
+]]
+
+
+--[[
+初始化eink显示驱动;
+
+@api eink_drv.init()
+@summary 配置并初始化微雪1.54寸墨水屏
+@return boolean 初始化成功返回true,失败返回false
+
+@usage
+-- 初始化eink显示
+local result = eink_drv.init()
+if result then
+    log.info("eink初始化成功")
+else
+    log.error("eink初始化失败")
+end
+]]
+
+local function eink_drv_init()
+    -- 按接线引脚正确配置GPIO号
+    local spi_id = 0
+    local pin_busy = 33
+    local pin_reset = 26
+    local pin_dc = 2
+    local pin_cs = 1
+
+    -- 开启异步刷新
+    eink.async(1)
+
+    -- 注意:eink初始化之前需要先初始化spi,使用spi对象方式初始化
+    spi_eink = spi.deviceSetup(spi_id, pin_cs, 0, 0, 8, 20 * 1000 * 1000, spi.MSB, 1, 1)
+
+    -- 初始化接到spi0的eink.MODEL_1in54_V2
+    eink.init(eink.MODEL_1in54_V2,
+        { port = "device", pin_dc = pin_dc, pin_busy = pin_busy, pin_rst = pin_reset },
+        spi_eink)
+
+    -- 设置显示窗口和方向
+    eink.setWin(200, 200, 0)
+    eink.clear(1, true)
+
+    log.info("eink_drv.init")
+end
+
+eink_drv_init()

+ 61 - 0
module/Air780EPM/demo/ui/eink/key_drv.lua

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

+ 87 - 0
module/Air780EPM/demo/ui/eink/main.lua

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

+ 158 - 0
module/Air780EPM/demo/ui/eink/readme.md

@@ -0,0 +1,158 @@
+# eink墨水屏演示系统
+
+## 一、功能模块介绍
+
+### 1.1 核心主程序模块
+1. **main.lua** - 主程序入口,负责系统初始化和任务调度
+2. **ui_main.lua** - 用户界面主控模块,管理页面切换和事件分发
+
+### 1.2 显示页面模块
+3. **home_page.lua** - 主页模块,提供应用入口和导航功能
+4. **eink_page.lua** - eink核心库演示模块
+5. **time_page.lua** - 时间显示演示模块
+
+### 1.3 驱动模块
+6. **eink_drv.lua** - eink显示驱动模块,基于eink核心库
+7. **key_drv.lua** - 按键驱动模块,管理BOOT键和PWR键
+
+## 二、按键功能说明
+
+### 2.1 按键消息
+- **"KEY_EVENT"** - 按键事件消息,包含按键类型和状态
+  - boot键事件:`boot_down`(按下)、`boot_up`(释放)
+  - pwr键事件:`pwr_down`(按下)、`pwr_up`(释放)
+
+### 2.2 按键功能定义
+- **主页页面**:
+  - BOOT键:切换选项(eink演示 ↔ 时间显示)
+  - PWR键:确认进入选中的页面
+
+- **eink演示页面**:
+  - PWR键:返回主页
+  - BOOT键:无功能
+
+- **时间显示页面**:
+  - BOOT键:切换时间显示格式(共6种格式)
+  - PWR键:返回主页
+
+## 三、显示效果
+
+<table>
+<tr>
+<td>主页<br/></td><td>eink库图像页面<br/></td><td>更新时间页面<br/></td></tr>
+<tr>
+<td rowspan="2">
+<img src="https://docs.openluat.com/cdn/image/Air780EPM_homepage.jpg" width="80" /><br/></td><td>
+<img src="https://docs.openluat.com/cdn/image/Air780EPM_einkpage.jpg" width="80" /><br/></td><td>
+<img src="https://docs.openluat.com/cdn/image/Air780EPM_eink显示时间.jpg" width="80" /><br/></td></tr>
+</table>
+
+## 四、硬件接线配置
+
+### 4.1 物料清单
+- Air780EPM核心板 × 1
+- 微雪1.54寸墨水屏 × 1 [demo所使用的墨水屏购买链接](https://e.tb.cn/h.7VUl8PgFVWhwLJS?tk=e3FVfDz34Ki)
+- 母对母杜邦线 × 8
+- TYPE-C 数据线 × 1
+- Air780EPM核心板和微雪1.54寸墨水屏的硬件接线方式为
+
+  - Air780EPM 核心板通过 TYPE-C USB 口供电(核心板正面开关拨到 ON 一端),此种供电方式下,VDD_EXT 引脚为 3.3V,可以直接给微雪1.54寸墨水屏供电;
+  - 为了演示方便,所以 Air780EPM 核心板上电后直接通过VDD_EXT引脚给微雪1.54寸墨水屏供电;
+  - 客户在设计实际项目时,一般来说,需要通过一个 GPIO 来控制 LDO 给配件板供电,这样可以灵活地控制配件板的供电,可以使项目的整体功耗降到最低;
+
+### 4.2 接线方式
+
+|Air780EPM核心板| 墨水屏引脚 |
+|---------------|-----------|
+| 26/GPIO33     | BUSY      |
+| 25/GPIO26     | RST       |
+| 23/GPIO2      | DC        |
+| 22/GPIO1      | CS        |
+| 86/SPI0CLK    | SCK       |
+| 85/SPI0MOSI   | DIN       |
+| 24/VDD_EXT    | VCC       |
+| GND           | GND       |
+
+#### 4.2.3 接线图
+![](https://docs.openLuat.com/cdn/image/Air780EHM_eink接线图.jpg)
+
+## 五、演示软件环境
+
+### 5.1 开发工具
+
+- [Luatools下载调试工具](https://docs.openluat.com/air780epm/luatos/common/download/) - 固件烧录和代码调试
+
+### 5.2 内核固件
+
+- [点击下载Air780EPM系列最新版本内核固件](https://docs.openluat.com/air780epm/luatos/firmware/version/),demo所使用的是LuatOS-SoC_V2018_Air780EPM 1号固件
+  
+## 六、演示操作步骤
+
+### 6.1 硬件准备
+1. 按照接线表连接eink屏幕
+2. 确保电源连接正确,通过TYPE-C USB口供电
+3. 确保所有连接正确无误
+
+
+### 6.2 软件配置
+在`main.lua`中选择加载对应的驱动模块:
+
+```lua
+-- 加载eink显示驱动管理功能模块
+require "eink_drv"
+
+-- 加载按键驱动管理功能模块
+require "key_drv"
+
+-- 加载eink核心库实现的用户界面功能模块
+-- 实现多页面切换、按键事件分发和界面渲染功能
+-- 包含主页、eink演示页和时间显示页
+require "ui_main"
+
+```
+
+### 6.3 软件烧录
+1. 使用Luatools烧录最新内核固件
+2. 下载并烧录本项目所有脚本文件
+3. 设备自动重启后开始运行
+
+### 6.4 功能测试
+
+#### 主页操作
+1. 设备启动后显示主页
+2. 使用BOOT键在两个选项间切换
+3. 使用PWR键进入选中的页面
+
+#### eink演示页面
+1. 查看基本图形绘制示例
+2. 查看文本显示效果
+3. 查看二维码和电池图标
+4. 按PWR键返回主页
+
+#### 时间显示页面
+1. 查看当前时间显示
+2. 使用BOOT键切换不同的时间格式
+3. 按PWR键返回主页
+
+### 6.5 预期效果
+- **主页**:清晰显示两个选项,选中状态明显
+- **eink演示页面**:图形绘制清晰,布局合理
+- **时间显示页面**:时间格式切换流畅
+- **按键交互**:响应及时准确,页面切换正常
+
+## 七、故障排除
+
+1. **屏幕不显示**:检查SPI接线,确认电源供电
+2. **按键无响应**:确认GPIO配置正确
+3. **时间显示异常**:检查系统时间设置
+4. **页面切换异常**:检查内存使用情况,适当调整刷新间隔
+
+## 八、注意事项
+
+1. eink屏幕刷新较慢,请勿频繁切换页面
+2. 按键操作后请等待屏幕刷新完成
+3. 长时间运行请确保电源稳定
+4. 避免在高温高湿环境下使用
+
+## 九、拓展使用
+本demo内所演示的图形显示接口均可在eink核心库内找到,更丰富和详细的使用说明可以点击进入[eink核心库](https://docs.openluat.com/osapi/core/eink/)

+ 142 - 0
module/Air780EPM/demo/ui/eink/ui/eink_page.lua

@@ -0,0 +1,142 @@
+--[[
+@module  eink_page
+@summary eink核心库演示模块
+@version 1.0
+@date    2025.12.25
+@author  江访  
+@usage
+本模块为eink核心库演示功能模块,主要功能包括:
+1、展示eink核心库的基本图形绘制功能;
+2、演示线、矩形、圆形等基本图形绘制;
+3、显示文本和二维码生成功能;
+4、提供电池图标显示功能;
+
+按键功能:
+- PWR键:返回主页
+- BOOT键:无功能
+
+对外接口:
+1、eink_page.draw():绘制eink演示页面
+2、eink_page.handle_key():处理eink页面按键事件
+3、eink_page.on_enter():页面进入时重置状态
+4、eink_page.on_leave():页面离开时执行清理操作
+]] 
+
+local eink_page = {}
+
+--[[
+绘制eink演示页面;
+绘制eink演示页面的所有图形和UI元素;
+
+@api eink_page.draw()
+@summary 绘制eink演示页面的所有图形和UI元素
+@return nil
+
+@usage
+-- 在UI主循环中调用
+eink_page.draw()
+]] 
+function eink_page.draw()
+    -- 清除绘图缓冲区(使用白色背景)
+    eink.clear(1, true)
+    
+    -- 绘制外边框(水平+垂直线组合)
+    eink.line(5, 5, 195, 5, 0)     -- 上边框水平线
+    eink.line(5, 195, 195, 195, 0) -- 下边框水平线
+    eink.line(5, 5, 5, 195, 0)     -- 左边框垂直线
+    eink.line(195, 5, 195, 195, 0) -- 右边框垂直线
+
+    -- 标题区域(22号英文字体)
+    eink.setFont(eink.font_opposansm22)
+    eink.rect(10, 10, 190, 40, 0, 0)     -- 标题背景(无斜线)
+    eink.print(35, 34, "LuatOS-eink", 0) -- 黑色文字
+
+    -- 区域分隔线(水平)
+    eink.line(10, 50, 190, 50, 0)   -- 标题与内容分隔线
+    eink.line(100, 55, 100, 190, 0) -- 左右区域分隔线(垂直)
+
+    -- 左侧区域(左半屏)
+    -- 1. 文本演示,中文目前仅支持12号英文字体
+    eink.setFont(eink.font_opposansm12)
+    eink.print(15, 65, "1. Font-12px", 0)
+    eink.print(20, 85, "PWR:Return", 0)
+
+    -- 2. 矩形与线条演示
+    eink.print(15, 110, "2. Graphics", 0)
+    eink.circle(33, 135, 15, 0, 0)    -- 空心圆形
+    eink.circle(73, 135, 15, 0, 1)    -- 实心圆形
+    eink.rect(20, 160, 60, 185, 0, 0) -- 空心矩形
+    eink.rect(70, 160, 90, 185, 0, 1) -- 实心矩形
+
+    -- 右侧区域(右半屏)
+    -- 3. 二维码演示
+    eink.print(110, 65, "3. QR Code", 0)
+    eink.qrcode(115, 70, "https://docs.openluat.com/osapi/core/eink/", 69)
+
+    -- 4. 电池与位图演示
+    eink.print(110, 160, "4. Icons", 0)
+    eink.bat(120, 170, 3750) -- 电池图标
+    eink.print(148, 178, "Battery", 0)
+
+    -- 刷新屏幕(不清屏)
+    eink.show(0, 0, true)
+end
+
+--[[
+处理按键事件;
+根据按键类型执行相应的操作;
+
+@api eink_page.handle_key(key_type, switch_page)
+@summary 处理eink页面按键事件
+@string key_type 按键类型
+@valid_values "pwr_up"
+@function switch_page 页面切换回调函数
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+local handled = eink_page.handle_key("pwr_up", switch_page)
+]] 
+function eink_page.handle_key(key_type, switch_page)
+    log.info("eink_page.handle_key", "key_type:", key_type)
+    
+    if key_type == "pwr_up" then
+        -- PWR键:返回首页
+        switch_page("home")
+        return true
+    end
+    -- BOOT键无功能
+    return false
+end
+
+--[[
+页面进入时重置状态;
+
+@api eink_page.on_enter()
+@summary 页面进入时重置状态
+@return nil
+
+@usage
+-- 在页面切换时调用
+eink_page.on_enter()
+]] 
+function eink_page.on_enter()
+    log.info("eink_page", "进入eink演示页面")
+end
+
+--[[
+页面离开时执行清理操作;
+
+@api eink_page.on_leave()
+@summary 页面离开时执行清理操作
+@return nil
+
+@usage
+-- 在页面切换时调用
+eink_page.on_leave()
+]] 
+function eink_page.on_leave()
+    log.info("eink_page", "离开eink演示页面")
+end
+
+return eink_page

+ 147 - 0
module/Air780EPM/demo/ui/eink/ui/home_page.lua

@@ -0,0 +1,147 @@
+--[[
+@module  home_page
+@summary eink主页模块,提供应用入口和导航功能
+@version 1.0
+@date    2025.12.25
+@author  江访  
+@usage
+本模块为主页模块,主要功能包括:
+1、提供应用入口和导航功能;
+2、显示系统标题和操作提示;
+3、管理两个功能选项的选中状态;
+4、处理主页面的按键事件;
+
+对外接口:
+1、home_page.draw():绘制主页面所有UI元素,包括选中指示
+2、home_page.handle_key():处理主页面按键事件
+3、home_page.on_enter():页面进入时重置选中状态
+]] 
+
+local home_page = {}
+
+-- 选项区域定义(eink屏幕200x200,需合理布局)
+local options = {
+    {name = "eink_demo", text = "eink Demo", x1 = 20, y1 = 80, x2 = 90, y2 = 130, color = 0},
+    {name = "time_demo", text = "Time Demo", x1 = 110, y1 = 80, x2 = 180, y2 = 130, color = 0}
+}
+
+-- 当前选中项索引
+local selected_index = 1
+
+
+--[[
+绘制主页界面;
+绘制主页面所有UI元素,包括选中指示;
+
+@api home_page.draw()
+@summary 绘制主页面所有UI元素,包括选中指示
+@return nil
+
+@usage
+-- 在UI主循环中调用
+home_page.draw()
+]] 
+function home_page.draw()
+    -- 清除绘图缓冲区
+    eink.clear(1, true)
+    eink.setFont(eink.font_opposansm20)
+    -- 显示标题
+    eink.rect(10, 10, 190, 45, 0, 0)     -- 标题背景框
+    eink.print(50, 35, "eink Demo", 0)
+
+    eink.setFont(eink.font_opposansm12)
+    -- 显示操作提示
+    eink.print(22, 65, "BOOT:Select PWR:Confirm", 0)
+
+    -- 绘制分隔线
+    eink.line(10, 70, 190, 70, 0)
+
+    -- 绘制所有选项框
+    for i, opt in ipairs(options) do
+        -- 绘制选项框
+        if i == selected_index then
+            -- 选中状态:实心矩形
+            eink.rect(opt.x1, opt.y1, opt.x2, opt.y2, 0, 1)
+            eink.print(opt.x1 + 3, opt.y1 + 25, opt.text, 1)  -- 白色文字
+        else
+            -- 未选中状态:空心矩形
+            eink.rect(opt.x1, opt.y1, opt.x2, opt.y2, 0, 0)
+            eink.print(opt.x1 + 3, opt.y1 + 25, opt.text, 0)  -- 黑色文字
+        end
+    end
+
+    -- 绘制底部信息
+    eink.print(50, 155, "eink Core Library", 0)
+    eink.print(55, 175, "Waveshare 1.54", 0)
+
+    -- 刷新屏幕
+    eink.show(0, 0, true)
+end
+
+--[[
+处理主页按键事件;
+根据按键类型执行相应的操作;
+
+@api home_page.handle_key(key_type, switch_page)
+@summary 处理主页按键事件
+@string key_type 按键类型
+@valid_values "confirm", "next", "prev", "back"
+@function switch_page 页面切换回调函数
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+local handled = home_page.handle_key("next", switch_page)
+]] 
+function home_page.handle_key(key_type, switch_page)
+    log.info("home_page.handle_key", "key_type:", key_type, "selected_index:", selected_index)
+    
+    if key_type == "confirm" or key_type == "pwr_up" then
+        -- 确认键:切换到选中的页面
+        local opt = options[selected_index]
+        switch_page(opt.name)
+        return true
+    elseif key_type == "boot_up" then
+        -- BOOT键:切换选项
+        selected_index = selected_index % #options + 1
+        return true
+    elseif key_type == "back" then
+        -- 返回键:当前主页不需要返回功能
+        return false
+    end
+    return false
+end
+
+--[[
+页面进入时重置选中状态;
+重置选中状态为第一个选项;
+
+@api home_page.on_enter()
+@summary 重置选中状态
+@return nil
+
+@usage
+-- 在页面切换时调用
+home_page.on_enter()
+]] 
+function home_page.on_enter()
+    selected_index = 1  -- 默认选中第一个
+    log.info("home_page", "进入主页")
+end
+
+--[[
+页面离开时执行清理操作;
+
+@api home_page.on_leave()
+@summary 页面离开时执行清理操作
+@return nil
+
+@usage
+-- 在页面切换时调用
+home_page.on_leave()
+]] 
+function home_page.on_leave()
+    log.info("home_page", "离开主页")
+end
+
+return home_page

+ 189 - 0
module/Air780EPM/demo/ui/eink/ui/time_page.lua

@@ -0,0 +1,189 @@
+--[[
+@module  time_page
+@summary 时间显示演示模块
+@version 1.0
+@date    2025.12.25
+@author  江访
+@usage
+本模块为时间显示演示功能模块,主要功能包括:
+1、使用os.date接口获取当前时间;
+2、显示多种时间格式;
+3、支持实时时间更新;
+
+按键功能:
+- PWR键:返回主页
+- BOOT键:切换时间显示格式
+
+对外接口:
+1、time_page.draw():绘制时间显示页面
+2、time_page.handle_key():处理时间页面按键事件
+3、time_page.on_enter():页面进入时重置状态
+4、time_page.on_leave():页面离开时执行清理操作
+]]
+
+local time_page = {}
+
+-- 时间显示状态
+local time_state = {
+    format_index = 1,       -- 当前显示格式索引
+    last_update = 0,        -- 最后更新时间
+    update_interval = 5000, -- 更新间隔(5秒)
+    display_formats = {     -- 时间显示格式列表
+        { name = "Full Format",  format = "%Y/%m/%d %H:%M:%S" },
+        { name = "Short Format", format = "%Y-%m-%d %H:%M" },
+        { name = "Time Only",    format = "%H:%M:%S" },
+        { name = "Date Only",    format = "%Y/%m/%d" },
+        { name = "Weekday",      format = "%A %H:%M" },
+        { name = "UTC Time",     format = "!%Y-%m-%d %H:%M:%S" }
+    }
+}
+
+--[[
+获取当前时间字符串
+@local
+@return string 格式化后的时间字符串
+]]
+local function get_time_string()
+    local format_info = time_state.display_formats[time_state.format_index]
+    return os.date(format_info.format)
+end
+
+--[[
+绘制时间显示页面;
+绘制时间显示页面的所有UI元素;
+
+@api time_page.draw()
+@summary 绘制时间显示页面的所有UI元素
+@return nil
+
+@usage
+-- 在UI主循环中调用
+time_page.draw()
+]]
+function time_page.draw()
+    -- 清除绘图缓冲区
+    eink.clear(1, true)
+
+    -- 显示标题
+    eink.setFont(eink.font_opposansm22)
+    eink.rect(10, 10, 190, 45, 0, 0) -- 标题背景框
+    eink.print(30, 35, "Time Display", 0)
+
+    eink.setFont(eink.font_opposansm16)
+    -- 显示当前时间
+    local time_str = get_time_string()
+
+    -- 时间显示框
+    eink.rect(15, 60, 185, 110, 0, 0)
+
+    -- 显示时间
+    eink.print(21, 90, time_str, 0)
+  
+    -- 显示格式信息
+    eink.setFont(eink.font_opposansm12)
+    local format_info = time_state.display_formats[time_state.format_index]
+    eink.print(30, 130, "Format:", 0)
+    eink.print(110, 130, format_info.name, 0)
+
+    -- 显示格式索引
+    eink.print(90, 145, string.format("%d/%d",
+        time_state.format_index,
+        #time_state.display_formats), 0)
+
+    -- 绘制分隔线
+    eink.line(10, 150, 190, 150, 0)
+
+    -- 显示操作提示
+    eink.print(35, 175, "BOOT:Change Format", 0)
+    eink.print(35, 190, "PWR: Return to Home", 0)
+
+    -- 刷新屏幕
+    eink.show(0, 0, true)
+
+    -- 更新最后更新时间
+    time_state.last_update = mcu.ticks()
+end
+
+--[[
+处理按键事件;
+根据按键类型执行相应的操作;
+
+@api time_page.handle_key(key_type, switch_page)
+@summary 处理时间页面按键事件
+@string key_type 按键类型
+@valid_values "boot_up", "pwr_up"
+@function switch_page 页面切换回调函数
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+local handled = time_page.handle_key("boot_up", switch_page)
+]]
+function time_page.handle_key(key_type, switch_page)
+    log.info("time_page.handle_key", "key_type:", key_type)
+
+    if key_type == "boot_up" then
+        -- BOOT键:切换时间显示格式
+        time_state.format_index = time_state.format_index % #time_state.display_formats + 1
+        log.info("time_page", "切换到格式:", time_state.format_index)
+        return true
+    elseif key_type == "pwr_up" then
+        -- PWR键:返回首页
+        switch_page("home")
+        return true
+    end
+    return false
+end
+
+--[[
+检查是否需要更新时间;
+基于时间间隔判断是否需要刷新显示;
+
+@api time_page.need_update()
+@summary 检查是否需要更新时间显示
+@return bool 需要更新返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+if time_page.need_update() then
+    time_page.draw()
+end
+]]
+function time_page.need_update()
+    local current_time = mcu.ticks()
+    return (current_time - time_state.last_update) >= time_state.update_interval
+end
+
+--[[
+页面进入时重置状态;
+
+@api time_page.on_enter()
+@summary 页面进入时重置状态
+@return nil
+
+@usage
+-- 在页面切换时调用
+time_page.on_enter()
+]]
+function time_page.on_enter()
+    time_state.format_index = 1
+    time_state.last_update = 0
+    log.info("time_page", "进入时间显示页面")
+end
+
+--[[
+页面离开时执行清理操作;
+
+@api time_page.on_leave()
+@summary 页面离开时执行清理操作
+@return nil
+
+@usage
+-- 在页面切换时调用
+time_page.on_leave()
+]]
+function time_page.on_leave()
+    log.info("time_page", "离开时间显示页面")
+end
+
+return time_page

+ 167 - 0
module/Air780EPM/demo/ui/eink/ui/ui_main.lua

@@ -0,0 +1,167 @@
+--[[
+@module  ui_main
+@summary eink用户界面主控模块,负责页面管理和事件分发
+@version 1.0
+@date    2025.12.18
+@author  江访  
+本模块为eink用户界面主控模块,主要功能包括:
+1、管理页面切换和事件分发;
+2、处理按键事件并调用对应页面的处理函数;
+3、协调各页面之间的状态转移;
+4、控制界面刷新频率;
+
+对外接口:
+1、ui_main():用户界面主任务,初始化显示和按键驱动,启动UI主循环
+]] 
+
+-- 加载子页面
+local home_page = require "home_page"
+local eink_page = require "eink_page"
+local time_page = require "time_page"
+
+-- 当前页面状态
+local current_page = "home"
+local last_page = ""
+
+--[[
+切换页面;
+从当前页面切换到目标页面;
+
+@api switch_page(new_page)
+@summary 执行页面切换操作
+@string new_page 目标页面名称
+@valid_values "home", "eink_demo", "time_demo"
+@return nil
+
+@usage
+-- 切换到主页
+switch_page("home")
+
+-- 切换到eink演示页面
+switch_page("eink_demo")
+
+-- 切换到时间显示页面
+switch_page("time_demo")
+]] 
+local function switch_page(new_page)
+    log.info("switch_page", "从", current_page, "切换到", new_page)
+    
+    -- 调用旧页面的退出函数
+    if current_page == "home" and home_page.on_leave then
+        home_page.on_leave()
+    elseif current_page == "eink_demo" and eink_page.on_leave then
+        eink_page.on_leave()
+    elseif current_page == "time_demo" and time_page.on_leave then
+        time_page.on_leave()
+    end
+
+    last_page = current_page
+    current_page = new_page
+
+    -- 调用新页面的进入函数
+    if new_page == "home" and home_page.on_enter then
+        home_page.on_enter()
+    elseif new_page == "eink_demo" and eink_page.on_enter then
+        eink_page.on_enter()
+    elseif new_page == "time_demo" and time_page.on_enter then
+        time_page.on_enter()
+    end
+
+    log.info("ui_main", "已切换到页面:", current_page)
+end
+
+--[[
+处理按键事件;
+根据按键类型和当前页面调用对应的处理函数;
+
+@api handle_key_event(key_event)
+@summary 处理按键事件并分发到对应页面
+@string key_event 按键事件类型
+@valid_values "boot_up", "pwr_up"
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在ui_main任务中调用
+local handled = handle_key_event("boot_up")
+]] 
+local function handle_key_event(key_event)
+    log.info("按键事件", "event:", key_event, "当前页面:", current_page)
+    
+    if key_event == "boot_up" then
+        -- BOOT键:在主页作为方向键,在时间页面切换格式
+        if current_page == "home" then
+            -- 主页:切换选项
+            return home_page.handle_key("boot_up", switch_page)
+        elseif current_page == "time_demo" then
+            -- 时间页面:切换时间格式
+            if time_page.handle_key then
+                return time_page.handle_key("boot_up", switch_page)
+            end
+        end
+        -- 其他页面BOOT键无功能
+        return false
+    elseif key_event == "pwr_up" then
+        -- PWR键:确认/返回功能
+        if current_page == "home" then
+            return home_page.handle_key("confirm", switch_page)
+        elseif current_page == "eink_demo" then
+            -- eink演示页面:返回首页
+            if eink_page.handle_key then
+                return eink_page.handle_key("pwr_up", switch_page)
+            end
+        elseif current_page == "time_demo" then
+            -- 时间页面:返回首页
+            if time_page.handle_key then
+                return time_page.handle_key("pwr_up", switch_page)
+            end
+        end
+    end
+    
+    return false
+end
+
+--[[
+用户界面主任务;
+初始化显示和按键驱动,启动UI主循环;
+
+@api ui_main()
+@summary 初始化显示和按键驱动,启动UI主循环
+@return nil
+
+@usage
+-- 在主程序中通过sys.taskInit调用
+sys.taskInit(ui_main)
+]] 
+local function ui_main()
+
+    -- 调用主页的进入函数
+    if home_page.on_enter then
+        home_page.on_enter()
+    end
+
+    while true do
+        -- 根据当前页面绘制内容
+        if current_page == "home" then
+            home_page.draw()
+        elseif current_page == "eink_demo" then
+            eink_page.draw()
+        elseif current_page == "time_demo" then
+            time_page.draw()
+        end
+
+        local result, key_event = sys.waitUntil("KEY_EVENT")
+        if result then
+            -- 处理按键事件
+            handle_key_event(key_event)
+        else
+            -- 超时:时间页面需要刷新显示
+            if current_page == "time_demo" then
+                if time_page.need_update then
+                    time_page.draw()
+                end
+            end
+        end
+    end
+end
+
+sys.taskInit(ui_main)

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

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

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

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

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

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

+ 213 - 0
module/Air780EPM/demo/ui/ht1621/readme.md

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

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

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

+ 59 - 0
module/Air8000/demo/ui/eink/eink_drv.lua

@@ -0,0 +1,59 @@
+--[[
+@module  eink_drv
+@summary eink墨水屏显示驱动模块,基于eink核心库
+@version 1.0
+@date    2025.12.18
+@author  江访
+@usage
+本模块为eink墨水屏显示驱动功能模块,主要功能包括:
+1、初始化微雪1.54寸墨水屏(eink.MODEL_1in54_V2);
+2、配置SPI通信参数和设备对象;
+
+本文件无对外接口,require "eink_drv"即可加载运行
+]]
+
+
+--[[
+初始化eink显示驱动;
+
+@api eink_drv.init()
+@summary 配置并初始化微雪1.54寸墨水屏
+@return boolean 初始化成功返回true,失败返回false
+
+@usage
+-- 初始化eink显示
+local result = eink_drv.init()
+if result then
+    log.info("eink初始化成功")
+else
+    log.error("eink初始化失败")
+end
+]]
+
+local function eink_drv_init()
+    -- 按接线引脚正确配置GPIO号
+    local spi_id = 1
+    local pin_busy = 16
+    local pin_reset = 17
+    local pin_dc = 20
+    local pin_cs = 21
+
+    -- 开启异步刷新
+    eink.async(1)
+
+    -- 注意:eink初始化之前需要先初始化spi,使用spi对象方式初始化
+    spi_eink = spi.deviceSetup(spi_id, pin_cs, 0, 0, 8, 20 * 1000 * 1000, spi.MSB, 1, 1)
+
+    -- 初始化接到spi0的eink.MODEL_1in54_V2
+    eink.init(eink.MODEL_1in54_V2,
+        { port = "device", pin_dc = pin_dc, pin_busy = pin_busy, pin_rst = pin_reset },
+        spi_eink)
+
+    -- 设置显示窗口和方向
+    eink.setWin(200, 200, 0)
+    eink.clear(1, true)
+
+    log.info("eink_drv.init")
+end
+
+eink_drv_init()

+ 61 - 0
module/Air8000/demo/ui/eink/key_drv.lua

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

+ 87 - 0
module/Air8000/demo/ui/eink/main.lua

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

+ 172 - 0
module/Air8000/demo/ui/eink/readme.md

@@ -0,0 +1,172 @@
+# eink墨水屏演示系统
+
+## 一、功能模块介绍
+
+### 1.1 核心主程序模块
+
+1. **main.lua** - 主程序入口,负责系统初始化和任务调度
+2. **ui_main.lua** - 用户界面主控模块,管理页面切换和事件分发
+
+### 1.2 显示页面模块
+
+3. **home_page.lua** - 主页模块,提供应用入口和导航功能
+4. **eink_page.lua** - eink核心库演示模块
+5. **time_page.lua** - 时间显示演示模块
+
+### 1.3 驱动模块
+
+6. **eink_drv.lua** - eink显示驱动模块,基于eink核心库
+7. **key_drv.lua** - 按键驱动模块,管理BOOT键和PWR键
+
+## 二、按键功能说明
+
+### 2.1 按键消息
+
+- **"KEY_EVENT"** - 按键事件消息,包含按键类型和状态
+  - boot键事件:`boot_down`(按下)、`boot_up`(释放)
+  - pwr键事件:`pwr_down`(按下)、`pwr_up`(释放)
+
+### 2.2 按键功能定义
+
+- **主页页面**:
+  - BOOT键:切换选项(eink演示 ↔ 时间显示)
+  - PWR键:确认进入选中的页面
+
+- **eink演示页面**:
+  - PWR键:返回主页
+  - BOOT键:无功能
+
+- **时间显示页面**:
+  - BOOT键:切换时间显示格式(共6种格式)
+  - PWR键:返回主页
+
+## 三、显示效果
+
+<table>
+<tr>
+<td>主页<br/></td><td>eink库图像页面<br/></td><td>更新时间页面<br/></td></tr>
+<tr>
+<td rowspan="2">
+<img src="https://docs.openluat.com/cdn/image/Air780EHM_homepage.jpg" width="80" /><br/></td><td>
+<img src="https://docs.openluat.com/cdn/image/Air780EHM_einkpage.jpg" width="80" /><br/></td><td>
+<img src="https://docs.openluat.com/cdn/image/Air780EHM_eink显示时间.jpg" width="80" /><br/></td></tr>
+</table>
+
+## 四、硬件接线配置
+
+### 4.1 物料清单
+
+- Air8000核心板 × 1
+- 微雪1.54寸墨水屏 × 1 [demo所使用的墨水屏购买链接](https://e.tb.cn/h.7VUl8PgFVWhwLJS?tk=e3FVfDz34Ki)
+- 母对母杜邦线 × 8
+- TYPE-C 数据线 × 1
+- Air8000核心板和微雪1.54寸墨水屏的硬件接线方式为
+
+  - Air8000 核心板通过 TYPE-C USB 口供电(核心板背面的开关拨到 USB ON 一端,正面开关拨到 供电 一端),此种供电方式下,VDD_EXT 引脚为 3.3V,可以直接给微雪1.54寸墨水屏供电;
+  - 为了演示方便,所以 Air8000 核心板上电后直接通过VDD_EXT引脚给微雪1.54寸墨水屏供电;
+  - 客户在设计实际项目时,一般来说,需要通过一个 GPIO 来控制 LDO 给配件板供电,这样可以灵活地控制配件板的供电,可以使项目的整体功耗降到最低;
+
+### 4.2 接线方式
+
+|Air8000核心板| 墨水屏引脚 |
+|-------------|-----------|
+| GPIO16      | BUSY      |
+| GPIO17      | RST       |
+| GPIO20      | DC        |
+| GPIO21      | CS        |
+| SPI1_SCLK   | SCK       |
+| SPI1_MOSI   | DIN       |
+| VDD_EXT     | VCC       |
+| GND         | GND       |
+
+#### 4.2.3 接线图
+
+![](https://docs.openLuat.com/cdn/image/Air8000_eink接线图.jpg)
+
+## 五、演示软件环境
+
+### 5.1 开发工具
+
+- [Luatools下载调试工具](https://docs.openluat.com/air8000/luatos/common/download/) - 固件烧录和代码调试
+
+### 5.2 内核固件
+
+- [点击下载Air8000系列最新版本内核固件](https://docs.openluat.com/air8000/luatos/firmware/version/),demo所使用的是LuatOS-SoC_V2018_Air8000 1号固件
+
+## 六、演示操作步骤
+
+### 6.1 硬件准备
+
+1. 按照接线表连接eink屏幕
+2. 确保电源连接正确,通过TYPE-C USB口供电
+3. 确保所有连接正确无误
+
+### 6.2 软件配置
+
+在`main.lua`中选择加载对应的驱动模块:
+
+```lua
+-- 加载eink显示驱动管理功能模块
+require "eink_drv"
+
+-- 加载按键驱动管理功能模块
+require "key_drv"
+
+-- 加载eink核心库实现的用户界面功能模块
+-- 实现多页面切换、按键事件分发和界面渲染功能
+-- 包含主页、eink演示页和时间显示页
+require "ui_main"
+
+```
+
+### 6.3 软件烧录
+
+1. 使用Luatools烧录最新内核固件
+2. 下载并烧录本项目所有脚本文件
+3. 设备自动重启后开始运行
+
+### 6.4 功能测试
+
+#### 主页操作
+
+1. 设备启动后显示主页
+2. 使用BOOT键在两个选项间切换
+3. 使用PWR键进入选中的页面
+
+#### eink演示页面
+
+1. 查看基本图形绘制示例
+2. 查看文本显示效果
+3. 查看二维码和电池图标
+4. 按PWR键返回主页
+
+#### 时间显示页面
+
+1. 查看当前时间显示
+2. 使用BOOT键切换不同的时间格式
+3. 按PWR键返回主页
+
+### 6.5 预期效果
+
+- **主页**:清晰显示两个选项,选中状态明显
+- **eink演示页面**:图形绘制清晰,布局合理
+- **时间显示页面**:时间格式切换流畅
+- **按键交互**:响应及时准确,页面切换正常
+
+## 七、故障排除
+
+1. **屏幕不显示**:检查SPI接线,确认电源供电
+2. **按键无响应**:确认GPIO配置正确
+3. **时间显示异常**:检查系统时间设置
+4. **页面切换异常**:检查内存使用情况,适当调整刷新间隔
+
+## 八、注意事项
+
+1. eink屏幕刷新较慢,请勿频繁切换页面
+2. 按键操作后请等待屏幕刷新完成
+3. 长时间运行请确保电源稳定
+4. 避免在高温高湿环境下使用
+
+## 九、拓展使用
+
+本demo内所演示的图形显示接口均可在eink核心库内找到,更丰富和详细的使用说明可以点击进入[eink核心库](https://docs.openluat.com/osapi/core/eink/)

+ 142 - 0
module/Air8000/demo/ui/eink/ui/eink_page.lua

@@ -0,0 +1,142 @@
+--[[
+@module  eink_page
+@summary eink核心库演示模块
+@version 1.0
+@date    2025.12.18
+@author  江访  
+@usage
+本模块为eink核心库演示功能模块,主要功能包括:
+1、展示eink核心库的基本图形绘制功能;
+2、演示线、矩形、圆形等基本图形绘制;
+3、显示文本和二维码生成功能;
+4、提供电池图标显示功能;
+
+按键功能:
+- PWR键:返回主页
+- BOOT键:无功能
+
+对外接口:
+1、eink_page.draw():绘制eink演示页面
+2、eink_page.handle_key():处理eink页面按键事件
+3、eink_page.on_enter():页面进入时重置状态
+4、eink_page.on_leave():页面离开时执行清理操作
+]] 
+
+local eink_page = {}
+
+--[[
+绘制eink演示页面;
+绘制eink演示页面的所有图形和UI元素;
+
+@api eink_page.draw()
+@summary 绘制eink演示页面的所有图形和UI元素
+@return nil
+
+@usage
+-- 在UI主循环中调用
+eink_page.draw()
+]] 
+function eink_page.draw()
+    -- 清除绘图缓冲区(使用白色背景)
+    eink.clear(1, true)
+    
+    -- 绘制外边框(水平+垂直线组合)
+    eink.line(5, 5, 195, 5, 0)     -- 上边框水平线
+    eink.line(5, 195, 195, 195, 0) -- 下边框水平线
+    eink.line(5, 5, 5, 195, 0)     -- 左边框垂直线
+    eink.line(195, 5, 195, 195, 0) -- 右边框垂直线
+
+    -- 标题区域(22号英文字体)
+    eink.setFont(eink.font_opposansm22)
+    eink.rect(10, 10, 190, 40, 0, 0)     -- 标题背景(无斜线)
+    eink.print(35, 34, "LuatOS-eink", 0) -- 黑色文字
+
+    -- 区域分隔线(水平)
+    eink.line(10, 50, 190, 50, 0)   -- 标题与内容分隔线
+    eink.line(100, 55, 100, 190, 0) -- 左右区域分隔线(垂直)
+
+    -- 左侧区域(左半屏)
+    -- 1. 文本演示,中文目前仅支持12号中文字体
+    eink.setFont(eink.font_opposansm12_chinese)
+    eink.print(15, 65, "1. 12中文字体", 0)
+    eink.print(20, 85, "按PWR键返回", 0)
+
+    -- 2. 矩形与线条演示
+    eink.print(15, 110, "2. 图形演示", 0)
+    eink.circle(33, 135, 15, 0, 0)    -- 空心圆形
+    eink.circle(73, 135, 15, 0, 1)    -- 实心圆形
+    eink.rect(20, 160, 60, 185, 0, 0) -- 空心矩形
+    eink.rect(70, 160, 90, 185, 0, 1) -- 实心矩形
+
+    -- 右侧区域(右半屏)
+    -- 3. 二维码演示
+    eink.print(110, 65, "3. 二维码", 0)
+    eink.qrcode(115, 70, "https://docs.openluat.com/osapi/core/eink/", 69)
+
+    -- 4. 电池与位图演示
+    eink.print(110, 160, "4. 状态图标", 0)
+    eink.bat(120, 170, 3750) -- 电池图标
+    eink.print(150, 180, "电量", 0)
+
+    -- 刷新屏幕(不清屏)
+    eink.show(0, 0, true)
+end
+
+--[[
+处理按键事件;
+根据按键类型执行相应的操作;
+
+@api eink_page.handle_key(key_type, switch_page)
+@summary 处理eink页面按键事件
+@string key_type 按键类型
+@valid_values "pwr_up"
+@function switch_page 页面切换回调函数
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+local handled = eink_page.handle_key("pwr_up", switch_page)
+]] 
+function eink_page.handle_key(key_type, switch_page)
+    log.info("eink_page.handle_key", "key_type:", key_type)
+    
+    if key_type == "pwr_up" then
+        -- PWR键:返回首页
+        switch_page("home")
+        return true
+    end
+    -- BOOT键无功能
+    return false
+end
+
+--[[
+页面进入时重置状态;
+
+@api eink_page.on_enter()
+@summary 页面进入时重置状态
+@return nil
+
+@usage
+-- 在页面切换时调用
+eink_page.on_enter()
+]] 
+function eink_page.on_enter()
+    log.info("eink_page", "进入eink演示页面")
+end
+
+--[[
+页面离开时执行清理操作;
+
+@api eink_page.on_leave()
+@summary 页面离开时执行清理操作
+@return nil
+
+@usage
+-- 在页面切换时调用
+eink_page.on_leave()
+]] 
+function eink_page.on_leave()
+    log.info("eink_page", "离开eink演示页面")
+end
+
+return eink_page

+ 147 - 0
module/Air8000/demo/ui/eink/ui/home_page.lua

@@ -0,0 +1,147 @@
+--[[
+@module  home_page
+@summary eink主页模块,提供应用入口和导航功能
+@version 1.0
+@date    2025.12.18
+@author  江访  
+@usage
+本模块为主页模块,主要功能包括:
+1、提供应用入口和导航功能;
+2、显示系统标题和操作提示;
+3、管理两个功能选项的选中状态;
+4、处理主页面的按键事件;
+
+对外接口:
+1、home_page.draw():绘制主页面所有UI元素,包括选中指示
+2、home_page.handle_key():处理主页面按键事件
+3、home_page.on_enter():页面进入时重置选中状态
+]] 
+
+local home_page = {}
+
+-- 选项区域定义(eink屏幕200x200,需合理布局)
+local options = {
+    {name = "eink_demo", text = "eink演示", x1 = 20, y1 = 80, x2 = 90, y2 = 130, color = 0},
+    {name = "time_demo", text = "时间显示", x1 = 110, y1 = 80, x2 = 180, y2 = 130, color = 0}
+}
+
+-- 当前选中项索引
+local selected_index = 1
+
+
+--[[
+绘制主页界面;
+绘制主页面所有UI元素,包括选中指示;
+
+@api home_page.draw()
+@summary 绘制主页面所有UI元素,包括选中指示
+@return nil
+
+@usage
+-- 在UI主循环中调用
+home_page.draw()
+]] 
+function home_page.draw()
+    -- 清除绘图缓冲区
+    eink.clear(1, true)
+    
+    -- 显示标题
+    eink.setFont(eink.font_opposansm12_chinese)
+    eink.rect(10, 10, 190, 45, 0, 0)     -- 标题背景框
+    eink.print(60, 30, "eink演示系统", 0)
+
+    -- 显示操作提示
+    eink.print(30, 65, "BOOT:切换 PWR:确认", 0)
+
+    -- 绘制分隔线
+    eink.line(10, 70, 190, 70, 0)
+
+    -- 绘制所有选项框
+    for i, opt in ipairs(options) do
+        -- 绘制选项框
+        if i == selected_index then
+            -- 选中状态:实心矩形
+            eink.rect(opt.x1, opt.y1, opt.x2, opt.y2, 0, 1)
+            eink.print(opt.x1 + 15, opt.y1 + 25, opt.text, 1)  -- 白色文字
+        else
+            -- 未选中状态:空心矩形
+            eink.rect(opt.x1, opt.y1, opt.x2, opt.y2, 0, 0)
+            eink.print(opt.x1 + 15, opt.y1 + 25, opt.text, 0)  -- 黑色文字
+        end
+    end
+
+    -- 绘制底部信息
+    eink.print(50, 155, "eink核心库演示", 0)
+    eink.print(60, 175, "微雪1.54寸", 0)
+
+    -- 刷新屏幕
+    eink.show(0, 0, true)
+end
+
+--[[
+处理主页按键事件;
+根据按键类型执行相应的操作;
+
+@api home_page.handle_key(key_type, switch_page)
+@summary 处理主页按键事件
+@string key_type 按键类型
+@valid_values "confirm", "next", "prev", "back"
+@function switch_page 页面切换回调函数
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+local handled = home_page.handle_key("next", switch_page)
+]] 
+function home_page.handle_key(key_type, switch_page)
+    log.info("home_page.handle_key", "key_type:", key_type, "selected_index:", selected_index)
+    
+    if key_type == "confirm" or key_type == "pwr_up" then
+        -- 确认键:切换到选中的页面
+        local opt = options[selected_index]
+        switch_page(opt.name)
+        return true
+    elseif key_type == "boot_up" then
+        -- BOOT键:切换选项
+        selected_index = selected_index % #options + 1
+        return true
+    elseif key_type == "back" then
+        -- 返回键:当前主页不需要返回功能
+        return false
+    end
+    return false
+end
+
+--[[
+页面进入时重置选中状态;
+重置选中状态为第一个选项;
+
+@api home_page.on_enter()
+@summary 重置选中状态
+@return nil
+
+@usage
+-- 在页面切换时调用
+home_page.on_enter()
+]] 
+function home_page.on_enter()
+    selected_index = 1  -- 默认选中第一个
+    log.info("home_page", "进入主页")
+end
+
+--[[
+页面离开时执行清理操作;
+
+@api home_page.on_leave()
+@summary 页面离开时执行清理操作
+@return nil
+
+@usage
+-- 在页面切换时调用
+home_page.on_leave()
+]] 
+function home_page.on_leave()
+    log.info("home_page", "离开主页")
+end
+
+return home_page

+ 197 - 0
module/Air8000/demo/ui/eink/ui/time_page.lua

@@ -0,0 +1,197 @@
+--[[
+@module  time_page
+@summary 时间显示演示模块
+@version 1.0
+@date    2025.12.18
+@author  江访  
+@usage
+本模块为时间显示演示功能模块,主要功能包括:
+1、使用os.date接口获取当前时间;
+2、显示多种时间格式;
+3、支持实时时间更新;
+
+按键功能:
+- PWR键:返回主页
+- BOOT键:切换时间显示格式
+
+对外接口:
+1、time_page.draw():绘制时间显示页面
+2、time_page.handle_key():处理时间页面按键事件
+3、time_page.on_enter():页面进入时重置状态
+4、time_page.on_leave():页面离开时执行清理操作
+]]
+
+local time_page = {}
+
+-- 时间显示状态
+local time_state = {
+    format_index = 1,     -- 当前显示格式索引
+    last_update = 0,      -- 最后更新时间
+    update_interval = 5000, -- 更新间隔(5秒)
+    display_formats = {   -- 时间显示格式列表
+        {name = "完整格式", format = "%Y年%m月%d日 %H:%M:%S"},
+        {name = "简洁格式", format = "%Y-%m-%d %H:%M"},
+        {name = "时间格式", format = "%H:%M:%S"},
+        {name = "日期格式", format = "%Y/%m/%d"},
+        {name = "星期格式", format = "%A %H:%M"},
+        {name = "UTC时间", format = "!%Y-%m-%d %H:%M:%S"}
+    }
+}
+
+--[[
+获取当前时间字符串
+@local
+@return string 格式化后的时间字符串
+]]
+local function get_time_string()
+    local format_info = time_state.display_formats[time_state.format_index]
+    return os.date(format_info.format)
+end
+
+--[[
+绘制时间显示页面;
+绘制时间显示页面的所有UI元素;
+
+@api time_page.draw()
+@summary 绘制时间显示页面的所有UI元素
+@return nil
+
+@usage
+-- 在UI主循环中调用
+time_page.draw()
+]]
+function time_page.draw()
+    -- 清除绘图缓冲区
+    eink.clear(1, true)
+
+    -- 显示标题
+    eink.setFont(eink.font_opposansm12_chinese)
+    eink.rect(10, 10, 190, 45, 0, 0)     -- 标题背景框
+    eink.print(70, 30, "时间显示", 0)
+
+    -- 显示当前时间
+    local time_str = get_time_string()
+
+    -- 时间显示框
+    eink.rect(20, 60, 180, 110, 0, 0)
+    
+    -- 统一使用12号中文字体显示时间
+    eink.setFont(eink.font_opposansm12_chinese)
+    
+    -- 根据字符串长度微调显示位置
+    if #time_str > 20 then
+        -- 长字符串向左偏移
+        eink.print(25, 85, time_str, 0)
+    else
+        -- 短字符串居中显示
+        eink.print(30, 85, time_str, 0)
+    end
+
+    -- 显示格式信息
+    local format_info = time_state.display_formats[time_state.format_index]
+    eink.print(30, 130, "当前时间格式:", 0)
+
+    eink.print(130, 130, format_info.name, 0)
+            -- 显示格式索引
+    eink.print(80, 145, string.format("%d/%d",
+        time_state.format_index,
+        #time_state.display_formats), 0)
+
+    -- 绘制分隔线
+    eink.line(10, 150, 190, 150, 0)
+
+    -- 显示操作提示
+    eink.print(55, 175, "BOOT键:切换格式", 0)
+    eink.print(55, 190, "PWR键:返回主页", 0)
+
+
+    -- 刷新屏幕
+    eink.show(0, 0, true)
+
+    -- 更新最后更新时间
+    time_state.last_update = mcu.ticks()
+end
+
+--[[
+处理按键事件;
+根据按键类型执行相应的操作;
+
+@api time_page.handle_key(key_type, switch_page)
+@summary 处理时间页面按键事件
+@string key_type 按键类型
+@valid_values "boot_up", "pwr_up"
+@function switch_page 页面切换回调函数
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+local handled = time_page.handle_key("boot_up", switch_page)
+]]
+function time_page.handle_key(key_type, switch_page)
+    log.info("time_page.handle_key", "key_type:", key_type)
+
+    if key_type == "boot_up" then
+        -- BOOT键:切换时间显示格式
+        time_state.format_index = time_state.format_index % #time_state.display_formats + 1
+        log.info("time_page", "切换到格式:", time_state.format_index)
+        return true
+    elseif key_type == "pwr_up" then
+        -- PWR键:返回首页
+        switch_page("home")
+        return true
+    end
+    return false
+end
+
+--[[
+检查是否需要更新时间;
+基于时间间隔判断是否需要刷新显示;
+
+@api time_page.need_update()
+@summary 检查是否需要更新时间显示
+@return bool 需要更新返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+if time_page.need_update() then
+    time_page.draw()
+end
+]]
+function time_page.need_update()
+    local current_time = mcu.ticks()
+    return (current_time - time_state.last_update) >= time_state.update_interval
+end
+
+--[[
+页面进入时重置状态;
+
+@api time_page.on_enter()
+@summary 页面进入时重置状态
+@return nil
+
+@usage
+-- 在页面切换时调用
+time_page.on_enter()
+]]
+function time_page.on_enter()
+    time_state.format_index = 1
+    time_state.last_update = 0
+    log.info("time_page", "进入时间显示页面")
+end
+
+--[[
+页面离开时执行清理操作;
+
+@api time_page.on_leave()
+@summary 页面离开时执行清理操作
+@return nil
+
+@usage
+-- 在页面切换时调用
+time_page.on_leave()
+]]
+function time_page.on_leave()
+    log.info("time_page", "离开时间显示页面")
+end
+
+return time_page

+ 167 - 0
module/Air8000/demo/ui/eink/ui/ui_main.lua

@@ -0,0 +1,167 @@
+--[[
+@module  ui_main
+@summary eink用户界面主控模块,负责页面管理和事件分发
+@version 1.0
+@date    2025.12.18
+@author  江访  
+本模块为eink用户界面主控模块,主要功能包括:
+1、管理页面切换和事件分发;
+2、处理按键事件并调用对应页面的处理函数;
+3、协调各页面之间的状态转移;
+4、控制界面刷新频率;
+
+对外接口:
+1、ui_main():用户界面主任务,初始化显示和按键驱动,启动UI主循环
+]] 
+
+-- 加载子页面
+local home_page = require "home_page"
+local eink_page = require "eink_page"
+local time_page = require "time_page"
+
+-- 当前页面状态
+local current_page = "home"
+local last_page = ""
+
+--[[
+切换页面;
+从当前页面切换到目标页面;
+
+@api switch_page(new_page)
+@summary 执行页面切换操作
+@string new_page 目标页面名称
+@valid_values "home", "eink_demo", "time_demo"
+@return nil
+
+@usage
+-- 切换到主页
+switch_page("home")
+
+-- 切换到eink演示页面
+switch_page("eink_demo")
+
+-- 切换到时间显示页面
+switch_page("time_demo")
+]] 
+local function switch_page(new_page)
+    log.info("switch_page", "从", current_page, "切换到", new_page)
+    
+    -- 调用旧页面的退出函数
+    if current_page == "home" and home_page.on_leave then
+        home_page.on_leave()
+    elseif current_page == "eink_demo" and eink_page.on_leave then
+        eink_page.on_leave()
+    elseif current_page == "time_demo" and time_page.on_leave then
+        time_page.on_leave()
+    end
+
+    last_page = current_page
+    current_page = new_page
+
+    -- 调用新页面的进入函数
+    if new_page == "home" and home_page.on_enter then
+        home_page.on_enter()
+    elseif new_page == "eink_demo" and eink_page.on_enter then
+        eink_page.on_enter()
+    elseif new_page == "time_demo" and time_page.on_enter then
+        time_page.on_enter()
+    end
+
+    log.info("ui_main", "已切换到页面:", current_page)
+end
+
+--[[
+处理按键事件;
+根据按键类型和当前页面调用对应的处理函数;
+
+@api handle_key_event(key_event)
+@summary 处理按键事件并分发到对应页面
+@string key_event 按键事件类型
+@valid_values "boot_up", "pwr_up"
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在ui_main任务中调用
+local handled = handle_key_event("boot_up")
+]] 
+local function handle_key_event(key_event)
+    log.info("按键事件", "event:", key_event, "当前页面:", current_page)
+    
+    if key_event == "boot_up" then
+        -- BOOT键:在主页作为方向键,在时间页面切换格式
+        if current_page == "home" then
+            -- 主页:切换选项
+            return home_page.handle_key("boot_up", switch_page)
+        elseif current_page == "time_demo" then
+            -- 时间页面:切换时间格式
+            if time_page.handle_key then
+                return time_page.handle_key("boot_up", switch_page)
+            end
+        end
+        -- 其他页面BOOT键无功能
+        return false
+    elseif key_event == "pwr_up" then
+        -- PWR键:确认/返回功能
+        if current_page == "home" then
+            return home_page.handle_key("confirm", switch_page)
+        elseif current_page == "eink_demo" then
+            -- eink演示页面:返回首页
+            if eink_page.handle_key then
+                return eink_page.handle_key("pwr_up", switch_page)
+            end
+        elseif current_page == "time_demo" then
+            -- 时间页面:返回首页
+            if time_page.handle_key then
+                return time_page.handle_key("pwr_up", switch_page)
+            end
+        end
+    end
+    
+    return false
+end
+
+--[[
+用户界面主任务;
+初始化显示和按键驱动,启动UI主循环;
+
+@api ui_main()
+@summary 初始化显示和按键驱动,启动UI主循环
+@return nil
+
+@usage
+-- 在主程序中通过sys.taskInit调用
+sys.taskInit(ui_main)
+]] 
+local function ui_main()
+
+    -- 调用主页的进入函数
+    if home_page.on_enter then
+        home_page.on_enter()
+    end
+
+    while true do
+        -- 根据当前页面绘制内容
+        if current_page == "home" then
+            home_page.draw()
+        elseif current_page == "eink_demo" then
+            eink_page.draw()
+        elseif current_page == "time_demo" then
+            time_page.draw()
+        end
+
+        local result, key_event = sys.waitUntil("KEY_EVENT")
+        if result then
+            -- 处理按键事件
+            handle_key_event(key_event)
+        else
+            -- 超时:时间页面需要刷新显示
+            if current_page == "time_demo" then
+                if time_page.need_update then
+                    time_page.draw()
+                end
+            end
+        end
+    end
+end
+
+sys.taskInit(ui_main)

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

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

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

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

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

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

+ 213 - 0
module/Air8000/demo/ui/ht1621/readme.md

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

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

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

+ 59 - 0
module/Air8101/demo/ui/eink/eink_drv.lua

@@ -0,0 +1,59 @@
+--[[
+@module  eink_drv
+@summary eink墨水屏显示驱动模块,基于eink核心库
+@version 1.0
+@date    2025.12.18
+@author  江访
+@usage
+本模块为eink墨水屏显示驱动功能模块,主要功能包括:
+1、初始化微雪1.54寸墨水屏(eink.MODEL_1in54_V2);
+2、配置SPI通信参数和设备对象;
+
+本文件无对外接口,require "eink_drv"即可加载运行
+]]
+
+
+--[[
+初始化eink显示驱动;
+
+@api eink_drv.init()
+@summary 配置并初始化微雪1.54寸墨水屏
+@return boolean 初始化成功返回true,失败返回false
+
+@usage
+-- 初始化eink显示
+local result = eink_drv.init()
+if result then
+    log.info("eink初始化成功")
+else
+    log.error("eink初始化失败")
+end
+]]
+
+local function eink_drv_init()
+    -- 按接线引脚正确配置GPIO号
+    local spi_id = 1
+    local pin_busy = 28
+    local pin_reset = 7
+    local pin_dc = 6
+    local pin_cs = 12
+
+    -- 注意:eink初始化之前需要先初始化spi,使用spi对象方式初始化
+    spi_eink = spi.deviceSetup(spi_id, pin_cs, 0, 0, 8, 20 * 1000 * 1000, spi.MSB, 1, 1)
+
+    -- 初始化接到spi0的eink.MODEL_1in54_V2
+    eink.init(eink.MODEL_1in54_V2,
+        { port = "device", pin_dc = pin_dc, pin_busy = pin_busy, pin_rst = pin_reset },
+        spi_eink)
+
+    -- 开启异步刷新
+    eink.async(1)
+
+    -- 设置显示窗口和方向
+    eink.setWin(200, 200, 0)
+    eink.clear(1, true)
+
+    log.info("eink_drv.init")
+end
+
+eink_drv_init()

+ 61 - 0
module/Air8101/demo/ui/eink/key_drv.lua

@@ -0,0 +1,61 @@
+--[[
+@module  key_drv
+@summary 按键驱动模块
+@version 1.0
+@date    2025.12.25
+@author  江访
+@usage
+本文件为按键驱动功能模块,核心业务逻辑为:
+1、初始化切换键(GPIO8)和确认键(GPIO5);
+2、配置按键事件的中断处理函数;
+3、实现按键防抖功能,防止误触发;
+4、对外发布按键消息;
+
+本文件没有对外接口,直接在main.lua中require "key_drv"就可以加载运行;
+]]
+
+-- 按键定义
+local key_switch = 8 -- GPIO8
+local key_confirm = 5  -- GPIO5
+
+
+-- 按键事件处理函数
+local function handle_switch_key(val)
+
+    if val == 0 then
+        sys.publish("KEY_EVENT", "switch_down")
+    else
+        sys.publish("KEY_EVENT", "switch_up")
+    end
+end
+
+local function handle_confirm_key(val)
+
+    if val == 0 then
+        sys.publish("KEY_EVENT", "confirm_down")
+    else
+        sys.publish("KEY_EVENT", "confirm_up")
+    end
+end
+
+--[[
+初始化按键GPIO;
+配置切换键(GPIO8)和确认键(GPIO5)的GPIO中断;
+
+@api init()
+@summary 配置切换键(GPIO8)和确认键(GPIO5)的GPIO中断
+@return bool 初始化只会返回true
+@usage
+
+]]
+local function init()
+    gpio.setup(key_switch, handle_switch_key, gpio.PULLUP, gpio.FALLING)
+    gpio.debounce(key_switch, 100, 0) -- 防抖,防止频繁触发
+
+    gpio.setup(key_confirm, handle_confirm_key, gpio.PULLUP, gpio.FALLING)
+    gpio.debounce(key_confirm, 100, 0) -- 防抖,防止频繁触发
+
+    log.info("key_drv", "按键初始化完成,切换键:GPIO8, 确认键:GPIO5")
+end
+
+init()

+ 87 - 0
module/Air8101/demo/ui/eink/main.lua

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

+ 175 - 0
module/Air8101/demo/ui/eink/readme.md

@@ -0,0 +1,175 @@
+# eink墨水屏演示系统
+
+## 一、功能模块介绍
+
+## 一、功能模块介绍
+
+### 1.1 核心主程序模块
+
+1. **main.lua** - 主程序入口,负责系统初始化和任务调度
+2. **ui_main.lua** - 用户界面主控模块,管理页面切换和事件分发
+
+### 1.2 显示页面模块
+
+1. **home_page.lua** - 主页模块,提供应用入口和导航功能
+2. **eink_page.lua** - eink核心库演示模块
+3. **time_page.lua** - 时间显示演示模块
+
+### 1.3 驱动模块
+
+1. **eink_drv.lua** - eink显示驱动模块,基于eink核心库
+2. **key_drv.lua** - 按键驱动模块,管理切换键(GPIO8)和确认键(GPIO5)
+
+## 二、按键功能说明
+
+### 2.1 按键消息
+
+- **"KEY_EVENT"** - 按键事件消息,包含按键类型和状态
+  - 切换键(GPIO8)事件:`switch_down`(按下)、`switch_up`(释放)
+  - 确认键(GPIO5)事件:`confirm_down`(按下)、`confirm_up`(释放)
+
+### 2.2 按键功能定义
+
+- **主页页面**:
+  - 切换键(GPIO8):切换选项(eink演示 ↔ 时间显示)
+  - 确认键(GPIO5):确认进入选中的页面
+
+- **eink演示页面**:
+  - 确认键(GPIO5):返回主页
+  - 切换键(GPIO8):无功能
+
+- **时间显示页面**:
+  - 切换键(GPIO8):切换时间显示格式(共6种格式)
+  - 确认键(GPIO5):返回主页
+
+## 三、显示效果
+
+<table>
+<tr>
+<td>主页<br/></td><td>eink库图像页面<br/></td><td>更新时间页面<br/></td></tr>
+<tr>
+<td rowspan="2">
+<img src="https://docs.openluat.com/cdn/image/Air780EHM_homepage.jpg" width="80" /><br/></td><td>
+<img src="https://docs.openluat.com/cdn/image/Air780EHM_einkpage.jpg" width="80" /><br/></td><td>
+<img src="https://docs.openluat.com/cdn/image/Air780EHM_eink显示时间.jpg" width="80" /><br/></td></tr>
+</table>
+
+## 四、硬件接线配置
+
+### 4.1 物料清单
+
+- Air8101核心板 × 1
+- 微雪1.54寸墨水屏 × 1 [demo所使用的墨水屏购买链接](https://e.tb.cn/h.7VUl8PgFVWhwLJS?tk=e3FVfDz34Ki)
+- 母对母杜邦线 × 8
+- TYPE-C 数据线 × 1
+- Air8101核心板和微雪1.54寸墨水屏的硬件接线方式为
+
+  - Air8101 核心板通过 TYPE-C USB 口供电(核心板背面的功耗测试开关拨到 OFF 一端),此种供电方式下,vbat 引脚为 3.3V,可以直接给微雪1.54寸墨水屏供电;
+  - 为了演示方便,所以 Air8101 核心板上电后直接通过vbat引脚给微雪1.54寸墨水屏供电;
+  - 客户在设计实际项目时,一般来说,需要通过一个 GPIO 来控制 LDO 给配件板供电,这样可以灵活地控制配件板的供电,可以使项目的整体功耗降到最低;
+
+### 4.2 接线方式
+
+|Air8101核心板| 墨水屏引脚 |
+|------------|-----------|
+| 75/GPIO28  | BUSY      |
+| 10/GPIO7   | RST       |
+| 75/GPIO28  | DC        |
+| 68/GPIO12  | CS        |
+| 65/GPIO2   | SCK       |
+| 76/GPIO4   | DIN       |
+| vbat       | VCC       |
+| gnd        | GND       |
+
+#### 4.2.3 接线图
+
+![](https://docs.openLuat.com/cdn/image/Air8101_eink接线图.jpg)
+
+## 五、演示软件环境
+
+### 5.1 开发工具
+
+- [Luatools下载调试工具](https://docs.openluat.com/air8101/luatos/common/download/) - 固件烧录和代码调试
+
+### 5.2 内核固件
+
+- [点击下载Air8101系列最新版本内核固件](https://docs.openluat.com/air8101/luatos/firmware/),demo所使用的是LuatOS-SoC_V2002_Air8101 1号固件
+  
+
+## 六、演示操作步骤
+
+### 6.1 硬件准备
+
+1. 按照接线表连接eink屏幕
+2. 确保电源连接正确,通过TYPE-C USB口供电
+3. 确保所有连接正确无误
+
+### 6.2 软件配置
+
+在`main.lua`中选择加载对应的驱动模块:
+
+```lua
+-- 加载eink显示驱动管理功能模块
+require "eink_drv"
+
+-- 加载按键驱动管理功能模块
+require "key_drv"
+
+-- 加载eink核心库实现的用户界面功能模块
+-- 实现多页面切换、按键事件分发和界面渲染功能
+-- 包含主页、eink演示页和时间显示页
+require "ui_main"
+
+```
+
+### 6.3 软件烧录
+
+1. 使用Luatools烧录最新内核固件
+2. 下载并烧录本项目所有脚本文件
+3. 设备自动重启后开始运行
+
+### 6.4 功能测试
+
+#### 主页操作
+
+1. 设备启动后显示主页
+2. 使用切换键(GPIO8)在两个选项间切换
+3. 使用确认键(GPIO5)进入选中的页面
+
+#### eink演示页面
+
+1. 查看基本图形绘制示例
+2. 查看文本显示效果
+3. 查看二维码和电池图标
+4. 按确认键(GPIO5)返回主页
+
+#### 时间显示页面
+
+1. 查看当前时间显示
+2. 使用切换键(GPIO8)切换不同的时间格式
+3. 按确认键(GPIO5)返回主页
+
+### 6.5 预期效果
+
+- **主页**:清晰显示两个选项,选中状态明显
+- **eink演示页面**:图形绘制清晰,布局合理
+- **时间显示页面**:时间格式切换流畅
+- **按键交互**:响应及时准确,页面切换正常
+
+## 七、故障排除
+
+1. **屏幕不显示**:检查SPI接线,确认电源供电
+2. **按键无响应**:确认GPIO配置正确
+3. **时间显示异常**:检查系统时间设置
+4. **页面切换异常**:检查内存使用情况,适当调整刷新间隔
+
+## 八、注意事项
+
+1. eink屏幕刷新较慢,请勿频繁切换页面
+2. 按键操作后请等待屏幕刷新完成
+3. 长时间运行请确保电源稳定
+4. 避免在高温高湿环境下使用
+
+## 九、拓展使用
+
+本demo内所演示的图形显示接口均可在eink核心库内找到,更丰富和详细的使用说明可以点击进入[eink核心库](https://docs.openluat.com/osapi/core/eink/)

+ 198 - 0
module/Air8101/demo/ui/eink/ui/eink_page.lua

@@ -0,0 +1,198 @@
+--[[
+@module  time_page
+@summary 时间显示演示模块
+@version 1.0
+@date    2025.12.25
+@author  江访  
+@usage
+本模块为时间显示演示功能模块,主要功能包括:
+1、使用os.date接口获取当前时间;
+2、显示多种时间格式;
+3、支持实时时间更新;
+
+按键功能:
+- 确认键:返回主页
+- 切换键:切换时间显示格式
+
+对外接口:
+1、time_page.draw():绘制时间显示页面
+2、time_page.handle_key():处理时间页面按键事件
+3、time_page.on_enter():页面进入时重置状态
+4、time_page.on_leave():页面离开时执行清理操作
+]]
+
+local time_page = {}
+
+-- 时间显示状态
+local time_state = {
+    format_index = 1,     -- 当前显示格式索引
+    last_update = 0,      -- 最后更新时间
+    update_interval = 5000, -- 更新间隔(5秒)
+    display_formats = {   -- 时间显示格式列表
+        {name = "完整格式", format = "%Y年%m月%d日 %H:%M:%S"},
+        {name = "简洁格式", format = "%Y-%m-%d %H:%M"},
+        {name = "时间格式", format = "%H:%M:%S"},
+        {name = "日期格式", format = "%Y/%m/%d"},
+        {name = "星期格式", format = "%A %H:%M"},
+        {name = "UTC时间", format = "!%Y-%m-%d %H:%M:%S"}
+    }
+}
+
+--[[
+获取当前时间字符串
+@local
+@return string 格式化后的时间字符串
+]]
+local function get_time_string()
+    local format_info = time_state.display_formats[time_state.format_index]
+    return os.date(format_info.format)
+end
+
+--[[
+绘制时间显示页面;
+绘制时间显示页面的所有UI元素;
+
+@api time_page.draw()
+@summary 绘制时间显示页面的所有UI元素
+@return nil
+
+@usage
+-- 在UI主循环中调用
+time_page.draw()
+]]
+function time_page.draw()
+    -- 清除绘图缓冲区
+    eink.clear(1, true)
+
+    -- 显示标题
+    eink.setFont(eink.font_opposansm12_chinese)
+    eink.rect(10, 10, 190, 45, 0, 0)     -- 标题背景框
+    eink.print(70, 30, "时间显示", 0)
+
+    -- 显示当前时间
+    local time_str = get_time_string()
+
+    -- 时间显示框
+    eink.rect(20, 60, 180, 110, 0, 0)
+    
+    -- 统一使用12号中文字体显示时间
+    eink.setFont(eink.font_opposansm12_chinese)
+    
+    -- 根据字符串长度微调显示位置
+    if #time_str > 20 then
+        -- 长字符串向左偏移
+        eink.print(25, 85, time_str, 0)
+    else
+        -- 短字符串居中显示
+        eink.print(30, 85, time_str, 0)
+    end
+
+    -- 显示格式信息
+    local format_info = time_state.display_formats[time_state.format_index]
+    eink.print(30, 130, "当前时间格式:", 0)
+
+    eink.print(130, 130, format_info.name, 0)
+            -- 显示格式索引
+    eink.print(80, 145, string.format("%d/%d",
+        time_state.format_index,
+        #time_state.display_formats), 0)
+
+    -- 绘制分隔线
+    eink.line(10, 150, 190, 150, 0)
+
+    -- 显示操作提示
+    eink.print(55, 175, "切换键:切换格式", 0)
+    eink.print(55, 190, "确认键:返回主页", 0)
+
+
+    -- 刷新屏幕
+    eink.show(0, 0, true)
+
+    -- 更新最后更新时间
+    time_state.last_update = mcu.ticks()
+end
+
+--[[
+处理按键事件;
+根据按键类型执行相应的操作;
+
+@api time_page.handle_key(key_type, switch_page)
+@summary 处理时间页面按键事件
+@string key_type 按键类型
+@valid_values "switch_up", "confirm_up"
+@function switch_page 页面切换回调函数
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+local handled = time_page.handle_key("switch_up", switch_page)
+]]
+function time_page.handle_key(key_type, switch_page)
+    log.info("time_page.handle_key", "key_type:", key_type)
+
+    if key_type == "switch_up" then
+        -- 切换键:切换时间显示格式
+        time_state.format_index = time_state.format_index % #time_state.display_formats + 1
+        time_page.draw()  -- 立即重绘以显示新的时间格式
+        log.info("time_page", "切换到格式:", time_state.format_index)
+        return true
+    elseif key_type == "confirm_up" then
+        -- 确认键:返回首页
+        switch_page("home")
+        return true
+    end
+    return false
+end
+
+--[[
+检查是否需要更新时间;
+基于时间间隔判断是否需要刷新显示;
+
+@api time_page.need_update()
+@summary 检查是否需要更新时间显示
+@return bool 需要更新返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+if time_page.need_update() then
+    time_page.draw()
+end
+]]
+function time_page.need_update()
+    local current_time = mcu.ticks()
+    return (current_time - time_state.last_update) >= time_state.update_interval
+end
+
+--[[
+页面进入时重置状态;
+
+@api time_page.on_enter()
+@summary 页面进入时重置状态
+@return nil
+
+@usage
+-- 在页面切换时调用
+time_page.on_enter()
+]]
+function time_page.on_enter()
+    time_state.format_index = 1
+    time_state.last_update = 0
+    log.info("time_page", "进入时间显示页面")
+end
+
+--[[
+页面离开时执行清理操作;
+
+@api time_page.on_leave()
+@summary 页面离开时执行清理操作
+@return nil
+
+@usage
+-- 在页面切换时调用
+time_page.on_leave()
+]]
+function time_page.on_leave()
+    log.info("time_page", "离开时间显示页面")
+end
+
+return time_page

+ 145 - 0
module/Air8101/demo/ui/eink/ui/home_page.lua

@@ -0,0 +1,145 @@
+--[[
+@module  home_page
+@summary eink主页模块,提供应用入口和导航功能
+@version 1.0
+@date    2025.12.25
+@author  江访  
+@usage
+本模块为主页模块,主要功能包括:
+1、提供应用入口和导航功能;
+2、显示系统标题和操作提示;
+3、管理两个功能选项的选中状态;
+4、处理主页面的按键事件;
+
+对外接口:
+1、home_page.draw():绘制主页面所有UI元素,包括选中指示
+2、home_page.handle_key():处理主页面按键事件
+3、home_page.on_enter():页面进入时重置选中状态
+]] 
+
+local home_page = {}
+
+-- 选项区域定义(eink屏幕200x200,需合理布局)
+local options = {
+    {name = "eink_demo", text = "eink演示", x1 = 20, y1 = 80, x2 = 90, y2 = 130, color = 0},
+    {name = "time_demo", text = "时间显示", x1 = 110, y1 = 80, x2 = 180, y2 = 130, color = 0}
+}
+
+-- 当前选中项索引
+local selected_index = 1
+
+
+--[[
+绘制主页界面;
+绘制主页面所有UI元素,包括选中指示;
+
+@api home_page.draw()
+@summary 绘制主页面所有UI元素,包括选中指示
+@return nil
+
+@usage
+-- 在UI主循环中调用
+home_page.draw()
+]] 
+function home_page.draw()
+    -- 清除绘图缓冲区
+    eink.clear(1, true)
+    
+    -- 显示标题
+    eink.setFont(eink.font_opposansm12_chinese)
+    eink.rect(10, 10, 190, 45, 0, 0)     -- 标题背景框
+    eink.print(60, 30, "eink演示系统", 0)
+
+    -- 显示操作提示
+    eink.print(30, 65, "切换键:切换 确认键:确认", 0)
+
+    -- 绘制分隔线
+    eink.line(10, 70, 190, 70, 0)
+
+    -- 绘制所有选项框
+    for i, opt in ipairs(options) do
+        -- 绘制选项框
+        if i == selected_index then
+            -- 选中状态:实心矩形
+            eink.rect(opt.x1, opt.y1, opt.x2, opt.y2, 0, 1)
+            eink.print(opt.x1 + 15, opt.y1 + 25, opt.text, 1)  -- 白色文字
+        else
+            -- 未选中状态:空心矩形
+            eink.rect(opt.x1, opt.y1, opt.x2, opt.y2, 0, 0)
+            eink.print(opt.x1 + 15, opt.y1 + 25, opt.text, 0)  -- 黑色文字
+        end
+    end
+
+    -- 绘制底部信息
+    eink.print(50, 155, "eink核心库演示", 0)
+    eink.print(60, 175, "微雪1.54寸", 0)
+
+    -- 刷新屏幕
+    eink.show(0, 0, true)
+end
+
+--[[
+处理主页按键事件;
+根据按键类型执行相应的操作;
+
+@api home_page.handle_key(key_type, switch_page)
+@summary 处理主页按键事件
+@string key_type 按键类型
+@valid_values "confirm_up", "switch_up"
+@function switch_page 页面切换回调函数
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+local handled = home_page.handle_key("switch_up", switch_page)
+]] 
+function home_page.handle_key(key_type, switch_page)
+    log.info("home_page.handle_key", "key_type:", key_type, "selected_index:", selected_index)
+    
+    if key_type == "confirm_up" then
+        -- 确认键:切换到选中的页面
+        local opt = options[selected_index]
+        switch_page(opt.name)
+        return true
+    elseif key_type == "switch_up" then
+        -- 切换键:切换选项
+        selected_index = selected_index % #options + 1
+        home_page.draw()  -- 立即重绘以显示新的选中状态
+        return true
+    end
+    return false
+end
+
+--[[
+页面进入时重置选中状态;
+重置选中状态为第一个选项;
+
+@api home_page.on_enter()
+@summary 重置选中状态
+@return nil
+
+@usage
+-- 在页面切换时调用
+home_page.on_enter()
+]] 
+function home_page.on_enter()
+    selected_index = 1  -- 默认选中第一个
+    log.info("home_page", "进入主页")
+end
+
+--[[
+页面离开时执行清理操作;
+
+@api home_page.on_leave()
+@summary 页面离开时执行清理操作
+@return nil
+
+@usage
+-- 在页面切换时调用
+home_page.on_leave()
+]] 
+function home_page.on_leave()
+    log.info("home_page", "离开主页")
+end
+
+return home_page

+ 197 - 0
module/Air8101/demo/ui/eink/ui/time_page.lua

@@ -0,0 +1,197 @@
+--[[
+@module  time_page
+@summary 时间显示演示模块
+@version 1.0
+@date    2025.12.18
+@author  江访  
+@usage
+本模块为时间显示演示功能模块,主要功能包括:
+1、使用os.date接口获取当前时间;
+2、显示多种时间格式;
+3、支持实时时间更新;
+
+按键功能:
+- PWR键:返回主页
+- BOOT键:切换时间显示格式
+
+对外接口:
+1、time_page.draw():绘制时间显示页面
+2、time_page.handle_key():处理时间页面按键事件
+3、time_page.on_enter():页面进入时重置状态
+4、time_page.on_leave():页面离开时执行清理操作
+]]
+
+local time_page = {}
+
+-- 时间显示状态
+local time_state = {
+    format_index = 1,     -- 当前显示格式索引
+    last_update = 0,      -- 最后更新时间
+    update_interval = 5000, -- 更新间隔(5秒)
+    display_formats = {   -- 时间显示格式列表
+        {name = "完整格式", format = "%Y年%m月%d日 %H:%M:%S"},
+        {name = "简洁格式", format = "%Y-%m-%d %H:%M"},
+        {name = "时间格式", format = "%H:%M:%S"},
+        {name = "日期格式", format = "%Y/%m/%d"},
+        {name = "星期格式", format = "%A %H:%M"},
+        {name = "UTC时间", format = "!%Y-%m-%d %H:%M:%S"}
+    }
+}
+
+--[[
+获取当前时间字符串
+@local
+@return string 格式化后的时间字符串
+]]
+local function get_time_string()
+    local format_info = time_state.display_formats[time_state.format_index]
+    return os.date(format_info.format)
+end
+
+--[[
+绘制时间显示页面;
+绘制时间显示页面的所有UI元素;
+
+@api time_page.draw()
+@summary 绘制时间显示页面的所有UI元素
+@return nil
+
+@usage
+-- 在UI主循环中调用
+time_page.draw()
+]]
+function time_page.draw()
+    -- 清除绘图缓冲区
+    eink.clear(1, true)
+
+    -- 显示标题
+    eink.setFont(eink.font_opposansm12_chinese)
+    eink.rect(10, 10, 190, 45, 0, 0)     -- 标题背景框
+    eink.print(70, 30, "时间显示", 0)
+
+    -- 显示当前时间
+    local time_str = get_time_string()
+
+    -- 时间显示框
+    eink.rect(20, 60, 180, 110, 0, 0)
+    
+    -- 统一使用12号中文字体显示时间
+    eink.setFont(eink.font_opposansm12_chinese)
+    
+    -- 根据字符串长度微调显示位置
+    if #time_str > 20 then
+        -- 长字符串向左偏移
+        eink.print(25, 85, time_str, 0)
+    else
+        -- 短字符串居中显示
+        eink.print(30, 85, time_str, 0)
+    end
+
+    -- 显示格式信息
+    local format_info = time_state.display_formats[time_state.format_index]
+    eink.print(30, 130, "当前时间格式:", 0)
+
+    eink.print(130, 130, format_info.name, 0)
+            -- 显示格式索引
+    eink.print(80, 145, string.format("%d/%d",
+        time_state.format_index,
+        #time_state.display_formats), 0)
+
+    -- 绘制分隔线
+    eink.line(10, 150, 190, 150, 0)
+
+    -- 显示操作提示
+    eink.print(55, 175, "BOOT键:切换格式", 0)
+    eink.print(55, 190, "PWR键:返回主页", 0)
+
+
+    -- 刷新屏幕
+    eink.show(0, 0, true)
+
+    -- 更新最后更新时间
+    time_state.last_update = mcu.ticks()
+end
+
+--[[
+处理按键事件;
+根据按键类型执行相应的操作;
+
+@api time_page.handle_key(key_type, switch_page)
+@summary 处理时间页面按键事件
+@string key_type 按键类型
+@valid_values "boot_up", "pwr_up"
+@function switch_page 页面切换回调函数
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+local handled = time_page.handle_key("boot_up", switch_page)
+]]
+function time_page.handle_key(key_type, switch_page)
+    log.info("time_page.handle_key", "key_type:", key_type)
+
+    if key_type == "boot_up" then
+        -- BOOT键:切换时间显示格式
+        time_state.format_index = time_state.format_index % #time_state.display_formats + 1
+        log.info("time_page", "切换到格式:", time_state.format_index)
+        return true
+    elseif key_type == "pwr_up" then
+        -- PWR键:返回首页
+        switch_page("home")
+        return true
+    end
+    return false
+end
+
+--[[
+检查是否需要更新时间;
+基于时间间隔判断是否需要刷新显示;
+
+@api time_page.need_update()
+@summary 检查是否需要更新时间显示
+@return bool 需要更新返回true,否则返回false
+
+@usage
+-- 在UI主循环中调用
+if time_page.need_update() then
+    time_page.draw()
+end
+]]
+function time_page.need_update()
+    local current_time = mcu.ticks()
+    return (current_time - time_state.last_update) >= time_state.update_interval
+end
+
+--[[
+页面进入时重置状态;
+
+@api time_page.on_enter()
+@summary 页面进入时重置状态
+@return nil
+
+@usage
+-- 在页面切换时调用
+time_page.on_enter()
+]]
+function time_page.on_enter()
+    time_state.format_index = 1
+    time_state.last_update = 0
+    log.info("time_page", "进入时间显示页面")
+end
+
+--[[
+页面离开时执行清理操作;
+
+@api time_page.on_leave()
+@summary 页面离开时执行清理操作
+@return nil
+
+@usage
+-- 在页面切换时调用
+time_page.on_leave()
+]]
+function time_page.on_leave()
+    log.info("time_page", "离开时间显示页面")
+end
+
+return time_page

+ 168 - 0
module/Air8101/demo/ui/eink/ui/ui_main.lua

@@ -0,0 +1,168 @@
+--[[
+@module  ui_main
+@summary eink用户界面主控模块,负责页面管理和事件分发
+@version 1.0
+@date    2025.12.25
+@author  江访  
+本模块为eink用户界面主控模块,主要功能包括:
+1、管理页面切换和事件分发;
+2、处理按键事件并调用对应页面的处理函数;
+3、协调各页面之间的状态转移;
+4、控制界面刷新频率;
+
+对外接口:
+1、ui_main():用户界面主任务,初始化显示和按键驱动,启动UI主循环
+]] 
+
+-- 加载子页面
+local home_page = require "home_page"
+local eink_page = require "eink_page"
+local time_page = require "time_page"
+
+-- 当前页面状态
+local current_page = "home"
+local last_page = ""
+
+--[[
+切换页面;
+从当前页面切换到目标页面;
+
+@api switch_page(new_page)
+@summary 执行页面切换操作
+@string new_page 目标页面名称
+@valid_values "home", "eink_demo", "time_demo"
+@return nil
+
+@usage
+-- 切换到主页
+switch_page("home")
+
+-- 切换到eink演示页面
+switch_page("eink_demo")
+
+-- 切换到时间显示页面
+switch_page("time_demo")
+]] 
+local function switch_page(new_page)
+    log.info("switch_page", "从", current_page, "切换到", new_page)
+    
+    -- 调用旧页面的退出函数
+    if current_page == "home" and home_page.on_leave then
+        home_page.on_leave()
+    elseif current_page == "eink_demo" and eink_page.on_leave then
+        eink_page.on_leave()
+    elseif current_page == "time_demo" and time_page.on_leave then
+        time_page.on_leave()
+    end
+
+    last_page = current_page
+    current_page = new_page
+
+    -- 调用新页面的进入函数
+    if new_page == "home" and home_page.on_enter then
+        home_page.on_enter()
+    elseif new_page == "eink_demo" and eink_page.on_enter then
+        eink_page.on_enter()
+    elseif new_page == "time_demo" and time_page.on_enter then
+        time_page.on_enter()
+    end
+
+    log.info("ui_main", "已切换到页面:", current_page)
+end
+
+--[[
+处理按键事件;
+根据按键类型和当前页面调用对应的处理函数;
+
+@api handle_key_event(key_event)
+@summary 处理按键事件并分发到对应页面
+@string key_event 按键事件类型
+@valid_values "switch_up", "confirm_up", "switch_down", "confirm_down"
+@return bool 事件处理成功返回true,否则返回false
+
+@usage
+-- 在ui_main任务中调用
+local handled = handle_key_event("switch_up")
+]] 
+local function handle_key_event(key_event)
+    log.info("按键事件", "event:", key_event, "当前页面:", current_page)
+    
+    if key_event == "switch_up" then
+        -- 切换键释放:在主页切换选项,在时间页面切换格式
+        if current_page == "home" then
+            -- 主页:切换选项
+            return home_page.handle_key("switch_up", switch_page)
+        elseif current_page == "time_demo" then
+            -- 时间页面:切换时间格式
+            if time_page.handle_key then
+                return time_page.handle_key("switch_up", switch_page)
+            end
+        end
+        -- 其他页面切换键无功能
+        return false
+    elseif key_event == "confirm_up" then
+        -- 确认键释放:确认/返回功能
+        if current_page == "home" then
+            return home_page.handle_key("confirm_up", switch_page)
+        elseif current_page == "eink_demo" then
+            -- eink演示页面:返回首页
+            if eink_page.handle_key then
+                return eink_page.handle_key("confirm_up", switch_page)
+            end
+        elseif current_page == "time_demo" then
+            -- 时间页面:返回首页
+            if time_page.handle_key then
+                return time_page.handle_key("confirm_up", switch_page)
+            end
+        end
+    end
+    
+    -- 可以忽略按下事件,只处理释放事件
+    return false
+end
+
+--[[
+用户界面主任务;
+初始化显示和按键驱动,启动UI主循环;
+
+@api ui_main()
+@summary 初始化显示和按键驱动,启动UI主循环
+@return nil
+
+@usage
+-- 在主程序中通过sys.taskInit调用
+sys.taskInit(ui_main)
+]] 
+local function ui_main()
+
+    -- 调用主页的进入函数
+    if home_page.on_enter then
+        home_page.on_enter()
+    end
+
+    while true do
+        -- 根据当前页面绘制内容
+        if current_page == "home" then
+            home_page.draw()
+        elseif current_page == "eink_demo" then
+            eink_page.draw()
+        elseif current_page == "time_demo" then
+            time_page.draw()
+        end
+
+        local result, key_event = sys.waitUntil("KEY_EVENT")
+        if result then
+            -- 处理按键事件
+            handle_key_event(key_event)
+        else
+            -- 超时:时间页面需要刷新显示
+            if current_page == "time_demo" then
+                if time_page.need_update and time_page.need_update() then
+                    time_page.draw()
+                end
+            end
+        end
+    end
+end
+
+sys.taskInit(ui_main)

+ 102 - 28
script/libs/excloud.lua

@@ -98,6 +98,62 @@ local is_open = false              -- 服务是否开启
 local is_connected = false         -- 是否已连接
 local is_authenticated = false     -- 是否已鉴权
 local sequence_num = 1             -- 流水号
+
+-- 辅助函数:构建multipart/form-data请求体
+local function build_multipart_form_data(forms, files)
+    local boundary = "----WebKitFormBoundary" .. tostring(os.time())
+    local body = {}
+
+    -- 添加表单数据
+    if forms then
+        for k, v in pairs(forms) do
+            table.insert(body, "--" .. boundary .. "\r\n")
+            table.insert(body, string.format("Content-Disposition: form-data; name=\"%s\"\r\n\r\n", k))
+            table.insert(body, tostring(v) .. "\r\n")
+        end
+    end
+
+    -- 添加文件数据
+    if files then
+        for k, file_path in pairs(files) do
+            local fd = io.open(file_path, "rb")
+            if fd then
+                local file_content = fd:read("*a")
+                fd:close()
+
+                local file_name = file_path:match("[^/\\]+$" or "")
+                local content_type = "application/octet-stream"
+
+                -- 根据文件扩展名设置Content-Type
+                local ext = file_name:match("%.(%w+)$" or ""):lower()
+                local content_types = {
+                    txt = "text/plain",
+                    jpg = "image/jpeg",
+                    jpeg = "image/jpeg",
+                    png = "image/png",
+                    gif = "image/gif",
+                    mp3 = "audio/mpeg",
+                    wav = "audio/wav",
+                    json = "application/json",
+                    html = "text/html"
+                }
+                if content_types[ext] then
+                    content_type = content_types[ext]
+                end
+
+                table.insert(body, "--" .. boundary .. "\r\n")
+                table.insert(body, string.format("Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n", k, file_name))
+                table.insert(body, "Content-Type: " .. content_type .. "\r\n\r\n")
+                table.insert(body, file_content .. "\r\n")
+            end
+        end
+    end
+
+    -- 添加结束边界
+    table.insert(body, "--" .. boundary .. "--\r\n")
+
+    return table.concat(body), boundary
+end
 local connection = nil             -- 连接对象
 local device_id_binary = nil       -- 二进制格式的设备ID
 local reconnect_timer = nil        -- 重连定时器
@@ -109,6 +165,7 @@ local heartbeat_timer = nil        -- 心跳定时器
 local heartbeat_interval = 300     -- 心跳间隔(秒),默认5分钟
 local heartbeat_data = {}          -- 心跳数据,默认空表
 local is_heartbeat_running = false -- 心跳是否正在运行
+local is_mtn_log_uploading = false -- 运维日志是否正在上传
 
 -- 数据类型定义
 local DATA_TYPES = {
@@ -740,6 +797,9 @@ end
 local function upload_mtn_log_files()
 
     sys.taskInit(function()
+        -- 设置上传标志位为true
+        is_mtn_log_uploading = true
+
         local total_files = 4 -- 固定为4个日志文件
         local success_count = 0
         local failed_count = 0
@@ -807,6 +867,10 @@ local function upload_mtn_log_files()
                 -- 文件不存在或为空,跳过上传
                 log.info("运维日志文件不存在或为空,跳过上传", "文件:", file_name)
             end
+            -- -- 文件间延迟,避免同时上传多个文件
+            -- if i < 4 then
+            --     sys.wait(2000)
+            -- end
         end
 
         log.info("运维日志上传完成", "成功:", success_count, "失败:", failed_count, "总计:", processed_count)
@@ -825,11 +889,20 @@ local function upload_mtn_log_files()
                 total_files = processed_count
             })
         end
+
+        -- 上传完成,设置标志位为false
+        is_mtn_log_uploading = false
     end)
 end
 
 -- 处理运维日志上传请求
 local function handle_mtn_log_upload_request()
+    -- 检查是否正在上传,如果是则直接返回,抛弃新请求
+    if is_mtn_log_uploading then
+        log.info("[excloud]运维日志正在上传中,抛弃新的上传请求")
+        return
+    end
+
     local total_files = 4 -- 固定为4个日志文件
     local latest_index = 4 -- 最新序号固定为4
 
@@ -858,7 +931,7 @@ local function handle_mtn_log_upload_request()
     -- 开始上传日志文件
     sys.timerStart(function()
         upload_mtn_log_files()
-    end, 100)
+    end, 10)
 end
 
 
@@ -1224,42 +1297,43 @@ local function upload_file(file_type, file_path, file_name)
     -- 执行HTTP请求,添加重传机制
     local max_retries = 1
     local retry_count = 0
-    local code, response
+    local code, headers, body
     local upload_success = false
     local result_msg = ""
 
     while retry_count <= max_retries do
-        code, response = httpplus.request(
-            {
-                method = "POST",
-                url = upload_info.url,
-                forms = { ["key"] = upload_info.data_param.key },
-                files = { [upload_info.data_key or "f"] = file_path }
-            })
+        -- 构建multipart/form-data请求体
+        local forms = { ["key"] = upload_info.data_param.key }
+        local files = { [upload_info.data_key or "f"] = file_path }
+        local request_body, boundary = build_multipart_form_data(forms, files)
+
+        -- 构建请求头
+        local headers = {
+            ["Content-Type"] = "multipart/form-data; boundary=" .. boundary,
+            ["Content-Length"] = tostring(#request_body)
+        }
+
+        -- 发送HTTP请求
+        log.info("[excloud]开始发送HTTP请求", "URL:", upload_info.url)
+        code, headers, body = http.request("POST", upload_info.url, headers, request_body, {timeout=30000}).wait()
 
         -- 检查响应
-        if response then
-            log.info("[excloud]excloud.getip文件上传响应", "HTTP Code:", code, "Body:", response.body:query(), "Body:",
-                json.encode(response))
-
-            if code == 200 then
-                local resp_data, err = json.decode(response.body:query())
-                if resp_data and resp_data.code == 0 then
-                    upload_success = true
-                    result_msg = "上传成功"
-                    log.info("[excloud]文件上传成功", "URL:", resp_data.value and resp_data.value.uri or "未知")
-                    break
-                else
-                    result_msg = "服务器返回错误: " .. (resp_data and tostring(resp_data.code) or "未知")
-                    log.error("文件上传失败", result_msg, "响应:", response.body:query())
-                end
+        if code == 200 then
+            log.info("[excloud]excloud.getip文件上传响应", "HTTP Code:", code, "Body:", body and (#body > 512 and #body or body) or "nil")
+
+            local resp_data, err = json.decode(body)
+            if resp_data and resp_data.code == 0 then
+                upload_success = true
+                result_msg = "上传成功"
+                log.info("[excloud]文件上传成功", "URL:", resp_data.value and resp_data.value.uri or "未知")
+                break
             else
-                result_msg = "HTTP请求失败: " .. tostring(code)
-                log.error("文件上传HTTP请求失败", result_msg)
+                result_msg = "服务器返回错误: " .. (resp_data and tostring(resp_data.code) or "未知")
+                log.error("文件上传失败", result_msg, "响应:", body)
             end
         else
-            log.error("[excloud]HTTP请求返回空响应")
-            result_msg = "HTTP请求失败: 空响应"
+            result_msg = "HTTP请求失败: " .. tostring(code)
+            log.error("文件上传HTTP请求失败", result_msg, "Headers:", headers, "Body:", body)
         end
 
         -- 如果失败且未达到最大重试次数,则重试

+ 473 - 0
script/libs/exremotecam.lua

@@ -0,0 +1,473 @@
+--[[
+@module exremotecam
+@summary exremotecam 远程摄像头OSD控制扩展库,提供摄像头OSD文字显示设置和拍照功能。
+@version 1.0
+@date    2025.12.29
+@author  拓毅恒
+@usage
+注:在使用exremotecam 扩展库时,需要确保网络连接正常,能够访问到目标摄像头。
+
+本文件的对外接口有2个:
+1、exremotecam.OSDsetup(Brand, Host, channel, text, X, Y):设置摄像头OSD文字显示
+-- 参数说明:
+--   Brand: 摄像头品牌,当前仅支持"Dhua"(大华)
+--   Host: 摄像头/NVR的IP地址
+--   channel: 摄像头通道号
+--   text: OSD文本内容,需用竖线分隔,格式如"1111|2222|3333|4444"
+--   X: 显示位置的X坐标
+--   Y: 显示位置的Y坐标
+
+2、exremotecam.getphoto(Brand, Host, channel):控制摄像头拍照
+-- 参数说明:
+--   Brand: 摄像头品牌,当前仅支持"Dhua"(大华)
+--   Host: 摄像头/NVR的IP地址
+--   channel: 摄像头通道号
+-- 返回:若SD卡可用,则图片保存为/sd/1.jpeg
+]]
+
+--------------------------------各品牌摄像头HTTP参数配置--------------------------------
+-- 大华参数
+local DH_TextAlign = 0 -- 文本对齐方式,0左对齐,3右对齐 默认左对齐
+local DH_channel = 0 -- 通道号
+-- 大华OSD默认配置参数
+local dh_osd_param = {
+    Host = "192.168.1.108",
+    url = "/cgi-bin/configManager.cgi?",
+    GetWidgest = "action=getConfig&name=VideoWidget",
+    SetWidgest = "action=setConfig&VideoWidget[0].FontColorType=Adapt&VideoWidget[0].CustomTitle[1].PreviewBlend=true&VideoWidget[0].CustomTitle[1].EncodeBlend=true&VideoWidget[0].CustomTitle[1].TextAlign="..DH_TextAlign.."&VideoWidget[0].CustomTitle[1].Text=",
+    Text = "NULL",
+    Postion = "&VideoWidget[0].CustomTitle[1].Rect[0]=83&VideoWidget[0].CustomTitle[1].Rect[1]=169&VideoWidget[0].CustomTitle[1].Rect[2]=2666&VideoWidget[0].CustomTitle[1].Rect[3]=607"
+}
+-- 大华抓图默认配置参数
+local DAHUA_MD5Param = {
+    username = "admin",
+    password = "Air123456",
+    realm = "Login to 7720fd71f7dd8d36eaabc67104aa4f38",--值要获取
+    nonce = "dcd98b7102dd2f0e8b11d0f600bfb0c093",  -- 示例nonce值
+    method = "GET:",  -- HTTP方法
+    qop = "auth",
+    nc = "00000001",
+    cnonce = "KeA8e2Cy",
+    response = "NULL",
+    url = "/cgi-bin/snapshot.cgi?",
+    timerul = "/cgi-bin/global.cgi?"
+}
+--------------------------------各品牌摄像头HTTP参数配置完毕--------------------------------
+
+--[[
+按竖线(|)分割字符串,支持多种返回格式
+@api split_string_by_pipe(input_str,return_type)
+@string input_str 要分割的字符串,格式如"1111|2222|3333"
+@string/number return_type 返回类型,可选值:
+        "all" - 返回完整拆分数组(默认值)
+        "count" - 返回元素数量
+        整数 - 返回指定索引的元素(索引从1开始)
+@return 根据return_type参数不同,返回不同结果:
+    - "all": table - 包含所有分割元素的数组
+    - "count": number - 分割后的元素数量
+    - 整数索引: string - 指定索引的元素,索引越界时返回错误信息
+    - 无效参数: string - 错误提示信息
+@usage:
+    -- 示例1: 完整数组返回
+    -- 输入: "OSD行1|OSD行2|OSD行3"
+    -- 代码: local result = split_string_by_pipe("OSD行1|OSD行2|OSD行3")
+    -- 输出: {"OSD行1", "OSD行2", "OSD行3"}
+    
+    -- 示例2: 返回元素数量
+    -- 输入: "OSD行1|OSD行2|OSD行3"
+    -- 代码: local count = split_string_by_pipe("OSD行1|OSD行2|OSD行3", "count")
+    -- 输出: 3
+    
+    -- 示例3: 返回指定索引元素
+    -- 输入: "OSD行1|OSD行2|OSD行3"
+    -- 代码: local second_item = split_string_by_pipe("OSD行1|OSD行2|OSD行3", 2)
+    -- 输出: "OSD行2"
+    
+    -- 示例4: 在OSDsetup中的实际应用
+    -- 代码: OSDsetup("Dhua", "192.168.1.108", 0, "温度|湿度|天气|风向", 0, 2000)
+    -- 内部处理: split_string_by_pipe("温度|湿度|天气|风向") 得到 {"温度", "湿度", "天气", "风向"}
+    -- 最终效果: 在大华摄像头OSD上显示这四行文字
+]]
+local function split_string_by_pipe(input_str, return_type)
+    -- 处理默认参数(如果未指定 return_type,默认返回完整数组)
+    return_type = return_type or "all"
+    -- 存储拆分后的结果
+    local split_result = {}
+
+    -- 核心拆分逻辑:遍历字符串,按 | 分割
+    for item in string.gmatch(input_str, "[^|]+") do
+        table.insert(split_result, item)  -- 将匹配到的元素加入数组
+    end
+
+    -- 根据 return_type 处理返回结果
+    if return_type == "all" then
+        -- 返回完整拆分数组
+        return split_result
+    elseif return_type == "count" then
+        -- 返回元素数量(#split_result 是 Lua 获取数组长度的方式)
+        return #split_result
+    elseif type(return_type) == "number" then
+        -- 返回指定索引的元素(Lua 数组索引从 1 开始)
+        if return_type >= 1 and return_type <= #split_result then
+            return split_result[return_type]
+        else
+            -- 处理索引越界
+            return string.format("索引 %d 越界,当前只有 %d 个元素(索引 1 到 %d)", 
+                                return_type, #split_result, #split_result)
+        end
+    else
+        -- 处理无效的 return_type 参数
+        return "return_type 无效!可选值:'all'、'count' 或整数索引"
+    end
+end
+
+--[[
+解析并验证OSD显示元素,确保不超出最大显示行数
+@api ElementJudg(Data, number)
+@string Data 竖线分隔的OSD文本内容,格式如"1111|2222|3333"
+@number number 最大允许显示的行数
+@return table 分割后的所有OSD元素数组
+@usage
+local osd_elements = ElementJudg("行1|行2|行3|行4", 3)
+-- 输出: "超出显示的范围,只能显示3行"
+-- 返回: {"行1", "行2", "行3", "行4"}
+
+注意事项:
+1. 函数会打印所有解析到的元素及其索引
+2. 当元素数量超过最大行数时,会记录警告日志
+3. 无论是否超出限制,都会返回完整的元素数组
+]]
+local function ElementJudg(Data,number)
+    -- 使用split_string_by_pipe函数按竖线分割OSD数据
+    local all_items = split_string_by_pipe(Data)
+    
+    -- 遍历并打印所有解析到的OSD元素及其索引
+    for i, item in ipairs(all_items) do
+        log.info("元素解析", "索引", i, "值", item)
+    end
+    -- 获取OSD元素的总数
+    local NUM = split_string_by_pipe(Data,"count")
+    
+    -- 检查元素数量是否超过最大允许行数
+    if NUM > number then
+        -- 记录警告日志,提示超出显示范围
+        log.info("超出显示的范围,只能显示"..number.."行")
+    end
+    
+    -- 返回完整的OSD元素数组(无论是否超出限制)
+    return all_items
+end
+
+--[[
+URL编码函数,用于将字符串转换为符合URL标准的编码格式
+@api urlencode(str)
+@string str 需要进行URL编码的字符串
+@return string 编码后的URL安全字符串,如果输入为nil则返回空字符串
+@usage: 
+    local encoded = urlencode("Hello World!")
+    -- 输出: "Hello+World%21"
+]]
+local function urlencode(str)
+    -- 检查输入参数是否存在
+    if (str) then
+        -- 将换行符转换为CRLF格式,符合HTTP标准
+        str = string.gsub(str, "\n", "\r\n")
+        -- 对非字母数字和空格的字符进行%XX编码
+        str = string.gsub(str, "([^%w ])", function(c) return string.format("%%%02X", string.byte(c)) end)
+        -- 将空格转换为+号,符合URL编码规范
+        str = string.gsub(str, " ", "+")
+    end
+    -- 返回编码后的字符串或空字符串(如果输入为nil)
+    return str or ""
+end
+
+--[[
+计算Digest认证中的HA1值,用于网络摄像头的身份验证
+@api CameraHA1(username,realm,password)
+@string username 用户名
+@string realm 认证域,由服务器在401响应中提供
+@string password 用户密码
+@return string 计算得到的HA1值(小写的MD5哈希值)
+@usage: 
+    local ha1 = CameraHA1("admin", "realm", "123456")
+    -- 输出: md5("admin:realm:123456")的小写哈希值
+]]
+local function CameraHA1(username,realm,password)
+    -- 计算HA1值:MD5(用户名:认证域:密码),并转换为小写
+    -- Digest认证标准要求使用小写的哈希值
+    local ha1 = string.lower(crypto.md5(username..":"..realm..":"..password))
+    -- 返回计算得到的HA1值
+    return ha1
+end
+
+--[[
+处理Digest认证,仅在收到401响应时调用
+@api handle_digest_auth(Host,url,params,headers,HA2)
+@string Host 摄像头的IP地址
+@string url 请求的URL路径
+@string params 请求参数
+@table headers 第一次HTTP请求返回的头部信息
+@string HA2 预先计算好的HA2值
+@return boolean, table 认证是否成功, 更新后的请求头部
+@usage: 
+    local code, headers, body = http.request("GET", "http://192.168.1.100/cgi-bin/test", initial_headers).wait()
+    if code == 401 then
+        local success, updated_headers = handle_digest_auth("192.168.1.100", "/cgi-bin/test", "param=value", headers, "ha2_value")
+        if success then
+            -- 使用更新后的头部发送第二次请求
+        end
+    end
+]]
+local function handle_digest_auth(Host, url, params, headers, HA2)
+    -- 将headers转换为JSON格式以便解析
+    local str = json.encode(headers)
+    local Authenticate = json.decode(str)
+    -- 获取WWW-Authenticate头信息
+    local www = Authenticate["WWW-Authenticate"]
+    
+    if not www then
+        log.info("DigestAuth", "没有找到WWW-Authenticate头信息")
+        return false, nil
+    end
+    
+    log.info("DigestAuth", "获取的鉴权信息:", www)
+    
+    -- 从鉴权信息中提取所需参数
+    DAHUA_MD5Param.realm = string.match(www,"realm=\"(.-)\"")  -- 提取认证域
+    DAHUA_MD5Param.nonce = string.match(www,"nonce=\"(.-)\"")  -- 提取随机数
+    
+    if not DAHUA_MD5Param.realm or not DAHUA_MD5Param.nonce then
+        log.info("DigestAuth", "无法提取realm或nonce参数")
+        return false, nil
+    end
+    
+    -- 计算HA1值(用户名、认证域、密码的MD5哈希)
+    local HA1 = CameraHA1(DAHUA_MD5Param.username, DAHUA_MD5Param.realm, DAHUA_MD5Param.password)
+    
+    -- 计算完整的response值(Digest认证的核心)
+    -- response = MD5(HA1:nonce:nc:cnonce:qop:HA2)
+    DAHUA_MD5Param.response = string.lower(crypto.md5(HA1..":"..DAHUA_MD5Param.nonce..":"..DAHUA_MD5Param.nc..":"..DAHUA_MD5Param.cnonce..":"..DAHUA_MD5Param.qop..":"..HA2))
+    
+    -- 构建完整的Authorization头部
+    local authorization_header = "Digest username=\"" .. DAHUA_MD5Param.username .. "\", realm=\"" .. DAHUA_MD5Param.realm .. "\", nonce=\"" .. DAHUA_MD5Param.nonce .. "\", uri=\"" .. url..params.. "\", qop=" .. DAHUA_MD5Param.qop .. ", nc=" .. DAHUA_MD5Param.nc .. ", cnonce=\"" .. DAHUA_MD5Param.cnonce .. "\", response=\"" .. DAHUA_MD5Param.response.."\""
+    
+    -- 更新请求头部,添加认证信息
+    local updated_headers = {['Host']=''..Host, ["Authorization"] = ''..authorization_header, ['Connection']='keep-alive'}
+    log.info("DigestAuth", "鉴权信息重组完成")
+    
+    return true, updated_headers
+end
+
+--[[
+设置大华(Dahua)摄像头的OSD(屏幕显示)模块
+@api DH_set_osd_module(Host,Data,TextAlign,channel,x,y)
+@string Host 摄像头的IP地址
+@string Data 要显示的OSD文本内容
+@number TextAlign OSD文本对齐方式,默认为全局的DH_TextAlign
+@number channel 摄像头通道号,默认为全局的DH_channel
+@number x OSD显示的X坐标,默认为0
+@number y OSD显示的Y坐标,默认为0
+@return nil 无返回值,函数通过日志输出执行结果
+@usage: 
+    DH_set_osd_module("192.168.1.100", "温度: 25℃", 0, 1, 100, 200)
+    -- 功能: 在IP为192.168.1.100的摄像头通道1上,坐标(100,200)处显示"温度: 25℃"
+]]
+local function DH_set_osd_module(Host,Data,TextAlign,channel,x,y)
+    -- 设置默认参数值
+    DH_TextAlign = TextAlign or DH_TextAlign  -- 对齐方式 如果没填用默认值左对齐
+    channel = channel or DH_channel           -- 通道号 如果没填用默认值0
+    x = x or 0  -- x坐标 如果没填用默认值为0
+    y = y or 0  -- y坐标 如果没填用默认值为0
+    
+    -- 构建OSD位置参数字符串
+    dh_osd_param.Postion = "&VideoWidget["..channel.."].CustomTitle[1].Rect[0]="..x.."&VideoWidget["..channel.."].CustomTitle[1].Rect[1]="..y.."&VideoWidget["..channel.."].CustomTitle[1].Rect[2]=0".."&VideoWidget["..channel.."].CustomTitle[1].Rect[3]=0"
+    -- 构建OSD设置参数字符串
+    dh_osd_param.SetWidgest = "action=setConfig&VideoWidget["..channel.."].FontColorType=Adapt&VideoWidget["..channel.."].CustomTitle[1].PreviewBlend=true&VideoWidget["..channel.."].CustomTitle[1].EncodeBlend=true&VideoWidget["..channel.."].CustomTitle[1].TextAlign="..DH_TextAlign.."&VideoWidget["..channel.."].CustomTitle[1].Text="
+    
+    -- 对OSD文本内容进行URL编码,确保特殊字符正确传输
+    local OsdData = urlencode(Data)
+    -- 拼接完整的OSD设置参数
+    local OSDTEXT = dh_osd_param.SetWidgest ..OsdData
+    ---log.info("打印放置位置",dh_osd_param.Postion)
+    
+    -- 计算HA2值,用于Digest认证
+    -- HA2 = MD5(方法:URL路径:请求参数)
+    local HA2 = string.lower(crypto.md5(DAHUA_MD5Param.method..dh_osd_param.url..OSDTEXT..dh_osd_param.Postion))
+    -- 构建HTTP请求头部
+    local Camera_header = {["Accept-Encoding"]="identity",["Host"]=""..Host}
+    
+    -- 发送第一次HTTP请求,获取鉴权信息
+    local full_params = OSDTEXT..dh_osd_param.Postion
+    local full_url = "http://"..Host..dh_osd_param.url..full_params
+    local code, headers, body = http.request("GET", full_url, Camera_header).wait()
+    log.info("DHosd", "第一次请求http,code:", code, headers)  -- 打印返回的状态码和头部信息
+    
+    -- 处理HTTP请求返回结果
+    if code == 401 then -- 401表示需要身份认证
+        -- 使用Digest认证函数处理认证
+        local success, updated_headers = handle_digest_auth(Host, dh_osd_param.url, full_params, headers, HA2)
+        if success then
+            -- 发送第二次HTTP请求,这次带有完整的认证信息
+            local code, headers, body = http.request("GET", full_url, updated_headers).wait()
+            log.info("DHosd", "第二次请求http,code:", code)
+        else
+            log.info("DHosd", "Digest认证失败")
+            return
+        end
+    elseif code == -4 then
+        -- 处理重组错误(参数错误)
+        log.info("DHosd", "重组错误,请检查参数是否正确")
+        return  -- 退出函数,节省资源
+    else
+        -- 处理其他HTTP错误
+        log.info("DHosd", "HTTP请求错误,code:", code)
+        return  -- 退出函数,节省资源
+    end
+end
+
+--[[
+设置摄像头OSD(屏幕显示)文字功能
+@api OSDsetup(Brand,Host,channel,text,X,Y)
+@string Brand 摄像头品牌,当前仅支持: "Dhua" - 大华
+@string Host 摄像头/NVR的IP地址
+@number channel 摄像头通道号(主要用于NVR)
+@string text OSD文本内容,需用竖线分隔,格式如"1111|2222|3333|4444",大华最多显示13行
+@number X 显示位置的X坐标
+@number Y 显示位置的Y坐标
+@return 无 无返回值
+@usage
+-- 大华摄像头OSD测试
+OSDsetup("Dhua", "192.168.0.163", 0, "行1|行2|行3", 0, 2000)
+
+-- 多通道NVR示例
+OSDsetup("Dhua", "192.168.0.200", 1, "温度: 25℃|湿度: 60%", 100, 50)
+]]
+local function OSDsetup(Brand,Host,channel,text,X,Y)
+    -- 判断摄像头品牌
+    if Brand == "Dhua" then
+        log.info("osdsetup","检测到大华摄像头,开始初始化")
+        -- 解析并验证OSD文本内容,大华摄像头最多支持13行
+        ElementJudg(text,13)
+        -- 调用大华摄像头OSD设置函数
+        -- 参数:IP地址、OSD文本数组、对齐方式、通道号、X坐标、Y坐标
+        DH_set_osd_module(Host,text,0,channel,X,Y)
+        
+        -- 以下品牌型号暂不支持,代码已注释
+        -- elseif Brand == "Hikvision" then
+        --     log.info("osdsetup","检测到海康摄像头,开始初始化")
+        --     local all_items = ElementJudg(Text,4)
+        --     HKOSDBdoyGetFun(Host,channel,all_items[1],all_items[2],all_items[3],all_items[4],X,Y)
+        -- elseif Brand == "Uniview" then
+        --     log.info("osdsetup","检测到宇视摄像头,开始初始化")
+        --     local all_items = ElementJudg(Text,6)
+        --     EZ_OSDSETFun(Host,channel,all_items[1],all_items[2],all_items[3],all_items[4],all_items[5],all_items[6],X,Y)
+        -- elseif Brand == "TianDiWeiye" then
+        --     log.info("osdsetup","检测到天地伟业摄像头,开始初始化")
+        --     local all_items = ElementJudg(Text,6)
+        --     -- TDOSDModify(Host,t)
+    else
+        -- 处理不支持的品牌
+        log.info("osdsetup","型号填写错误或暂不支持!!!")
+    end
+end
+
+--[[
+大华摄像头拍照功能,获取指定通道的快照图片
+@api DHPicture(Host,channel)
+@string Host 摄像头/NVR的IP地址
+@number channel 摄像头通道号
+@return 无 无返回值,若SD卡可用则图片保存为/sd/1.jpeg
+@usage
+-- 获取大华摄像头通道0的快照图片
+DHPicture("192.168.1.108", 0)
+
+-- 获取大华NVR通道1的快照图片
+DHPicture("192.168.0.200", 1)
+]]
+local function DHPicture(Host,channel)
+    log.info("DHPicture","开始执行")
+    
+    -- 构建拍照请求参数:通道号和图片类型(0表示快照)
+    local resultStr = "channel="..channel.."&type=0"
+    
+    -- 计算HA2值:对HTTP方法、URL路径和请求参数进行MD5加密
+    local HA2 = string.lower(crypto.md5(DAHUA_MD5Param.method..DAHUA_MD5Param.url..resultStr))
+    
+    -- 准备基础HTTP请求头部
+    local Camera_header = {["Accept-Encoding"]="identity",["Host"]=""..Host}
+    
+    -- 发送第一次HTTP请求,主要目的是获取Digest认证信息
+    local full_url = "http://"..Host..DAHUA_MD5Param.url..resultStr
+    local code, headers, body = http.request("GET", full_url, Camera_header).wait()
+    log.info("DHPicture","第一次请求http,code:",code,headers)
+    
+    -- 获取到鉴权信息
+    if  code ==401 then
+        -- 使用统一的Digest认证函数处理认证
+        local success, updated_headers = handle_digest_auth(Host, DAHUA_MD5Param.url, resultStr, headers, HA2)
+        if success then
+            Camera_header = updated_headers
+            log.info("DHPicture","鉴权信息重组完成")
+        else
+            log.info("DHPicture", "Digest认证失败")
+            return
+        end
+    end
+    
+    -- 检查SD卡状态
+    local can_save_to_sd = false
+    local data, err = fatfs.getfree("/sd")
+    if data then
+        can_save_to_sd = true
+        log.info("DHPicture", "SD卡可用空间信息:", json.encode(data))
+    else
+        log.info("DHPicture", "无法获取SD卡空间信息:", err)
+    end
+    
+    -- 根据SD卡状态发送请求
+    local code, headers, body
+    if can_save_to_sd then
+        -- 发送第二次请求(带有完整的认证信息),获取图片并保存到/sd/1.jpeg
+        code, headers, body = http.request("GET", full_url, Camera_header, nil, {dst = "/sd/1.jpeg"}).wait()
+    else
+        -- 发送第二次请求(带有完整的认证信息),不保存图片
+        code, headers, body = http.request("GET", full_url, Camera_header).wait()
+        log.info("DHPicture", "没有检测到SD卡,无法保存图片到SD卡中,请确认SD卡状态后重试")
+    end
+    
+    log.info("DHPicture","第二次请求http,code:", code, body)
+    if code == 200 then
+        log.info("DHPicture","拍照完成")
+    end
+end
+
+--[[
+多品牌摄像头拍照通用接口,根据品牌调用对应厂商的拍照功能
+@api getphoto(Brand,Host,channel)
+@string Brand 摄像头品牌,当前仅支持: "Dhua" - 大华
+@string Host 摄像头/NVR的IP地址
+@number channel 摄像头通道号
+@return 无 无返回值,若SD卡可用则图片保存为/sd/1.jpeg
+@usage
+-- 获取大华摄像头通道0的快照图片
+getphoto("Dhua", "192.168.1.108", 1)
+
+-- 获取大华NVR通道1的快照图片
+getphoto("Dhua", "192.168.0.200", 1)
+]]
+local function getphoto(Brand,Host,channel)
+    -- 判断摄像头品牌
+    if Brand == "Dhua" then
+        log.info("getphoto","检测到大华摄像头,开始初始化")
+        DHPicture(Host,channel)
+    else
+        -- 处理不支持的品牌
+        log.info("getphoto","型号填写错误或暂不支持!!!")
+        return
+    end
+end
+
+return {
+    OSDsetup = OSDsetup,
+    getphoto = getphoto
+}