Procházet zdrojové kódy

update:新增Air8000系列eink 墨水屏demo

江访 před 1 měsícem
rodič
revize
f58aa4ee01

+ 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)