Compare commits

...

40 Commits

Author SHA1 Message Date
908e7a0a4d feat: 添加Atguigu键盘板支持和IP5306电源管理驱动
添加了两个新的键盘板支持:
- atguigu_keyboard_dongle (基于nrf52833)
- atguigu_mini_keyboard (基于nrf52840)

同时添加了完整的IP5306电源管理芯片驱动,包括:
- 支持充电状态检测
- 提供软硬件保活脉冲功能
- 硬件后端利用nRF的RTC+GPIOTE+GPPI实现低功耗保活
- 软件后端作为备用方案
2026-04-10 08:18:46 +08:00
2356cb4fe8 refactor(display): 重构显示模块UI逻辑分离到独立组件
- 将UI相关的代码从display_module.c中提取到新的display_ui.c文件
- 创建display_ui.h头文件定义UI模型和接口函数
- 在CMakeLists.txt中添加UI目录包含路径和源文件引用
- 修改display_module.c中的UI相关数据结构和函数调用
- 将UI创建和刷新逻辑替换为对新UI模块的调用
- 优化了时间日期文本的更新机制,提高性能
- 移除了原有的内部UI实现代码,保持模块职责清晰
2026-04-02 17:05:16 +08:00
b5433f0403 feat(power): 更新电源管理模块从IP5305到IP5306
- 在CMakeLists.txt中更新ZEPHYR_EXTRA_MODULES路径从ip5305到ip5306
- 在文档中更新未纳入索引的项目项,将IP5305相关引用替换为IP5306
- 在prj.conf中将CONFIG_IP5305配置更改为CONFIG_IP5306
- 在battery_module.c中更新头文件包含、设备获取和函数调用从ip5305到ip5306
- 更新日志输出中的设备名称以匹配新的IP5306芯片
2026-04-01 17:08:04 +08:00
881b36274c feat(qdec_module): 添加电源管理功能支持
- 引入pm/device.h和sys/atomic.h头文件以支持电源管理和原子操作
- 添加active原子变量来跟踪模块激活状态
- 实现qdec_module_suspend()和qdec_module_resume()函数处理挂起和唤醒
- 集成power_event事件订阅,在电源下电时挂起模块,在唤醒时恢复模块
- 修改工作调度逻辑,确保仅在活动状态下执行旋转编码器数据处理
- 重构初始化逻辑,使用qdec_reset_state()函数重置内部状态
2026-04-01 16:18:52 +08:00
e369567998 docs: 添加蓝牙SIG官方规格中BLE Profile汇总文档
新增bluetooth_sig_ble_profiles_summary.md文档,包含:
- 基于GATT的传统BLE Profile
- 基于LE Audio的音频类Profile
- 基于Bluetooth Mesh/NLC的Profile
- 共59个Profile的详细分类说明
- 各Profile的官方条目链接和应用场景描述
- 为new_kbd项目提供键盘相关Profile参考
2026-04-01 15:06:54 +08:00
c0a6e45911 feat(bt): 添加蓝牙设备信息服务的即插即用配置
为蓝牙设备信息服务添加VID和PID配置,支持即插即用功能,
包括供应商ID(0x1209)、产品ID(0x0001)和VID源配置。
2026-04-01 11:52:10 +08:00
5b4353d94f feat(ble): 使用Zephyr BAS服务替换自定义电池服务实现
- 在prj.conf中启用CONFIG_BT_BAS、CONFIG_BT_DIS和CONFIG_BT_DIS_PNP配置项
- 移除自定义的电池服务实现代码
- 改用zephyr/bluetooth/services/bas.h提供的标准BAS服务API
- 简化电池状态事件处理逻辑,直接调用bt_bas_set_battery_level设置电池级别
- 移除手动GATT通知实现,依赖系统BAS服务自动处理通知功能
2026-04-01 10:48:05 +08:00
302df0230d feat(app): 使用传感器驱动重构电池和模式切换模块
- 将电池模块从ADC直接采样改为使用sensor子系统,通过battery_sense传感器获取电压
- 将模式切换模块从ADC采样改为使用mode_sense传感器获取模式电压
- 移除GPIO控制的电池使能脚,改用传感器的PM管理机制
- 更新DTS配置,移除大量设备状态设置,添加boot-mode保留内存支持

perf(pm): 调整分区大小以支持单应用引导模式

- 将mcuboot分区从0xc000扩大到0x100,为单应用模式提供更大空间
- 相应调整app分区地址布局,确保内存分配合理
- 移除secondary镜像相关配置,优化flash使用

refactor(boot): 添加MCUBOOT单应用模式配置

- 在sysbuild中启用单应用模式支持
- 为引导加载程序添加保留内存和启动模式配置
- 配置CDC ACM串口用于引导模式通信
2026-03-31 15:09:23 +08:00
82be5cae52 docs: 添加 Nordic NCS 和 Zephyr 官方知识索引文档
添加了两个新的文档文件:
- nordic_ncs_官方知识索引.md:收录 Nordic NCS 官方文档链接,
包括 Zephyr 镜像文档、CAF 框架、Partition Manager 等相关内容
- zephyr_官方知识索引.md:收录 Zephyr 官方文档链接,
涵盖构建配置、设备树、内核并发、蓝牙、USB、外设驱动等模块

这两个索引文档基于项目实际使用的 nrf52840 芯片和 NCS 3.2.3 版本,
通过浏览器渲染验证确保链接有效性,并按功能模块进行分类整理。
2026-03-31 08:36:40 +08:00
2c7eae4de1 feat: 添加HID主机命令和主题颜色功能
- 添加了新的事件类型包括display_theme_event、hid_host_ack_event、
  hid_host_command_error_event和hid_host_command_event用于处理HID主机命令

- 在CMakeLists.txt中添加了新的源文件,包括显示主题事件和HID主机命令相关模块

- 实现了HID主机命令协议定义,包括主题颜色和时间同步命令

- 在BLE HID模块中添加了对供应商命令报告的支持,增加新的报告ID用于主机命令传输

- 扩展了HID传输路由机制,支持USB和BLE双通道传输

- 实现了显示模块的主题颜色存储功能,支持通过settings持久化保存主题颜色

- 添加了完整的BLE时间同步服务PC主机接入文档

- 修改了电池采样逻辑,增加2秒延迟以等待电池电压稳定
2026-03-30 15:57:38 +08:00
277462a8fe feat(ble): 添加快速广告配置并优化连接状态检查
添加了BLE快速广告相关的配置选项到prj.conf中,包括快速广告间隔、超时等参数。
同时修复了ble_bond_module中的连接状态检查逻辑,避免在挂起后保留LE连接时进行不必要的
断开操作。

在ble_hid_module和usb_hid_module中改进了HID传输事件处理逻辑,确保在相应模式未激活
或连接未建立时正确提交传输完成事件,提高了设备响应的准确性。

BREAKING CHANGE: 广告行为在连接保持情况下有所改变,可能影响配对流程。
2026-03-28 13:59:59 +08:00
64fec3a19e Merge branch 'display' 2026-03-28 09:13:12 +08:00
0b874a5c86 feat(display): 添加完整的LVGL显示界面和电源管理功能
- 集成自定义字体ui_font_keyboard_small_18和ui_font_keyboard_time_48
- 配置LVGL编译选项,启用flex布局和压缩字体支持
- 实现完整的显示UI界面,包括日期时间、电池状态、连接状态等组件
- 添加显示模块的电源管理功能,支持自动休眠和唤醒
- 实现与电池状态、键盘LED、模式切换等事件的交互响应
- 添加1分钟空闲超时自动熄屏功能
- 使用自定义精简字体替换默认蒙特塞拉特字体
2026-03-28 09:12:33 +08:00
3d57e6416a feat: 添加时间同步管理功能
- 新增time_manager模块用于统一管理时间同步状态
- 实现BLE时间同步GATT服务(time_sync_event和ble_time_sync_module)
- 添加time_sync_protocol定义统一的协议帧格式
- 支持UTC时间戳、时区偏移和精度信息的时间同步
- 实现settings持久化存储时间校准数据
- 提供time_manager快照API供其他模块查询当前时间状态
- 增加对BLE/USB/手动三种同步源的支持和区分
2026-03-27 11:25:22 +08:00
988fe11914 Merge branch 'display' 2026-03-27 09:40:19 +08:00
b424c04a01 feat(bt): 添加蓝牙外设首选超时配置
新增 CONFIG_BT_PERIPHERAL_PREF_TIMEOUT 配置项,
设置值为 400,用于优化蓝牙外设连接超时参数。
2026-03-27 09:36:13 +08:00
195f0a9e2b refactor(usb_hid_module): 统一代码风格和结构体定义格式
- 将所有结构体定义改为大括号独立成行的格式
- 调整函数参数对齐以提高可读性
- 统一 if 语句的大括号风格,确保一致性
- 优化代码缩进和空格布局
2026-03-23 18:25:51 +08:00
d02e33d97b feat(display): 添加LVGL显示支持和PWM背光控制
添加了完整的LVGL集成支持,包括:

- 在app.overlay中配置显示设备树,添加背光别名和SPI3总线支持
- 集成PWM背光控制,通过pwm-leds子系统管理背光亮度
- 配置LVGL自动初始化和工作队列运行模式
- 实现显示模块的工作队列更新机制,包含UI创建和定时刷新
- 添加详细的LVGL移植说明文档,涵盖设备树配置、调试步骤和常见问题
- 调整分区配置以适应LVGL固件大小需求
- 启用MCUBoot bootloader支持OTA功能

该变更使得系统能够在ST7789V显示屏上正常运行LVGL界面,并通过PWM控制背光。
2026-03-23 09:16:34 +08:00
7ebb8fc87c refactor(hid_tx_manager): 简化BLE模式下的条件判断
移除了冗余的active_mode检查,因为vendor dirty标志的处理
不应该依赖于特定的传输模式,提高了代码的通用性。
2026-03-20 18:00:20 +08:00
6ca70d2580 feat(app): 添加显示模块支持ST7789V显示屏
- 新增display_module.c实现LVGL显示功能,包括标签创建和定时刷新
- 在CMakeLists.txt中添加display_module.c到应用源文件列表
- 在app.overlay中配置显示设备选择和SPI接口使能
- 增加DISPLAY、MIPI_DBI、ST7789V、LVGL等相关配置选项
- 调整pm_static.yml中的应用分区大小以适应新的固件尺寸
- 禁用MCUBOOT和MCUMGR相关配置以节省空间
2026-03-20 17:25:57 +08:00
7e0f224ec8 docs(ble_bond): 添加自定义BLE Bond多身份连接设计说明文档
新增详细的自定义BLE Bond设计文档,涵盖多身份连接场景的支持说明。
文档内容包括:CAF自带BLE Bond与自定义BLE Bond的差异对比、架构图解、
多身份实现的关键工作点、实现注意事项以及推荐的分层设计方案。
2026-03-20 15:54:21 +08:00
a46b7ad8b8 feat(usb_hid): 支持HID供应商报告类型并增加输出报告大小
支持HID供应商特定报告类型的处理,在USB HID模块中添加了对REPORT_ID_VENDOR
的支持,并相应地修改了设备覆盖文件中的输出报告大小配置。

功能变更包括:
- 在app.overlay中将out-report-size从8增加到31以支持更大的报告
- 添加hid_vendor_mask_event.h头文件引入
- 实现try_extract_vendor_mask函数用于解析供应商特定掩码数据
- 在hid_stub_set_report和hid_stub_output_report函数中添加供应商掩码处理逻辑
- 更新handle_hid_tx_event函数以允许REPORT_ID_VENDOR类型的报告
2026-03-20 15:50:20 +08:00
3ff9f8c6fa feat(hid): 添加Vendor报告类型支持键盘掩码功能
- 新增REPORT_ID_VENDOR报告ID和相关常量定义
- 在HID报告描述符中添加Vendor页面的输入输出集合定义
- 更新BLE HID服务配置以支持3个输入报告和2个输出报告
- 实现hid_vendor_mask_event事件用于处理Vendor掩码数据
- 修改keyboard模块以支持物理状态和掩码状态分离
- 添加vendor_output_report_handler处理来自主机的掩码更新
- 更新CMakeLists.txt包含新的事件源文件
- 修改hid_tx_manager以支持Vendor报告的发送管理
2026-03-20 15:17:47 +08:00
9b29910299 feat(hid_tx_manager): 重构HID传输管理器以支持报告状态跟踪
- 引入原子标志位替换布尔变量,提高线程安全性
- 使用消息队列替代循环缓冲区实现传输队列
- 添加boot和NKRO报告状态管理功能
- 实现脏标记机制优化报告发送流程
- 改进传输完成事件处理逻辑
2026-03-20 13:47:54 +08:00
579dc35a36 feat: 添加HID传输管理和旋转编码器支持
添加了hid_tx_event和hid_tx_done_event事件类型,用于统一管理HID
数据传输,并在ble_hid_module和usb_hid_module中实现相应的处理逻辑。

新增qdec_module模块来处理旋转编码器输入,将旋转事件转换为步进事件,
并在keyboard_module中集成音量控制功能。

更新CMakeLists.txt以包含新的事件和模块文件,在app.overlay中启
用qdec设备,并在prj.conf中添加SENSOR配置。

BREAKING CHANGE: 将原有的hid_boot_event和hid_report_event替换
为统一的hid_tx_event事件系统。
2026-03-20 11:04:48 +08:00
2a389ef19b feat: 更新键盘固件的事件系统和模块配置
- 在CMakeLists.txt中添加hid_boot_event.c、keyboard_led_event.c和ble_slot_ctrl_module.c源文件
- 新增Kconfig配置项NEW_KBD_BLE_BOND_ENABLE用于启用应用特定的BLE绑定支持
- 修改prj.conf配置,禁用配对模式下的设备名称广播功能
- 重构电池状态事件结构,将charging和full布尔字段改为flags位域,并提供相应的访问函数
- 添加hid_boot_event事件类型,用于处理HID Boot协议输入报告
- 重命名keyboard_led_state_event为keyboard_led_event并改进LED状态处理逻辑
- 移除hid_protocol_event中的transport字段,简化协议事件处理
- 分离hid_report_event和hid_boot_event,明确区分Report和Boot协议报文处理
- 重构battery_module.c代码结构,改用上下文结构体管理电池模块状态
- 更新ble_battery_module.c使用新的电池状态事件访问接口
2026-03-18 13:41:36 +08:00
2a8b44d058 feat(app): 添加蓝牙电量模块支持
- 在CMakeLists.txt中添加ble_battery_module.c源文件
- 实现BLE电池服务模块,提供电池电量GATT服务
- 支持电池电量读取和通知功能
- 集成到应用事件管理器,监听电池状态事件
- 当蓝牙连接就绪时设置模块状态为READY
2026-03-16 17:45:00 +08:00
a9025d0f49 refactor(events): 移除未使用的usb_hid_event定义
移除src/events/usb_hid_event.c和usb_hid_event.h文件,
以及CMakeLists.txt中的相关源文件引用。

feat(modules): 电池模块增加充电状态电源管理限制策略

- 添加充电状态下的电源限制逻辑,充电时限制到ALIVE级别禁止自动休眠,
  非充电时恢复到SUSPENDED级别
- 默认非充电态允许进入SUSPENDED但禁止进入OFF

refactor(modules): 重构usb_hid_module减少模块间耦合

- 移除对power_manager_event.h和usb_hid_event.h的依赖
- 简化模块功能描述,去除对外发布usb_hid_event的职责
- 移除内部usb_hid_event相关的状态管理和发布逻辑
- 修改USB连接处理逻辑,在VBUS就绪时提交唤醒事件
2026-03-16 17:09:49 +08:00
4e8bb71f83 feat(usb_hid): 重构USB HID模块状态管理并优化电源管理
- 修改USB HID事件结构,将enable字段替换为stack_state枚举,
  区分VBUS连接状态和协议栈启用状态
- 更新日志和分析器中的字段映射,正确显示栈状态而非布尔使能值
- 添加电源管理限制功能,在USB连接时保持系统活跃状态
- 重构LED输入报告处理逻辑,支持引导和NKRO设备的报告解析
- 调整USB连接状态变更逻辑,仅反映VBUS连接情况
- 将power_manager超时配置从300秒调整为30秒以优化响应速度

BREAKING CHANGE: USB HID事件结构中的enable字段已替换为stack_state枚举类型
2026-03-16 15:22:10 +08:00
7587df7553 feat(led): 添加键盘LED状态管理模块
- 新增keyboard_led_state_event事件用于处理USB/BLE HID输出报告中的LED状态
- 实现led_state_module模块,管理Num Lock指示灯和BLE状态指示灯
- 定义LED状态效果映射,包括熄灭、常亮、慢闪、快闪等效果
- 将hid_keymap_def.h从configuration目录移至inc目录
- 在BLE和USB HID模块中添加对输出报告LED掩码的解析和处理
- 配置DTS中的led_1为可用状态,更新CMakeLists.txt构建配置
2026-03-16 11:39:27 +08:00
cd8101428d feat(keyboard): 添加完整的HID键盘功能模块
- 新增keyboard_module.c实现完整的键盘HID功能,包括按键映射、
  协议处理和报告生成
- 添加hid_protocol_event和hid_report_event事件系统支持
- 实现键盘和consumer类型的HID报告处理
- 支持Boot协议和Report协议两种模式
- 添加hid_keymap_def.h定义键盘映射表

refactor(ble_hid): 重构HIDS模块为BLE HID模块

- 将hids_module.c重命名为ble_hid_module.c
- 集成新的hid_protocol_event和hid_report_event事件处理
- 改进协议切换逻辑,添加协议事件发布功能
- 优化BLE HID报告发送机制

refactor(CMakeLists): 更新构建配置

- 添加新的事件模块源文件到构建列表
- 添加keyboard_module.c替换原有的button_map_module.c
- 添加ble_hid_module.c替换原有的hids_module.c
- 配置HID密钥映射定义路径编译选项

refactor(events): 简化USB HID事件结构

- 移除USB HID事件中的冗余状态字段
- 更新事件日志和分析器字段定义

docs(hid): 添加HID报告描述符文档

- 定义REPORT_ID_KEYBOARD和REPORT_ID_CONSUMER枚举值
- 整理HID报告相关的常量定义
2026-03-14 18:00:14 +08:00
a3196ef162 feat(ble): 添加蓝牙广播控制模块实现动态广告管理
- 在CMakeLists.txt中添加ble_adv_ctrl_module.c源文件
- 启用MCU管理器相关配置(CONFIG_MCUMGR等)以支持OTA功能
- 添加CAF模块挂起事件配置(CONFIG_CAF_MODULE_SUSPEND_EVENTS)
- 实现ble_adv_ctrl_module模块,根据当前模式类型控制蓝牙广告的挂起/恢复:
  * BLE模式时恢复广告,允许广播
  * USB/2.4G模式时挂起广告,禁止广播
- 模块在启动时默认请求挂起状态,防止模式切换前出现意外广播
- 通过CAF事件系统与ble_adv模块交互,实现广告控制逻辑
2026-03-14 14:04:59 +08:00
e893ddded6 feat: 添加USB HID模块支持
- 添加usb_hid_event事件定义和实现,用于管理USB HID状态
- 添加usb_hid_module模块,实现USB HID协议栈的完整生命周期管理
- 在CMakeLists.txt中注册新的事件和模块源文件
- 在设备树overlay中配置三个HID设备:HID_BOOT、HID_NKRO、HID_RAW
- 在prj.conf中启用USB设备栈相关配置选项
- 修复电池模块和模式切换模块中的重复挂起问题
- 改进蓝牙绑定模块的错误处理和日志记录
- 在app.overlay中启用usbd节点并添加PMIC配置调整
2026-03-14 12:13:25 +08:00
81846a870f feat: 添加配置事件和蓝牙配对模块支持多设备连接
添加了新的配置事件类型用于本地模块配置,包括事件定义和头文件,
以及蓝牙配对模块来管理多个配对设备。更新了CMakeLists.txt以包含
新的源文件,并修改prj.conf增加蓝牙配对数量限制。

- 新增config_event事件类型用于本地配置通信
- 实现ble_bond_module用于管理蓝牙配对和身份切换
- 配置蓝牙最大配对数和身份数为4
- 支持通过配置通道进行设备选择、删除等操作
2026-03-13 16:45:34 +08:00
05f4f117b0 feat(hids): 添加HID服务模块支持键盘和多媒体功能
- 新增hids_module.c实现蓝牙HID服务,支持键盘NKRO和Consumer控制
- 添加hid_report_descriptor.h定义统一的HID描述符,包括键盘、多媒体和RAW HID
- 在CMakeLists.txt中注册hids模块源文件
- 配置prj.conf启用蓝牙HID相关配置项,设置设备名称和外观
- 修改main.c移除启动LED效果,简化主函数逻辑
- 添加settings_loader_def.h确保模块依赖正确加载
- 配置pm_static.yml分配flash存储空间给mcuboot和settings
- 调整电源管理超时时间从20秒增加到300秒
- 启用MCUBOOT引导加载器支持
2026-03-13 16:31:02 +08:00
b3516b988a feat(battery): 添加电池管理模块和IP5305 PMIC支持
- 添加电池状态监测模块,包括ADC采样和SOC估算功能
- 集成IP5305电源管理芯片支持,配置I2C通信和保活机制
- 实现电池状态事件系统,包含充电状态、满电状态和电量百分比
- 添加电池使能GPIO控制和采样工作队列
- 配置设备树支持电池检测和PMIC控制
- 添加外部模块路径到CMakeLists.txt并更新.gitignore
2026-03-13 08:05:31 +08:00
86af0d2373 feat: 添加模式切换模块支持多协议键盘
添加了完整的模式切换功能,通过ADC采样检测模式拨码开关,
实现USB、BLE和2.4G三种工作模式的自动识别和切换。

- 新增mode_event事件用于传递模式状态
- 实现mode_switch_module模块,包含ADC初始化、
  模式识别算法和状态管理逻辑
- 配置CMakeLists.txt添加新源文件和头文件目录
- 更新设备树配置启用ADC和IO通道
- 添加Kconfig选项CONFIG_ADC=y
- 实现防抖机制和稳定的模式检测逻辑
- 集成到CAF事件系统,支持电源管理状态切换
2026-03-11 10:44:50 +08:00
3d9ce9168f feat(kbd): 添加CAF按钮模块支持
- 在CMakeLists.txt中添加button_map_module.c源文件和头文件目录
- 创建inc/buttons_def.h定义按钮矩阵引脚配置
- 配置prj.conf启用CAF按钮相关功能和参数
- 实现src/modules/button_map_module.c按钮映射逻辑
- 支持6x4键盘矩阵的按键事件处理和Linux输入键码转换
2026-03-10 16:09:33 +08:00
cd3400a9ba feat(led): 使用CAF LEDs模块替代自定义LED驱动
- 移除src/modules/led_module.c中的自定义GPIO LED实现
- 在CMakeLists.txt中移除对led_module.c的引用
- 更新prj.conf配置,启用CAF LEDs相关功能
- 添加CONFIG_CAF_POWER_MANAGER配置项以支持电源管理
- 在main.c中集成LED事件,在系统就绪时触发启动LED效果
- 实现白色常亮LED效果作为系统启动指示
2026-03-10 15:00:37 +08:00
c5778e6c7a feat(keyboard): 添加LED模块支持
- 在CMakeLists.txt中添加src/modules/led_module.c源文件
- 创建app.overlay设备树配置文件,启用GPIO0、GPIO1、GPIOTE和LED_0
- 在prj.conf中启用CONFIG_GPIO配置选项
- 实现led_module.c,包含LED GPIO控制逻辑和CAF事件处理
2026-03-10 14:38:54 +08:00
105 changed files with 12470 additions and 2 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
build*/
external*/

View File

@@ -1,6 +1,63 @@
cmake_minimum_required(VERSION 3.20.0)
if(EXISTS "E:/extra/modules/ip5306")
list(APPEND ZEPHYR_EXTRA_MODULES "E:/extra/modules/ip5306")
endif()
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(new_kbd)
target_sources(app PRIVATE src/main.c)
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/inc)
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/events)
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/ui)
zephyr_compile_definitions(
LV_LVGL_H_INCLUDE_SIMPLE=1
LV_FONT_MONTSERRAT_14=0
LV_FONT_UNSCII_8=0
LV_FONT_DEFAULT=\&ui_font_keyboard_small_18
"LV_FONT_CUSTOM_DECLARE=LV_FONT_DECLARE(ui_font_keyboard_small_18) LV_FONT_DECLARE(ui_font_keyboard_time_48)"
)
target_compile_definitions(app PRIVATE
APP_HID_KEYMAP_DEF_PATH=\"hid_keymap_def.h\"
)
target_sources(app PRIVATE
src/main.c
src/events/battery_status_event.c
src/events/config_event.c
src/events/display_theme_event.c
src/events/hid_boot_event.c
src/events/hid_host_ack_event.c
src/events/hid_host_command_error_event.c
src/events/hid_host_command_event.c
src/events/hid_protocol_event.c
src/events/hid_report_event.c
src/events/hid_tx_done_event.c
src/events/hid_tx_event.c
src/events/hid_vendor_mask_event.c
src/events/keyboard_led_event.c
src/events/mode_event.c
src/events/qdec_step_event.c
src/events/time_sync_event.c
src/modules/battery_module.c
src/modules/ble_adv_ctrl_module.c
src/modules/ble_battery_module.c
src/modules/ble_bond_module.c
src/modules/ble_slot_ctrl_module.c
src/modules/display_module.c
src/modules/hid_host_command_module.c
src/modules/hid_tx_manager_module.c
src/modules/keyboard_module.c
src/modules/led_state_module.c
src/modules/mode_switch_module.c
src/modules/qdec_module.c
src/modules/time_manager_module.c
src/modules/usb_hid_module.c
src/modules/ble_hid_module.c
src/ui/display_ui.c
src/ui/fonts/ui_font_keyboard_small_18.c
src/ui/fonts/ui_font_keyboard_time_48.c
)

17
Kconfig Normal file
View File

@@ -0,0 +1,17 @@
menu "new_kbd"
config NEW_KBD_BLE_BOND_ENABLE
bool "Enable app-specific BLE bond support"
default y
depends on BT_BONDABLE
depends on BT_SETTINGS
depends on CAF_SETTINGS_LOADER
depends on CAF_BLE_COMMON_EVENTS
select CAF_BLE_BOND_SUPPORTED
help
Inform CAF modules that the application provides its own BLE bond
implementation and advertising must wait for it to become ready.
endmenu
source "Kconfig.zephyr"

47
app.overlay Normal file
View File

@@ -0,0 +1,47 @@
#include <zephyr/dt-bindings/gpio/gpio.h>
/ {
chosen {
zephyr,boot-mode = &boot_mode0;
};
hid_dev_0: hid_dev_0 {
compatible = "zephyr,hid-device";
label = "HID_BOOT";
protocol-code = "keyboard";
in-report-size = <8>;
in-polling-period-us = <1000>;
out-report-size = <8>;
out-polling-period-us = <1000>;
};
hid_dev_1: hid_dev_1 {
compatible = "zephyr,hid-device";
label = "HID_NKRO";
protocol-code = "none";
in-report-size = <31>;
in-polling-period-us = <1000>;
out-report-size = <31>;
out-polling-period-us = <1000>;
};
raw_hid: hid_dev_2 {
compatible = "zephyr,hid-device";
label = "HID_RAW";
protocol-code = "none";
in-report-size = <64>;
in-polling-period-us = <1000>;
out-report-size = <64>;
out-polling-period-us = <1000>;
};
};
&gpregret1 {
status = "okay";
boot_mode0: boot_mode@0 {
compatible = "zephyr,retention";
status = "okay";
reg = <0x0 0x1>;
};
};

View File

@@ -0,0 +1,2 @@
config BOARD_ATGUIGU_KEYBOARD_DONGLE
select SOC_NRF52833_QDAA

View File

@@ -0,0 +1,2 @@
&pinctrl {
};

View File

@@ -0,0 +1,42 @@
/dts-v1/;
#include <nordic/nrf52833_qdaa.dtsi>
#include "atguigu_keyboard_dongle-pinctrl.dtsi"
/ {
model = "Keyboard Dongle";
compatible = "atguigu,atguigu-keyboard-dongle";
chosen {
zephyr,sram = &sram0;
zephyr,flash = &flash0;
zephyr,code-partition = &slot0_partition;
};
};
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 DT_SIZE_K(48)>;
};
slot0_partition: partition@c000 {
label = "image-0";
reg = <0x0000c000 DT_SIZE_K(220)>;
};
slot1_partition: partition@43000 {
label = "image-1";
reg = <0x00043000 DT_SIZE_K(220)>;
};
storage_partition: partition@7a000 {
label = "storage";
reg = <0x0007a000 DT_SIZE_K(24)>;
};
};
};

View File

@@ -0,0 +1,10 @@
identifier: atguigu_keyboard_dongle/nrf52833
name: Keyboard Dongle
vendor: atguigu
type: mcu
arch: arm
ram: 128
flash: 512
toolchain:
- zephyr
supported: []

View File

@@ -0,0 +1,2 @@
CONFIG_ARM_MPU=y
CONFIG_HW_STACK_PROTECTION=y

View File

@@ -0,0 +1,9 @@
set(OPENOCD_NRF5_SUBFAMILY "nrf52")
board_runner_args(jlink "--device=nRF52833_xxAA" "--speed=4000")
board_runner_args(pyocd "--target=nrf52833" "--frequency=4000000")
include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake)
include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake)

View File

@@ -0,0 +1,5 @@
board:
name: atguigu_keyboard_dongle
vendor: atguigu
socs:
- name: nrf52833

View File

@@ -0,0 +1,2 @@
# Suppress "unique_unit_address_if_enabled" to handle some overlaps
list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled")

View File

@@ -0,0 +1,2 @@
config BOARD_ATGUIGU_MINI_KEYBOARD
select SOC_NRF52840_QFAA

View File

@@ -0,0 +1,73 @@
&pinctrl {
qdec_default: qdec_default {
group1 {
psels = <NRF_PSEL(QDEC_A, 0, 10)>,
<NRF_PSEL(QDEC_B, 1, 6)>;
bias-pull-up;
};
};
qdec_sleep: qdec_sleep {
group1 {
psels = <NRF_PSEL(QDEC_A, 0, 10)>,
<NRF_PSEL(QDEC_B, 1, 6)>;
low-power-enable;
};
};
led_spi_default: led_spi_default {
group1 {
psels = <NRF_PSEL(SPIM_MOSI, 0, 20)>;
};
};
led_spi_sleep: led_spi_sleep {
group1 {
psels = <NRF_PSEL(SPIM_MOSI, 0, 20)>;
low-power-enable;
};
};
lcd_spi_default: lcd_spi_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 13)>,
<NRF_PSEL(SPIM_MOSI, 0, 28)>;
};
};
lcd_spi_sleep: lcd_spi_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 13)>,
<NRF_PSEL(SPIM_MOSI, 0, 28)>;
low-power-enable;
};
};
i2c1_default: i2c1_default {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 1, 0)>,
<NRF_PSEL(TWIM_SCL, 0, 24)>;
bias-pull-up;
};
};
i2c1_sleep: i2c1_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SDA, 1, 0)>,
<NRF_PSEL(TWIM_SCL, 0, 24)>;
low-power-enable;
};
};
pwm0_default: pwm0_default {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 11)>;
};
};
pwm0_sleep: pwm0_sleep {
group1 {
psels = <NRF_PSEL(PWM_OUT0, 1, 11)>;
low-power-enable;
};
};
};

View File

@@ -0,0 +1,240 @@
/dts-v1/;
#include <nordic/nrf52840_qiaa.dtsi>
#include "atguigu_mini_keyboard-pinctrl.dtsi"
#include <zephyr/dt-bindings/pinctrl/nrf-pinctrl.h>
#include <zephyr/dt-bindings/led/led.h>
/ {
model = "Mini Keyboard, 17 keys";
compatible = "atguigu,atguigu-mini-keyboard";
chosen {
zephyr,sram = &sram0;
zephyr,flash = &flash0;
zephyr,code-partition = &slot0_partition;
zephyr,display = &st7789v3;
};
aliases {
backlight = &backlight;
};
mode_sense: mode-sense {
compatible = "voltage-divider";
io-channels = <&adc 5>;
output-ohms = <1>;
full-ohms = <1>;
};
battery_sense: battery-sense {
compatible = "voltage-divider";
io-channels = <&adc 7>;
output-ohms = <100000>;
full-ohms = <200000>;
power-gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>;
power-on-sample-delay-us = <100>;
};
pwm_leds: pwm_leds {
compatible = "pwm-leds";
status = "okay";
backlight: pwm_led_0 {
pwms = <&pwm0 0 PWM_MSEC(10) PWM_POLARITY_INVERTED>;
};
};
led_0: led_0 {
compatible = "gpio-leds";
status = "okay";
chan0 {
gpios = <&gpio0 17 GPIO_ACTIVE_LOW>;
};
};
led_1: led_1 {
compatible = "gpio-leds";
status = "okay";
chan0 {
gpios = <&gpio1 2 GPIO_ACTIVE_LOW>;
};
};
mipi_dbi: mipi_dbi {
compatible = "zephyr,mipi-dbi-spi";
status = "okay";
spi-dev = <&spi3>;
dc-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
write-only;
#address-cells = <1>;
#size-cells = <0>;
st7789v3: st7789v@0 {
compatible = "sitronix,st7789v";
status = "okay";
reg = <0>;
mipi-max-frequency = <32000000>;
width = <320>;
height = <172>;
x-offset = <0>;
y-offset = <34>;
vcom = <0x2b>;
gctrl = <0x35>;
vrhs = <0x11>;
vdvs = <0x20>;
mdac = <0xA0>;
lcm = <0x2c>;
colmod = <0x55>;
gamma = <0x01>;
porch-param = [ 0c 0c 00 33 33 ];
cmd2en-param = [ 5a 69 02 01 ];
pwctrl1-param = [ a4 a1 ];
pvgam-param = [ d0 00 02 07 0a 28 32 44 42 06 0e 12 14 17 ];
nvgam-param = [ d0 00 02 07 0a 28 31 54 47 0e 1c 17 1b 1e ];
ram-param = [ 00 f0 ];
rgb-param = [ c0 02 14 ];
mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";
};
};
};
&spi2 {
status = "okay";
pinctrl-0 = <&led_spi_default>;
pinctrl-1 = <&led_spi_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 21 GPIO_ACTIVE_LOW>;
led_strip: ws2812@0 {
compatible = "worldsemi,ws2812-spi";
supply-gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
reg = <0>;
spi-max-frequency = <8000000>;
chain-length = <17>;
color-mapping = <LED_COLOR_ID_GREEN LED_COLOR_ID_RED LED_COLOR_ID_BLUE>;
spi-one-frame = <0xFC>;
spi-zero-frame = <0xC0>;
};
};
&i2c1 {
status = "okay";
pinctrl-0 = <&i2c1_default>;
pinctrl-1 = <&i2c1_sleep>;
pinctrl-names = "default", "sleep";
clock-frequency = <400000>;
ip5306: pmic@75 {
status = "okay";
compatible = "injoinic,ip5306";
reg = <0x75>;
keepalive-gpios = <&gpio0 22 GPIO_ACTIVE_LOW>;
keepalive-interval-ms = <10000>;
keepalive-offload;
};
};
/* 编码器 */
&qdec {
status = "okay";
/* 引用上面定义的标签 */
pinctrl-0 = <&qdec_default>;
pinctrl-1 = <&qdec_sleep>;
/* 指定 pinctrl-0 为默认pinctrl-1 为睡眠 */
pinctrl-names = "default", "sleep";
/* 别忘了 QDEC 必须的两个属性 */
steps = <40>;
led-pre = <0>;
};
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "mcuboot";
reg = <0x00000000 DT_SIZE_K(48)>;
};
slot0_partition: partition@c000 {
label = "image-0";
reg = <0x0000c000 DT_SIZE_K(472)>;
};
slot1_partition: partition@82000 {
label = "image-1";
reg = <0x00082000 DT_SIZE_K(472)>;
};
storage_partition: partition@f8000 {
label = "storage";
reg = <0x000f8000 DT_SIZE_K(32)>;
};
};
};
&spi3 {
status = "okay";
pinctrl-0 = <&lcd_spi_default>;
pinctrl-1 = <&lcd_spi_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio0 2 GPIO_ACTIVE_LOW>;
};
&adc {
status = "okay";
#address-cells = <1>;
#size-cells = <0>;
channel@5 {
reg = <5>;
zephyr,gain = "ADC_GAIN_1_6";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 20)>;
zephyr,input-positive = <NRF_SAADC_AIN5>;
zephyr,resolution = <12>;
};
channel@7 {
reg = <7>;
zephyr,gain = "ADC_GAIN_1_6";
zephyr,reference = "ADC_REF_INTERNAL";
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 20)>;
zephyr,input-positive = <NRF_SAADC_AIN7>;
zephyr,resolution = <12>;
};
};
&pwm0 {
status = "okay";
pinctrl-0 = <&pwm0_default>;
pinctrl-1 = <&pwm0_sleep>;
pinctrl-names = "default", "sleep";
};
&gpio0 {
status = "okay";
};
&gpio1 {
status = "okay";
};
&gpiote {
status = "okay";
};
&usbd {
status = "okay";
};
&uicr {
nfct-pins-as-gpios;
gpio-as-nreset;
};

View File

@@ -0,0 +1,10 @@
identifier: atguigu_mini_keyboard/nrf52840
name: Mini Keyboard, 17 keys
vendor: atguigu
type: mcu
arch: arm
ram: 256
flash: 1024
toolchain:
- zephyr
supported: []

View File

@@ -0,0 +1,2 @@
CONFIG_ARM_MPU=y
CONFIG_HW_STACK_PROTECTION=y

View File

@@ -0,0 +1,9 @@
set(OPENOCD_NRF5_SUBFAMILY "nrf52")
board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000")
board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000")
include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake)
include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake)

View File

@@ -0,0 +1,5 @@
board:
name: atguigu_mini_keyboard
vendor: atguigu
socs:
- name: nrf52840

View File

@@ -0,0 +1,2 @@
# Suppress "unique_unit_address_if_enabled" to handle some overlaps
list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled")

View File

@@ -0,0 +1,411 @@
# 自定义 BLE Bond 以支持多身份连接的设计说明
## 1. 背景
CAF 自带的 `ble_bond` 适合简单外设场景。它默认假设应用只使用 `BT_ID_DEFAULT` 一个本地身份,职责主要是:
- 等待 `settings_loader` 完成初始化
- 根据按键触发删除默认身份上的 bond
- 通过 `CONFIG_CAF_BLE_BOND_SUPPORTED` 告知 CAF 应用实现了 BLE bond
如果应用希望像多设备键盘一样支持多个 Bluetooth identity 对应多个 slot则必须提供自定义 `ble_bond`。因为此时已经不只是“是否有 bond”而是“哪一个 peer 属于哪一个 slot、当前应使用哪一个 identity、切换 slot 时如何与 `ble_adv`/`ble_state` 协同”。
---
## 2. CAF 自带 BLE Bond 与自定义 BLE Bond 的差异
### 2.1 CAF 自带 BLE Bond
特点:
- 只管理默认 identity
- 不维护 slot 到 identity 的映射
- 不发布 `ble_peer_operation_event`
- 不处理多主机切换
- 不处理 peer 所属 slot 自动归位
适用场景:
- 单主机 BLE 外设
- 仅需擦除 bond
- 不需要多身份、多 slot、多 peer 路由
### 2.2 自定义 BLE Bond
特点:
- 维护应用 slot 与 Bluetooth stack identity 的映射
- 保存当前选中的 slot
- 在 slot 切换时发布 `ble_peer_operation_event`
-`ble_adv` 协同切换广告 identity
-`ble_state` 协同确认连接落在正确 identity 上
- 处理同一 peer 属于哪个 slot
- 可扩展实现 peer 迁移、自动回 slot、自动重配等策略
适用场景:
- 多设备键盘
- 每个 slot 对应独立 bond
- 需要在多个 identity 间切换广告和连接
- 需要对 host 行为做应用级策略控制
---
## 3. 两版 BLE Bond 架构图
### 3.1 CAF 自带 BLE Bond 架构
```mermaid
flowchart LR
A[settings_loader] --> B[CAF ble_bond]
C[click_event] --> B
B -->|bt_unpair BT_ID_DEFAULT| D[Bluetooth Stack]
B -->|MODULE_STATE_READY| E[CAF Modules]
D --> F[单一默认 Identity]
```
说明:
- `CAF ble_bond` 不参与 slot 选择
- 也不参与广告 identity 路由
- 仅在用户触发擦除时对默认 identity 执行 `bt_unpair()`
### 3.2 自定义多身份 BLE Bond 架构
```mermaid
flowchart TD
A[settings_loader] --> B[custom ble_bond]
C[config_event / slot control] --> B
D[ble_peer_event CONNECTED/SECURED/DISCONNECTED] --> B
B --> E[slot <-> bt_stack_id LUT]
B --> F[current selected slot]
B --> G[ble_peer_operation_event]
G --> H[CAF ble_adv]
G --> I[slot name / GAP name provider]
H --> J[Advertising on selected Identity]
J --> K[Windows / Host]
K --> D
B -->|validate connection identity| L[bt_conn_disconnect]
B -->|find peer owner slot| M[auto switch / reconnect]
```
说明:
- 自定义版本已经成为 BLE peer 管理的核心策略层
- 它既要维护持久化状态,也要处理运行时连接路由
- `ble_adv` 只负责按事件切换 identity 广播,不负责理解 slot 语义
---
## 4. 相比 CAF 自带 BLE Bond额外必须完成的工作
### 4.1 身份资源规划
必须先定义:
- 一共有多少个应用 slot
- `CONFIG_BT_ID_MAX` 至少要覆盖这些 slot
- 是否保留 `BT_ID_DEFAULT`
- 每个 slot 对应哪个 stack identity
当前实现中:
- `slot_count = 3`
- `APP_PEER_COUNT = CONFIG_BT_ID_MAX - 1`
- `BT_ID_DEFAULT` 不直接用于可切换 slot
- slot 0/1/2 分别映射到 identity 1/2/3
### 4.2 持久化保存应用层状态
CAF 自带实现不保存 slot 语义。自定义版本必须自行保存:
- 当前选中的 slot
- slot 到 stack identity 的 LUT
否则重启后:
- `ble_adv` 可能在错误 identity 上广播
- 连接会落到旧 identity
- 导致加密后被应用判定为错误 slot
### 4.3 向 CAF 明确声明“应用实现了 BLE bond”
如果应用没有通过 Kconfig `select CAF_BLE_BOND_SUPPORTED`:
- `ble_adv` 不会等待自定义 `ble_bond` ready
- 开机可能在错误 identity 上提前广播
- 最终出现连接到 old id、加密后又断开的异常
因此自定义 BLE bond 不能只写 C 文件,还必须补齐 Kconfig 接线。
### 4.4 设计并发布 peer operation 事件
CAF 自带 `ble_bond` 不发布 `ble_peer_operation_event`,但多身份实现必须发布,例如:
- `PEER_OPERATION_SELECTED`
- `PEER_OPERATION_ERASE_ADV`
- `PEER_OPERATION_ERASED`
这些事件会驱动:
- `ble_adv` 切换广告 identity
- 名称提供器更新 GAP/广告名
- LED/状态模块同步 UI
### 4.5 处理 slot 切换时的现有连接
当用户切换 slot 时,应用必须决定:
- 现有 LE 连接是否断开
- 断开后是否立刻以新 identity 重启广告
如果不主动断开:
- 连接仍停留在旧 identity
- 逻辑上已经切到新 slot但链路仍属于旧 slot
- 后续加密、重连、广告状态都会错乱
### 4.6 在连接建立后校验“连接是否属于当前 slot”
多身份实现必须在 `PEER_STATE_SECURED` 甚至更早的 `CONNECTED` 时校验:
- 当前连接的 `info.id`
- 当前选中 slot 对应的 `bt_stack_id`
如果不一致:
- 这是旧 identity、旧连接或错误路由
- 必须主动断开,避免错误 bond 被继续使用
### 4.7 处理 peer 与 slot 的归属关系
CAF 自带实现没有“peer 属于哪个 slot”这个概念。多身份实现必须处理:
- 同一个 peer 之前绑定在哪个 slot
- 当前连接是不是连错 slot
- 是否自动切回该 peer 所属 slot
- 是否允许 peer 从一个 slot 迁移到另一个 slot
当前 C1 实现已经支持:
- 当 peer 连到错误 slot 时
- 自动识别该 peer 真实所属 slot
- 自动切回正确 slot
- 主动断开一次,等待 host 重连
### 4.8 处理 bond 擦除和 identity reset
多身份实现通常不能只 `bt_unpair(BT_ID_DEFAULT, NULL)`,还要区分:
- 删除某个 slot 的 peer
- 删除全部 slot
- 是否要 `bt_id_reset()`
- 擦除后是否需要重启该 identity 的广告会话
这部分已经不再是单纯 bond 删除而是“identity 生命周期管理”。
---
## 5. 自定义 BLE Bond 实现中的重点注意事项
### 5.1 先解决 Kconfig 接线,再调连接逻辑
这是最容易遗漏的点。
如果没有正确让应用 `select CAF_BLE_BOND_SUPPORTED`:
- `ble_adv` 启动顺序会不对
- 自定义 `ble_bond` 即使逻辑正确,也会在启动阶段表现异常
这属于架构接线问题,不是连接状态机问题。
### 5.2 广播名和 GAP 名必须统一策略
多 slot 场景下,名称策略必须明确:
- 是每个 slot 不同名字
- 还是所有 slot 同名
如果广告名和 GAP Device Name 不一致:
- Windows 可能扫描到一个名字
- 配对后读到另一个名字
- 容易造成驱动实例重建、名字重命名、缓存混乱
当前项目已经验证:
- 统一 GAP 名与广告名后
- Windows 行为更稳定
### 5.3 不要假设 Windows 删除设备就等于双方 bond 都删了
这在多 slot 里尤其关键。
Windows 端删设备后:
- 主机端密钥可能删了
- 设备端旧 slot 的本地 bond 仍然存在
这会导致:
- 新 slot 上重新配对被 SMP 拒绝
- 日志出现 `Refusing new pairing. The old bond must be unpaired first.`
因此自定义 BLE bond 必须明确“本地 bond”和“主机侧配对记录”不是同一件事。
### 5.4 host 删除密钥无法直接读取,只能通过行为推断
设备无法直接读取 Windows 是否删除了配对密钥。
只能根据以下现象推断:
- 已绑定 slot 上能否顺利加密
- 是否重新发起 pairing
- 是否出现安全失败或 SMP 冲突
因此设计 C2 这类“自动清旧 bond 并重配”的逻辑时,必须以“推断”而不是“查询”来实现。
### 5.5 连接事件与安全事件分工要清楚
推荐分层:
- `CONNECTED` 阶段处理 peer 归属和 slot 自动路由
- `SECURED` 阶段确认连接确实落在当前选中 identity
- `DISCONNECTED` 阶段清理自动切换或迁移的临时状态
如果所有逻辑都堆到 `SECURED`:
- 处理时机偏晚
- 经常已经进入 SMP/安全过程
- 更容易触发 host 侧异常
### 5.6 自动切 slot 时要接受“一次断开再重连”
这是应用策略设计里的现实约束。
当 peer 连错 slot 时:
- 应用切换到正确 slot
- 需要让旧连接断开
- 然后等待 host 按正确 identity 重新连入
不要追求“同一条连接无感切换 identity”这在 BLE identity 语义上不现实,也不稳定。
### 5.7 `bt_foreach_bond()` 和地址匹配要谨慎
如果实现 peer 所属 slot 判断,通常会用:
- `bt_conn_get_dst(conn)`
- `bt_foreach_bond(local_id, ...)`
- `bt_addr_le_cmp()`
需要注意:
- public address 与 random/RPA 地址行为不同
- 已绑定设备可能因为隐私地址导致匹配行为更复杂
- 实际测试必须覆盖 Windows、Android、iOS 等 host
当前项目日志里 host 使用 public address因此匹配较直接。
### 5.8 identity reset 的语义要和 UI/配置动作对齐
在自定义多 slot 实现中,`erase_peer()` 往往同时包含:
- 删除 bond
- reset identity
- 触发广告重启
因此必须保证 UI 动作和内部语义一致。例如:
- “清当前 slot 配对” 是否意味着彻底 reset 当前 identity
- “清全部配对” 是否会影响所有 slot 广播身份
如果定义不清,后续调试会非常混乱。
### 5.9 必须大量依赖日志建立可观测性
自定义 BLE bond 不像 CAF 默认版那样简单。建议至少保留以下日志:
- 当前 slot 与 stack identity
- settings 恢复结果
- slot 切换请求
- `PEER_OPERATION_SELECTED`
- 连接建立的 `info.id`
- `SECURED` 时的 identity 匹配结果
- peer 所属 slot 自动识别结果
- 自动切 slot 开始与结束
没有这些日志,多身份调试成本会非常高。
---
## 6. 推荐的实现分层
为了避免自定义 BLE bond 过度膨胀,建议分层如下:
- `ble_bond`
负责 slot/identity 映射、持久化状态、peer 归属、配对策略
- `ble_adv`
负责在指定 identity 上广播
- `slot_name / GAP name provider`
负责名称策略
- `slot_ctrl`
负责把按键或配置命令转换成 slot 选择请求
- `ble_state`
负责连接、安全、断开事件广播
这个分层里最重要的原则是:
- `ble_adv` 不理解 slot 语义
- `ble_state` 不理解 slot 策略
- slot 语义统一由自定义 `ble_bond` 决策
---
## 7. 当前项目自定义 BLE Bond 已实现的内容
当前 `new_kbd` 自定义 `ble_bond` 已经具备:
- 3 个应用 slot
- slot 到 identity 的持久化映射
- 当前 slot 的持久化保存
- `settings_loader` 后初始化 identity
- `PEER_OPERATION_SELECTED` 广播给 `ble_adv`
- slot 切换时主动断开现有 LE 连接
- `SECURED` 时校验当前连接是否落在期望 identity
- C1: peer 连错 slot 时自动识别所属 slot 并切回
尚未自动化的部分:
- C2: 主机端删密钥后,本地自动删除旧 bond 并允许重配
- peer 迁移到新 slot 的完整自动化策略
- 更复杂 host 场景下的异常恢复
---
## 8. 结论
CAF 自带 `ble_bond` 是“单 identity 的 bond 擦除模块”。
一旦要支持多 slot、多 identity、多 host 路由,自定义 `ble_bond` 就不再只是替代品,而是整套 BLE peer 管理策略的核心。
相对 CAF 自带实现,额外工作主要集中在:
- identity 规划
- 状态持久化
-`ble_adv`/`ble_state` 的事件协同
- slot 选择与连接路由
- peer 归属识别
- 多 host / host 缓存 / bond 生命周期处理
实现时最需要注意的是:
- Kconfig 接线必须正确
- 广播 identity 与当前 slot 必须严格一致
- 广告名/GAP 名策略必须统一
- host 删除配对记录不等于本地 bond 已删除
- 自动切 slot 和重连应被视为正常流程,而不是异常

View File

@@ -0,0 +1,281 @@
# BLE 时间同步服务 PC 上位机接入文档
## 1. 概述
当前项目实现了一套独立的 BLE 时间同步服务,供 PC 上位机在蓝牙连接后向键盘写入当前 UTC 时间、时区和时间精度。
这套服务的设计目标是:
- 不走 HID 报告通道,避免与键盘输入链路耦合。
- BLE 侧只负责协议适配,实际时间状态统一交给 `time_manager` 管理。
- 主机只负责“下发时间”,设备不通过此服务回读时间,也不通过 notify 返回确认。
结论上,这是一条:
- 自定义 GATT Service
- 单个可写 Characteristic
- 仅支持写入
- 需要加密连接
的单向时间同步通道。
## 2. GATT 定义
### 2.1 Service UUID
`0b7f5000-38d2-4f62-8f6f-36c4fd73a110`
### 2.2 Characteristic UUID
`0b7f5001-38d2-4f62-8f6f-36c4fd73a110`
### 2.3 Characteristic 属性
- `Write`
- `Write Without Response`
### 2.4 Characteristic 权限
- `Write Encrypted`
也就是说,上位机在写入前必须先完成配对/加密。未加密链路下,写入会被 GATT 层拒绝。
## 3. 设备端处理逻辑
### 3.1 依赖条件
设备端只有在以下两个条件同时满足时,才接受时间写入:
- BLE 栈已经 ready
- `time_manager` 已经 ready
如果模块尚未 ready写入会返回 ATT 错误。
### 3.2 写入成功后的行为
设备端收到合法 payload 后会:
1. 校验版本、长度和 flags。
2. 解析 `utc_ms``timezone_min``accuracy_ms`
3. 自动把同步来源标记为 `BLE`
4. 投递 `time_sync_event``time_manager`
5. `time_manager` 更新运行时钟状态,并延迟异步写入 settings。
注意:
- 当前 BLE 时间同步服务没有 `Read``Notify` 能力。
- 主机拿到 GATT 写成功,只能说明设备接受了这次写入。
- 设备不会通过这个服务主动回传“当前时间”。
## 4. 数据包格式
### 4.1 总长度
固定 `16` 字节。
### 4.2 字段布局
| 偏移 | 长度 | 字段名 | 类型 | 字节序 |
| --- | --- | --- | --- | --- |
| 0 | 1 | `version` | `uint8` | - |
| 1 | 1 | `flags` | `uint8` | - |
| 2 | 2 | `timezone_min` | `int16` | little-endian |
| 4 | 8 | `utc_ms` | `uint64` | little-endian |
| 12 | 4 | `accuracy_ms` | `uint32` | little-endian |
### 4.3 当前协议版本
- `version = 1`
### 4.4 flags 定义
当前只定义 1 个 bit
- `BIT(0)` = `TIMEZONE_VALID`
因此当前版本必须满足:
- `flags & 0x01 != 0`
推荐主机固定写:
- `flags = 0x01`
## 5. 字段语义
### 5.1 `timezone_min`
单位:分钟。
含义:本地时区相对 UTC 的偏移。
示例:
- 中国标准时间 UTC+8 -> `480`
- 印度 UTC+5:30 -> `330`
- UTC-5 -> `-300`
设备端当前接受范围:
- `-1440 ~ +1440`
### 5.2 `utc_ms`
单位:毫秒。
含义UTC 时间戳,不带本地时区偏移。
要求:
- 必须大于 `0`
### 5.3 `accuracy_ms`
单位:毫秒。
含义:本次时间来源的估计精度。
建议:
- 若上位机没有可靠精度信息,可直接写 `0`
- 若来自系统时间并已做网络对时,也可以给一个保守值,例如 `50``100`
## 6. 主机写入建议
### 6.1 推荐使用 Write Request
虽然设备同时支持:
- Write
- Write Without Response
但 PC 上位机侧建议优先使用 **Write Request带响应写**,原因是:
- 更容易拿到 ATT 层成功/失败结果
- 更方便在开发阶段定位协议错误
- 更适合作为配置/同步类命令
### 6.2 推荐时序
1. 扫描并连接设备。
2. 完成配对/加密。
3. 发现时间同步 Service 和 Characteristic。
4. 组包为固定 16 字节 payload。
5. 执行一次带响应写入。
6. 若写成功,可视为设备已接受此次同步请求。
## 7. Python 示例
下面示例基于 `bleak`,演示如何向设备写入当前系统时间。
```python
import asyncio
import struct
import time
from bleak import BleakClient
TIME_SYNC_CHAR_UUID = "0b7f5001-38d2-4f62-8f6f-36c4fd73a110"
def build_time_sync_payload(timezone_min: int, accuracy_ms: int = 0) -> bytes:
version = 1
flags = 0x01 # TIMEZONE_VALID
utc_ms = int(time.time() * 1000)
return struct.pack("<BBhQI", version, flags, timezone_min, utc_ms, accuracy_ms)
async def main(address: str):
payload = build_time_sync_payload(timezone_min=480, accuracy_ms=50)
async with BleakClient(address) as client:
await client.write_gatt_char(
TIME_SYNC_CHAR_UUID,
payload,
response=True,
)
print("time sync write done")
if __name__ == "__main__":
asyncio.run(main("XX:XX:XX:XX:XX:XX"))
```
说明:
- `struct.pack("<BBhQI", ...)` 对应协议定义的 little-endian 格式。
- `timezone_min=480` 表示 UTC+8。
- `response=True` 表示使用带响应写入。
## 8. 常见错误与排查
### 8.1 写入被拒绝
常见原因:
- 还没有完成配对/加密
- 写入长度不是 16 字节
- `version != 1`
- `flags` 未设置 `TIMEZONE_VALID`
- 使用了 prepare write / long write
### 8.2 写入成功但设备时间没更新
优先检查:
- 设备日志是否出现 `Accepted BLE time sync ...`
- 设备日志是否出现 `Time synchronized src=1 ...`
- 上位机是否错误地把本地时间直接当成 UTC 写入
### 8.3 时区显示错误
最常见原因:
- 主机把“本地毫秒时间戳”写进了 `utc_ms`
- 同时又传了 `timezone_min`
正确做法是:
- `utc_ms` 始终写 UTC 毫秒
- 本地时区单独放在 `timezone_min`
## 9. ATT 错误码语义
设备端当前会返回的典型 ATT 错误如下:
- `BT_ATT_ERR_UNLIKELY`
- 模块尚未 ready例如 BLE 栈或 `time_manager` 还未初始化完成
- `BT_ATT_ERR_INVALID_OFFSET`
- 非零 offset 写入
- `BT_ATT_ERR_ATTRIBUTE_NOT_LONG`
- 使用 prepare write / 长写流程
- `BT_ATT_ERR_VALUE_NOT_ALLOWED`
- payload 长度、版本、flags 或字段内容不合法
## 10. 当前服务边界
当前版本仅支持:
- 主机 -> 设备 单向校时
当前不支持:
- BLE 读回当前设备时间
- 校时结果通知
- 历史同步记录查询
- DST 单独字段
- 通过 payload 指定同步来源
同步来源在设备端固定记为:
- `TIME_SYNC_SOURCE_BLE`
## 11. 对接建议
对 PC 上位机实现建议如下:
- 首选带响应写入,不要默认用 write without response
- 时间戳统一使用 UTC 毫秒
- 时区单独使用分钟偏移
- 每次建立加密连接后可主动同步一次时间
- 若 PC 有系统授时状态,可把估计精度填入 `accuracy_ms`

View File

@@ -0,0 +1,314 @@
# Bluetooth SIG 规格页中的 BLE Profile 汇总
更新时间2026-04-01Asia/Hong_Kong
## 1. 说明与筛选口径
本文基于 Bluetooth SIG 官方规格索引页 `https://www.bluetooth.com/specifications/specs/` 当前处于 `Adopted` 状态的条目进行筛选,只保留运行在 Bluetooth Low Energy 体系上的 Profile包括
- 基于 GATT 的传统 BLE Profile
- 基于 LE Audio 的音频类 Profile
- 基于 Bluetooth Mesh / NLC 的 Profile
本文中的“详细说明”不是官方规范原文摘录,也不是对规范条款的逐句转述,而是我基于 BLE/GATT、LE Audio、Mesh/NLC 的通用工程记忆做的高层总结。用于快速建立全局认知和做架构预判是合适的;如果要做互操作或拿来做 BQB/QDID 级别实现,仍应回到对应规范逐条核对。
本次共纳入 **59** 个 Profile。
## 2. 总表
| Profile | 最新版本 | 类别 | 官方条目 |
| --- | --- | --- | --- |
| Alert Notification Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/alert-notification-profile-1-0/ |
| Ambient Light Sensor NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/ambient-light-sensor-nlc-profile-1-0-1/ |
| Asset Tracking Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/asset-tracking-profile-1-0/ |
| Authorization Control Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/authorization-control-profile-1-0/ |
| Automation IO Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/automation-io-profile-1-0/ |
| Basic Audio Profile | 1.0.2 | LE Audio | https://www.bluetooth.com/specifications/specs/basic-audio-profile-1-0-2/ |
| Basic Lightness Controller NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/basic-lightness-controller-nlc-profile-1-0-1/ |
| Basic Scene Selector NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/basic-scene-selector-nlc-profile-1-0-1/ |
| Binary Sensor Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/binary-sensor-profile-1-0/ |
| Blood Pressure Profile | 1.1.1 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/blood-pressure-profile-1-1-1/ |
| Calendar Tasks and Notes Profile | 1.0.1 | 核心/GATT | https://www.bluetooth.com/specifications/specs/calendar-tasks-and-notes-profile-1-0-1/ |
| Call Control Profile | 1.0 | LE Audio | https://www.bluetooth.com/specifications/specs/call-control-profile-1-0/ |
| Common Audio Profile | 1.0.1 | LE Audio | https://www.bluetooth.com/specifications/specs/common-audio-profile-1-0-1/ |
| Continuous Glucose Monitoring Profile | 1.0.2 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/continuous-glucose-monitoring-profile-1-0-2/ |
| Cookware Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/cookware-profile-1-0/ |
| Coordinated Set Identification Profile | 1.1 | LE Audio | https://www.bluetooth.com/specifications/specs/coordinated-set-identification-profile-1-1/ |
| Cycling Power Profile | 1.1.1 | 运动/定位 | https://www.bluetooth.com/specifications/specs/cycling-power-profile-1-1-1/ |
| Cycling Speed and Cadence Profile | 1.0.1 | 运动/定位 | https://www.bluetooth.com/specifications/specs/cycling-speed-and-cadence-profile/ |
| Device Time Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/device-time-profile-1-0/ |
| Dimming Control NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/dimming-control-nlc-profile-1-0-1/ |
| Electronic Shelf Label Profile | 1.0.1 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/electronic-shelf-label-profile-1-0-1/ |
| Emergency Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/emergency-profile-1-0/ |
| Energy Monitor NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/energy-monitor-nlc-profile1-0-1/ |
| Environmental Sensing Profile | 1.0.1 | 核心/GATT | https://www.bluetooth.com/specifications/specs/environmental-sensing-profile-1-0-1/ |
| Find Me Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/find-me-profile-1-0/ |
| Fitness Machine Profile | 1.0.1 | 运动/定位 | https://www.bluetooth.com/specifications/specs/fitness-machine-profile-1-0-1/ |
| Gaming Audio Profile | 1.0.1 | LE Audio | https://www.bluetooth.com/specifications/specs/gaming-audio-profile-4/ |
| Generic Health Sensor Profile | 1.0 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/generic-health-sensor-profile/ |
| Global Navigation Satellite System Profile | 1.0 | 运动/定位 | https://www.bluetooth.com/specifications/specs/global-navigation-satellite-system-profile-1-0/ |
| Glucose Profile | 1.0.1 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/glucose-profile-1-0-1/ |
| Health Thermometer Profile | 1.0 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/health-thermometer-profile-1-0/ |
| Hearing Access Profile | 1.0.1 | LE Audio | https://www.bluetooth.com/specifications/specs/hearing-access-profile-1-0-1/ |
| Heart Rate Profile | 1.0 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/heart-rate-profile-1-0/ |
| HID Over GATT Profile | 1.1 | 核心/GATT | https://www.bluetooth.com/specifications/specs/hid-over-gatt-profile/ |
| HVAC Integration NLC Profile | 1.0 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/hvac-integration-nlc-profile/ |
| Industrial Measurement Device Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/industrial-measurement-device-profile-1-0/ |
| Insulin Delivery Profile | 1.0.2 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/insulin-delivery-profile-1-0-2/ |
| Internet Protocol Support Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/internet-protocol-support-profile-1-0/ |
| Location and Navigation Profile | 1.0.1 | 运动/定位 | https://www.bluetooth.com/specifications/specs/location-and-navigation-profile-1-0-1/ |
| Media Control Profile | 1.0 | LE Audio | https://www.bluetooth.com/specifications/specs/media-control-profile/ |
| Mesh Configuration Database Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/mesh-configuration-database-profile-1-0-1/ |
| Mesh Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/mesh-profile-1-0-1/ |
| Microphone Control Profile | 1.0 | LE Audio | https://www.bluetooth.com/specifications/specs/microphone-control-profile-1-0/ |
| Object Transfer Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/object-transfer-profile-1-0/ |
| Occupancy Sensor NLC Profile | 1.0.1 | Mesh/NLC | https://www.bluetooth.com/specifications/specs/occupancy-sensor-nlc-profile-1-0-1/ |
| Phone Alert Status Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/phone-alert-status-profile-1-0/ |
| Physical Activity Monitor Profile | 1.0 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/physical-activity-monitor-profile-1-0/ |
| Proximity Profile | 1.0.1 | 核心/GATT | https://www.bluetooth.com/specifications/specs/proximity-profile-1-0-1/ |
| Public Broadcast Profile | 1.0.2 | LE Audio | https://www.bluetooth.com/specifications/specs/public-broadcast-profile-1-0-2/ |
| Pulse Oximeter Profile | 1.0.1 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/pulse-oximeter-profile-1-0-1/ |
| Ranging Profile | 1.0 | 商业/工业/定位 | https://www.bluetooth.com/specifications/specs/ranging-profile-1-0/ |
| Reconnection Configuration Profile | 1.0.1 | LE Audio | https://www.bluetooth.com/specifications/specs/reconnection-configuration-profile-1-0-1/ |
| Running Speed and Cadence Profile | 1.0.1 | 运动/定位 | https://www.bluetooth.com/specifications/specs/running-speed-and-cadence-profile-1-0-1/ |
| Scan Parameters Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/scan-parameters-profile-1-0/ |
| Telephony and Media Audio Profile | 1.0.1 | LE Audio | https://www.bluetooth.com/specifications/specs/telephony-and-media-audio-profile-1-0-1/ |
| Time Profile | 1.0 | 核心/GATT | https://www.bluetooth.com/specifications/specs/time-profile-1-0/ |
| Voice Assistant Profile | 1.0 | LE Audio | https://www.bluetooth.com/specifications/specs/voice-assistant-profile-1-0/ |
| Volume Control Profile | 1.0 | LE Audio | https://www.bluetooth.com/specifications/specs/volume-control-profile-1-0/ |
| Weight Scale Profile | 1.0.1 | 健康/医疗 | https://www.bluetooth.com/specifications/specs/weight-scale-profile-1-0-1/ |
## 3. 逐项详细说明
### 3.1 核心 / GATT 类
#### Alert Notification Profile
这是一个把“手机或主设备上的提醒事件”标准化转给外设的 Profile常见目标是手表、腕带、桌面提醒器之类的低功耗接收端。它关注的是类别化通知、已读状态、未读计数、立即提醒这类轻量信息而不是完整消息正文同步。工程上它适合做“提醒镜像”不适合做完整 IM 客户端。
#### Automation IO Profile
这个 Profile 面向简单工业控制和楼宇场景,核心思想是把离散输入、离散输出、模拟量等基础 I/O 点位做成统一抽象。它适合开关量、继电器、按钮、状态量等低复杂度控制接口,不强调复杂控制逻辑本身。工程上常被当作 BLE 版“轻量远程 I/O 面板”。
#### Calendar Tasks and Notes Profile
它的定位是让外设以标准方式访问日历、待办和笔记这类个人信息对象。和“通知”类 Profile 相比,它更偏对象化数据同步,强调项目集合、元数据、内容条目和访问流程。适用场景通常是可穿戴设备、车载设备或桌面配件做轻量 PIM 同步。
#### Device Time Profile
这个 Profile 的重点是设备侧时间基准管理适合让一个时间主设备给多个从设备做校时。它通常关心设备当前时间、时区、DST 或更新时间来源等信息。工程上它比通用的 Time Profile 更偏“设备维护”视角。
#### Environmental Sensing Profile
这是 BLE 里很常见的环境类 Profile覆盖温度、湿度、气压、露点、风速、空气质量等大量环境传感字段。它的价值在于给环境监测节点定义统一组织方式减少私有 GATT 设计。做传感器平台时,它通常是优先考虑的标准基线。
#### Find Me Profile
这是最早一批 BLE Profile 之一,场景非常直接:主设备触发外设发声、闪灯或震动,从而“找回”钥匙扣、标签或其他小设备。它的交互非常简单,通常没有复杂数据通道。工程上它偏“单向触发”,不是精确定位方案。
#### HID Over GATT Profile
HOGP 是 BLE 键盘、鼠标、遥控器、触控设备最核心的标准 Profile 之一。它把传统 HID 语义映射到 GATT让低功耗输入设备能和主机系统直接互通并支持报告模式、协议模式、Boot 兼容路径等。对键盘项目来说,它通常就是“标准输入外设”的第一选择。
#### Internet Protocol Support Profile
这个 Profile 的意义在于让 BLE 不只是传属性值,还能承载 IPv6/6LoWPAN 之类的上层网络协议。它面向的是更偏物联网基础设施的场景,而不是简单外设控制。工程上只有在你明确需要把设备纳入 IP 网络模型时才值得采用。
#### Object Transfer Profile
OTP 面向“对象”而不是“单个特征值”,适合在 BLE 上搬运图片、固件片段、记录文件、联系人卡片等具备元数据和可寻址性的内容。它通常依赖对象目录、对象元信息、读写偏移、创建删除等对象管理能力。和自定义长特征值相比,它更规范,但实现复杂度也更高。
#### Phone Alert Status Profile
PASP 主要用于把手机侧“来电铃声、振动、静音、提醒状态”这类手机告警状态同步给外设。它和 Alert Notification Profile 关系很近,但更强调电话和提醒设备状态而不是通知明细本身。典型设备是手环、腕表和桌面提醒器。
#### Proximity Profile
Proximity Profile 更关注链路“靠近/远离”带来的行为,比如距离太远时报警、靠近后自动静音或恢复。它通常依赖发射功率与链路损耗这类简单近远估计,不等同于现代高精度测距。工程上可以把它理解为 Find Me 的“距离关联版”。
#### Scan Parameters Profile
这个 Profile 主要用来让扫描方和被扫描方就扫描参数做一定程度的协同,目标是兼顾发现时延和功耗。它是很早期 BLE 生态为手机与外设配合而定义的补充机制。现代系统里它未必总是显性出现,但在兼容旧实现时仍值得知道。
#### Time Profile
Time Profile 是 BLE 时间同步体系的通用基线,关注当前时间、精度、参考时间来源以及时间更新。它适合需要基本时钟同步但又不想自定义时间服务的设备。和 Device Time Profile 相比,它更像“通用时间语义”的上层封装。
### 3.2 健康 / 医疗类
#### Blood Pressure Profile
这个 Profile 面向电子血压计,核心数据通常是收缩压、舒张压、平均动脉压、测量时间戳和脉搏等。它的价值在于让手机健康应用、家庭医疗网关和血压计之间具备一致的数据模型。工程上应特别注意单位、时序和一条记录的完整性。
#### Continuous Glucose Monitoring Profile
CGM Profile 面向连续血糖监测设备,强调连续采样、趋势、会话状态和告警管理。它比传统 Glucose Profile 更适合长时间持续数据流和设备状态追踪。医疗属性很强,实现时通常还要同时考虑记录缓存、断线补传和告警可靠性。
#### Generic Health Sensor Profile
从命名和生态位置看,它是为了给大量新型健康传感器提供一个更泛化的标准外壳,避免每类新传感器都重新造一套 Profile。它适合指标多样但又不值得为单一指标单独立 Profile 的健康设备。工程上可把它看作医疗/健康类的“可扩展通用框架”。
#### Glucose Profile
Glucose Profile 更偏间歇式测量设备,例如指尖采血血糖仪。它通常围绕测量记录、上下文信息、时间戳和历史数据访问来组织。和 CGM 相比,它不强调连续流,而更强调离散测量记录管理。
#### Health Thermometer Profile
这个 Profile 用于体温计或温度类健康设备,重点是体温测量值、测量部位、时间戳等。它在家庭健康设备中很常见,互操作也比较成熟。工程实现通常不复杂,但要处理好单位、一次性测量与持续测量模式的区分。
#### Heart Rate Profile
这是最常见的 BLE 健康/运动 Profile 之一,面向心率带、手环、运动设备。它核心传的是实时心率值,并可包含能量消耗或 RR-Interval 等附加信息。因为支持广、实现轻,很多平台把它当成 BLE 互操作示范案例。
#### Insulin Delivery Profile
这个 Profile 面向胰岛素泵或给药相关设备,重点是输注状态、历史记录、治疗参数和控制安全性。它比一般传感器 Profile 更强调状态机和风险控制语义。工程上要把“数据展示”和“可导致治疗动作的控制”严格分层。
#### Physical Activity Monitor Profile
它覆盖日常活动跟踪场景,如步数、活动时长、活动等级、消耗估算等。相比 Heart Rate 这类单一生理信号,它更偏行为统计与日常健康监测。适合手环、健康贴片、轻量可穿戴。
#### Pulse Oximeter Profile
这个 Profile 主要用于脉搏血氧设备,核心指标是 SpO2、脉率以及相关上下文。它在家用健康、睡眠监测和康复设备中很常见。工程上要区分瞬时显示值和可归档记录值避免 UI 和记录逻辑混淆。
#### Weight Scale Profile
Weight Scale Profile 面向体重秤和身体成分相关设备,基础数据是体重,也可带 BMI、身高、时间戳等信息。它的价值在于手机健康应用能直接理解标准数据而不需要厂商私有解析。对消费级产品来说这个 Profile 的生态成熟度很高。
### 3.3 运动 / 定位类
#### Cycling Power Profile
面向功率计、自行车台、训练设备,核心是功率值以及踏频、扭矩、左右平衡、曲线段等扩展信息。它属于运动传感里实现要求较高的一类,因为用户往往关心高刷新率和训练准确性。做骑行训练生态时,这个 Profile 很关键。
#### Cycling Speed and Cadence Profile
这个 Profile 用于车轮速度和踏频传感器,数据模型比功率计更轻。它非常适合电池供电的低功耗传感器做长续航设计。常见场景是码表、运动表、骑行 App 与车轮/踏频传感器互联。
#### Fitness Machine Profile
FTMP 面向跑步机、动感单车、划船机、椭圆机等健身器材,既包含器材上报,也允许控制类交互。它的意义是把“训练设备”从单纯传感器提升到可配置、可联动的训练终端。工程上需要特别留意控制权限和状态一致性。
#### Global Navigation Satellite System Profile
这个 Profile 面向 GNSS 接收设备,把位置、速度、卫星状态或导航相关信息以标准方式暴露给对端。它适合外置卫星定位模块、运动设备或定位记录器。工程上常见用途是把 GNSS 功能从主机中解耦到独立 BLE 外设。
#### Location and Navigation Profile
这个 Profile 是通用位置/导航数据封装,通常涵盖位置、速度、航向、海拔和导航点信息。它比单纯 GNSS 更高层,能容纳更多导航语义。适用于户外设备、船舶设备、骑行/跑步导航附件。
#### Running Speed and Cadence Profile
RSC Profile 面向跑步脚环、鞋夹或可穿戴传感器,关注跑速、步频、步长、跑/走状态等。它和骑行速度/踏频 Profile 类似,属于低功耗、低带宽但很实用的运动类标准。对跑步生态兼容性来说,它是基础件。
### 3.4 LE Audio 类
#### Basic Audio Profile
BAP 是 LE Audio 体系里的基础音频传输 Profile围绕单播/广播音频流、流配置和基本音频能力组织。可以把它理解为 LE Audio 的“音频承载基石”。很多更上层的音频 Profile 都会依赖它提供的流控制和能力表达。
#### Call Control Profile
CCP 用来表达通话相关控制语义,比如接听、挂断、保持、来电状态变化等。它的目的不是定义音频编解码,而是把“通话控制面”标准化。对耳机、免提设备、车载设备来说,它是电话业务的关键拼图。
#### Common Audio Profile
CAP 更像 LE Audio 各类设备协同行为的公共约束层,定义一组通用角色和组合方式,避免不同音频 Profile 各自形成孤岛。它通常与 CSIP、BAP 等配合使用。工程上可把它看成 LE Audio 设备行为一致性的“总装配规则”。
#### Coordinated Set Identification Profile
CSIP 用来把多个独立设备组织成一个协调集合,典型例子就是左右耳耳机、双扬声器或多设备音频系统。它解决的问题是“多个物理设备如何在逻辑上被识别为一组”。没有它,多设备配对、切换和同步体验会明显变差。
#### Gaming Audio Profile
从命名和 LE Audio 位置看,它面向游戏场景的低时延、双向音频和设备控制协同。它通常比通用媒体播放更强调时延预算、语音聊天和交互响应。工程上适合游戏耳机、掌机配件、无线麦克风方案。
#### Hearing Access Profile
HAP 面向助听器、听辅设备及其控制终端,重点是接入、控制、状态读取以及个性化听力功能配合。它是 LE Audio 在医疗辅助听力方向的重要接口层。实现时要特别关注可靠性、低时延和多设备协同。
#### Media Control Profile
MCP 把媒体播放控制抽象成标准接口,例如播放、暂停、下一曲、上一曲、快进、元数据查看等。它解决的是控制面统一,不直接替代音频流承载本身。对耳机、遥控器、车载控制面板都很有价值。
#### Microphone Control Profile
这个 Profile 聚焦麦克风相关控制,最典型的是静音/取消静音以及麦克风状态同步。它常用于耳机、会议设备、采集配件和语音终端。虽然看起来简单,但在多端协同和 UI 同步上很有必要。
#### Public Broadcast Profile
PBP 是 Auracast/Public Broadcast 体系中的关键 Profile解决的是公开广播音频如何被发现、识别和接入。它偏广播音频的“可发现性与可消费性”约束而不只是裸流传输。适合商场、机场、影院、公共导览等大范围音频分发场景。
#### Reconnection Configuration Profile
从命名和音频生态位置看,这个 Profile 用来描述设备断开后如何重连、优先重连谁、何时自动恢复等重连策略。它的目的通常是改善多主机、多耳塞、多场景切换时的用户体验。工程上它属于“体验型 Profile”对 TWS 这类设备尤其重要。
#### Telephony and Media Audio Profile
TMAP 是 LE Audio 设备在电话与媒体两大主场景中的角色组合规范。它更多定义“设备是什么、在这个场景里应该怎么协同”,而不是单一控制命令。可以把它理解为 LE Audio 终端类型与用例映射。
#### Voice Assistant Profile
从生态定位看VAP 面向语音助手接入场景,让耳机或外设能标准化启动助手会话、处理语音交互路径和相关状态。它通常和麦克风控制、音频流控制、设备角色协同使用。适用场景是耳机呼出助手、可穿戴语音入口、车载语音终端。
#### Volume Control Profile
VCP 负责音量控制与状态同步,包括绝对音量、音量步进、静音状态等。它的价值是让多类 LE Audio 终端对音量行为有一致表达。工程上如果你不希望每个设备都做私有音量协议,它是很自然的选择。
### 3.5 商业 / 工业 / 定位类
#### Asset Tracking Profile
这个 Profile 面向资产标签与定位基础设施,典型场景是仓储、零售、医院、物流等环境中的物品追踪。它通常会和广播、周期广播、方向查找或其他定位能力协作,而不是只靠一个静态 GATT 表。工程上它强调“标签、定位器、后台系统”三者的协同。
#### Authorization Control Profile
从命名看,它用于标准化“授权对象”和“访问控制状态”的 BLE 交互,适合门锁、闸机、设备启停授权、数字钥匙等场景。它的重点更像权限校验和授权流程,而不是通用传感。实现时安全边界、凭证生命周期和离线行为都应优先设计。
#### Binary Sensor Profile
这是一个很实用但语义非常克制的 Profile用来表达开/关、在/不在、触发/未触发、占用/未占用这类二值状态。它适合门磁、按钮、限位、干接点、简单安防触发器等设备。它的优势是简单、低功耗、互操作成本低。
#### Cookware Profile
从命名和家电生态位置看,它面向智能锅具、烹饪容器或配套温控设备。核心信息大概率围绕锅具温度、烹饪阶段、加热状态、定时和食材完成度提示。它适合做厨房配件的标准化接口,但实际产品里常仍会叠加厂商私有逻辑。
#### Electronic Shelf Label Profile
ESL Profile 是零售领域非常明确的商业场景标准,用于价签设备和控制基础设施之间的低功耗、批量、可靠更新。它强调群组调度、同步刷新、超低功耗待机和门店级大规模部署能力。对商超场景来说,这是 BLE 向“基础设施协议”扩展的代表作。
#### Emergency Profile
从名称判断,它服务于紧急告警、求助、事件上报和联动响应这类场景,例如老人监护、工业告警、紧急按钮设备。它通常不只是传一个布尔值,更强调事件等级、状态和响应流程。工程实现时要把“告警一定要送达”的可靠性目标单独考虑。
#### Industrial Measurement Device Profile
这个 Profile 面向工业测量设备,例如压力表、流量计、过程测量节点、便携仪表等。它的目标是让工业现场数据采集具备统一结构,而不是每家仪表都自定义私有 GATT。对工业场景来说它比消费电子 Profile 更强调量测语义和系统集成。
#### Ranging Profile
Ranging Profile 是 BLE 进入高精度距离测量后的关键 Profile核心目的就是把测距流程、角色和结果表达标准化。它通常适用于数字钥匙、物体接近判断、室内精细定位和安全接入场景。和早期 Proximity Profile 相比,它更接近“精确距离感知”而不是粗略远近估计。
### 3.6 Mesh / NLC 类
#### Mesh Configuration Database Profile
这个 Profile 更偏管理面,关注 Mesh 网络的配置数据库如何表达、交换和持久化。它不是普通终端用户直接感知的功能,而是配置工具、网关、后台系统之间保持网络一致性的基础。工程上它有助于把 Mesh 网络从“临时配网”提升为“可运维系统”。
#### Mesh Profile
Mesh Profile 是 Bluetooth Mesh 的核心定义了节点、消息中继、发布订阅、低功耗节点、Friend、代理等网络行为。虽然它不是传统意义上的单设备 GATT Profile但它的承载底座仍是 Bluetooth LE 广播和相关承载机制,所以应纳入 BLE 体系讨论。对照明、楼宇和大规模传感网络来说,它是总根。
#### Ambient Light Sensor NLC Profile
这个 NLC Profile 面向网络化照明控制里的环境光传感器节点。它把环境照度读数纳入 NLC 语义,使照明系统能做日光补偿、节能调光和场景联动。适合办公室、商业照明和智能楼宇。
#### Basic Lightness Controller NLC Profile
它对应最基础的亮度控制器角色,强调“以标准化方式控制灯光亮度”。和底层 Mesh Model 相比NLC Profile 给的是更高层、面向成品设备的约束组合。工程上它降低了不同厂商控制面板和灯具的对接成本。
#### Basic Scene Selector NLC Profile
这个 Profile 用来标准化场景选择器,例如面板上的“会议模式、演示模式、离开模式”按钮。它适合把复杂灯光/楼宇状态打包成少量场景入口。对用户交互而言,它比直接调模型参数友好得多。
#### Dimming Control NLC Profile
这个 Profile 更强调连续调光过程和控制行为,比单纯的“设定亮度值”更贴近真实调光器交互。它适合旋钮面板、滑条、墙控设备等。工程上通常和灯光控制器、亮度控制逻辑配合使用。
#### Energy Monitor NLC Profile
它把能耗监测设备纳入 NLC 语义,可用于照明回路或楼宇设备的电能数据采集。场景包括能耗可视化、节能优化和维护分析。它的价值在于让照明系统不只“能控”,还能“可度量”。
#### HVAC Integration NLC Profile
这个 Profile 体现了 NLC 从照明向楼宇环境系统扩展的趋势,用于暖通空调相关设备和照明/楼宇控制系统互联。它适合温控器、风机盘管接口、环境联动面板等设备。工程上它更强调跨子系统协同,而不只是灯控本身。
#### Occupancy Sensor NLC Profile
这个 Profile 面向人体存在/占用检测节点,是楼宇自动化里非常核心的传感输入。它常用于自动开灯、延时关灯、空调节能、会议室占用判断。和 Binary Sensor 相比,它位于 NLC 生态内部,强调与照明/楼宇场景联动。
## 4. 未纳入的网页条目
Bluetooth SIG 规格页里还存在不少名称中带有 `Profile` 的条目,但它们不属于 BLE 体系,或者主要是 BR/EDR / Classic Bluetooth 方向,因此本次没有纳入,例如:
- 3D Synchronization Profile
- A/V Remote Control Profile
- Advanced Audio Distribution Profile
- Basic Imaging Profile
- Basic Printing Profile
- BR/EDR Connection Handover Profile
- Device Identification Profile
- Dial-Up Networking Profile
- File Transfer Profile
- Generic A/V Distribution Profile
- Generic Object Exchange Profile
- Generic PIM Profile
- Hands-Free Profile
- Hardcopy Cable Replacement Profile
- Headset Profile
- Health Device Profile
- Human Interface Device Profile
- Message Access Profile
- Multi Profile Specification
- Object Push Profile
- Personal Area Networking Profile
- Phone Book Access Profile
- Serial Port Profile
- SIM Access Profile
- Synchronization Profile
- Video Distribution Profile
说明:其中个别条目可能在现代产品里和 LE 设备共存,但其规范本体并不是“基于 BLE 的 Profile”所以这里按“协议归属”而不是“产品是否可能双模”来排除。
## 5. 对 `new_kbd` 项目的直接参考价值
如果你的项目是键盘/输入设备,最直接相关的是:
- `HID Over GATT Profile`BLE 键盘主标准
- `Battery Service``Device Information Service``Bonding/Security` 等虽然不在本文 Profile 汇总内,但会和 HOGP 一起构成实际产品骨架
- 如果后续要做查找键盘、靠近告警,可参考 `Find Me Profile` / `Proximity Profile`
- 如果要做文件、配置对象同步,可看 `Object Transfer Profile` 的对象化思路,但一般键盘产品没必要直接上完整 OTP
如果要继续,我可以在下一步再补一份“**和 BLE 键盘最相关的 Profile / Service / Characteristic 清单**”,直接映射到你当前 `new_kbd` 工程的实现关注点。

View File

@@ -0,0 +1,791 @@
# 基于 Zephyr 的 LVGL 移植说明
## 1. 目标
本文档记录 `new_kbd` 项目在 `atguigu_mini_keyboard/nrf52840` 板卡上调通 LCD 与 LVGL 的过程,重点覆盖以下内容:
- 板级设备树需要满足的显示与背光条件
- 如何先用最基础的 Zephyr `display` 子系统验证硬件链路
- 如何切回 Zephyr 自带的 LVGL 适配层
- 当前项目已经落地的关键配置
- 常见问题与排查顺序
本文档对应的工程位置:
- 应用目录: `E:\projects\new_kbd`
- 板卡目录: `E:\extra\boards\atguigu\atguigu_mini_keyboard`
---
## 2. 硬件链路确认
当前板卡上的显示相关资源如下:
- 面板驱动: `sitronix,st7789v`
- 总线: `mipi-dbi-spi`
- 显示节点: `st7789v3`
- 背光: `pwm_leds/backlight`
- 背光 PWM 输出: `pwm0`
板级 DTS 中的关键节点在:
- [atguigu_mini_keyboard.dts](E:\extra\boards\atguigu\atguigu_mini_keyboard\atguigu_mini_keyboard.dts)
- [atguigu_mini_keyboard-pinctrl.dtsi](E:\extra\boards\atguigu\atguigu_mini_keyboard\atguigu_mini_keyboard-pinctrl.dtsi)
当前已经确认:
- 屏幕本身可以通过 Zephyr `display_write()` 正常显示颜色切换画面
- 背光不是普通 GPIO 开关,而是 `pwm-leds` 设备
- `p0.13` 与当前 LCD 使能无关,已经从显示路径里移除
这意味着:
- SPI/MIPI DBI 链路是通的
- ST7789V 初始化参数至少在当前板上可用
- 背光控制链路是通的
- 后续如果 LVGL 出现黑屏,优先怀疑的是 LVGL 初始化、线程模型或对象刷新,而不是底层显示驱动
---
## 3. 设备树要求
应用侧 overlay 在:
- [app.overlay](E:\projects\new_kbd\app.overlay)
当前与显示相关的关键要求有三条:
### 3.1 选择显示设备
必须在 `chosen` 中指定:
```dts
/ {
chosen {
zephyr,display = &st7789v3;
};
};
```
否则 Zephyr 的 `display` 子系统和 LVGL 模块都找不到目标显示设备。
### 3.2 打开显示控制器和面板节点
应用 overlay 中需要显式把下面这些节点设为 `okay`:
```dts
&spi2 {
status = "okay";
};
&mipi_dbi {
status = "okay";
};
&st7789v3 {
status = "okay";
};
```
### 3.3 打开 PWM 背光
当前背光不是由显示驱动自动管理,而是走 LED/PWM 子系统,所以还需要:
```dts
/ {
aliases {
backlight = &backlight;
};
};
&pwm_leds {
status = "okay";
};
&pwm0 {
status = "okay";
};
```
这样应用代码里可以通过 `LED_DT_SPEC_GET(DT_NODELABEL(backlight))` 拿到背光设备,再调用 `led_set_brightness_dt()` 设亮度。
---
## 4. 先验证裸显示,再切回 LVGL
这次移植采用了两阶段策略:
### 4.1 第一阶段: 先验证 Zephyr `display` 子系统
在这个阶段,应用不启用 LVGL而是直接调用:
- `display_get_capabilities()`
- `display_blanking_off()`
- `display_write()`
通过 RGB565 彩条整屏写入,确认:
- 面板已经真正收到像素数据
- 刷新链路不是空转
- 背光与显示内容是解耦的
这个阶段已经验证成功,屏幕可以正常显示颜色切换画面。
### 4.2 第二阶段: 切回 Zephyr 自带 LVGL 适配层
在确认底层链路没问题后,再恢复 LVGL。这样如果再次黑屏就可以把排查范围收缩到:
- LVGL 是否真的初始化成功
- UI 是否在正确线程创建
- 对象是否被正确刷新
- `display_blanking_off()` 与背光使能时机是否正确
这个两阶段方法是这次调试中最关键的分界线。
---
## 5. 当前 LVGL 方案
当前项目使用的是 Zephyr 自带的 LVGL 移植,不再手写 `lv_timer_handler()` 主循环。
相关应用代码在:
- [display_module.c](E:\projects\new_kbd\src\modules\display_module.c)
当前方案要点如下:
### 5.1 使用 Zephyr 自带的 LVGL 自动初始化
依赖 `CONFIG_LV_Z_AUTO_INIT=y`,由 Zephyr 在应用启动前完成:
- `lv_init()`
- display 注册
- 渲染缓冲区创建
- LVGL 核心初始化
因此应用层不需要再手动调用 `lvgl_init()`
### 5.2 使用 Zephyr 的 LVGL workqueue
依赖 `CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y`
这样 LVGL 核心 `lv_timer_handler()` 由 Zephyr 模块自己调度,应用层不再手动驱动 LVGL 时钟。
应用里如果还需要定期更新 UI当前做法是:
- 自己维护一个 `k_work_delayable`
- 通过 `lvgl_get_workqueue()` 把这个 work 投递到 LVGL 专用 workqueue
- 在 work 回调里 `lvgl_lock()` 后访问 LVGL API
这样做的原因是:
- 避免 UI 更新逻辑和 LVGL 核心不在同一执行上下文
- 避免对象创建、样式更新与内部刷新竞争
- 对当前板子来说,这是比“任意线程加锁直接操作 LVGL”更保守、更容易稳定的方案
### 5.3 先开背光,再创建 UI
当前时序是:
1. `display_blanking_off()`
2. `display_backlight_init()`
3. 将显示模块标记为 ready
4. 立即在 LVGL workqueue 上调度一次 UI 更新
背光初始化通过 `pwm-leds` 完成,不再依赖额外 GPIO。
### 5.4 当前 UI 验证策略
为了避免“画面有了但刚好看不见”的误判,当前 UI 使用了高对比度方案:
- 背景色在两种颜色之间切换
- 标题为 `Zephyr LVGL running`
- 计数文本为 `tick N`
如果这三项都能显示,基本可以判定 LVGL 渲染链路已经正常工作。
---
## 6. 当前关键配置
应用配置文件在:
- [prj.conf](E:\projects\new_kbd\prj.conf)
当前与 LVGL/显示相关的关键配置如下:
```conf
CONFIG_DISPLAY=y
CONFIG_MIPI_DBI=y
CONFIG_ST7789V=y
CONFIG_LVGL=y
CONFIG_LV_CONF_MINIMAL=y
CONFIG_LV_BUILD_EXAMPLES=n
CONFIG_LV_BUILD_DEMOS=n
CONFIG_LV_USE_LABEL=y
CONFIG_LV_FONT_MONTSERRAT_14=y
CONFIG_LV_Z_AUTO_INIT=y
CONFIG_LV_Z_BITS_PER_PIXEL=16
CONFIG_LV_Z_LVGL_MUTEX=y
CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y
CONFIG_LV_Z_MEM_POOL_SIZE=16384
```
各项含义:
- `CONFIG_LV_Z_BITS_PER_PIXEL=16`
与 ST7789V 当前 `RGB565` 像素格式对应,避免渲染缓冲格式不匹配
- `CONFIG_LV_Z_LVGL_MUTEX=y`
提供 `lvgl_lock()` / `lvgl_unlock()`,确保应用层访问 LVGL API 时有统一互斥保护
- `CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y`
让 Zephyr 自动维护 LVGL 核心执行循环,不需要应用手动跑 `lv_timer_handler()`
- `CONFIG_LV_Z_MEM_POOL_SIZE=16384`
作为当前最小可工作的内存池配置
---
## 7. 构建方式
当前构建已不再默认强制使用 `-Og`,而是遵从工程自身配置。
当前工程已经启用:
```conf
CONFIG_SIZE_OPTIMIZATIONS=y
```
因此默认会走 size optimization适合当前 OTA 分区大小约束。
### 7.1 当前构建 skill 的行为
`ncs-fresh-build` skill 已经调整为:
- 不再默认注入 `CONFIG_DEBUG_OPTIMIZATIONS=y`
- 默认构建目录为项目根下的 `build`
- 如果项目下已有 `build` 且带构建元数据,则执行 `resume build`
- 如果不存在,则在项目根下创建 `build`
也就是说,当前默认构建输出目录是:
- `E:\projects\new_kbd\build`
### 7.2 当前已验证的构建结果
在当前 LVGL 配置下,系统构建通过,成功产出:
- `merged.hex`
- `dfu_application.zip`
最近一次 LVGL 镜像的资源占用约为:
- FLASH: `437780 / 482816`
- RAM: `96052 / 256 KB`
说明在当前 OTA 分区布局下LVGL 版本仍然可以装下。
---
## 8. 当前显示模块代码结构
当前显示模块入口在:
- [display_module.c](E:\projects\new_kbd\src\modules\display_module.c)
整体逻辑如下:
1. 等待 `main` 模块进入 `MODULE_STATE_READY`
2. 检查显示设备是否 ready
3. 读取显示能力并打印日志
4. 调用 `display_blanking_off()`
5. 初始化背光 PWM 亮度
6. 将模块标记为 initialized
7. 把第一个 UI 更新 work 投递到 LVGL workqueue
8. 后续每秒更新一次背景与计数文本
这种结构的好处是:
- 显示模块与主应用启动顺序解耦
- LVGL 核心循环交给 Zephyr 模块维护
- 应用只负责自己的 UI 逻辑
- 以后如果要继续扩展页面,也可以沿用同样的工作队列模型
---
## 9. 常见问题与排查顺序
### 9.1 背光亮,但完全黑屏
先不要怀疑 LVGL先切回裸 `display_write()` 测试版本。
如果裸显示也黑屏,优先检查:
- `zephyr,display` 是否指向正确节点
- `spi2` / `mipi_dbi` / `st7789v3` 是否都为 `okay`
- ST7789V 初始化参数是否匹配当前屏
- 面板 offset / colmod / madctl 是否正确
### 9.2 裸 `display_write()` 正常,但 LVGL 黑屏
优先检查:
- `CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE` 是否启用
- 应用是否仍在手动调用 `lv_timer_handler()`
- UI 更新是否发生在错误线程
- 是否遗漏 `lvgl_lock()`
- 文本和背景颜色是否刚好一致
### 9.3 背光不亮
优先检查:
- `backlight` alias 是否存在
- `&pwm_leds` 是否启用
- `&pwm0` 是否启用
- `pwm0_default` 的 pinctrl 是否正确
- 应用是否真的调用了 `led_set_brightness_dt()`
### 9.4 构建能过,但镜像塞不进 OTA 分区
优先检查:
- 是否误用了 `-Og`
- `CONFIG_SIZE_OPTIMIZATIONS` 是否生效
- 是否启用了不必要的 LVGL demos/examples
- 字体、部件和日志级别是否过大
---
## 10. 后续建议
如果后续继续完善显示功能,建议按下面顺序推进:
1. 先固定当前最小可工作 UI不要同时引入太多控件
2. 增加 RTT 日志,确认 LVGL 初始化和周期更新是否稳定
3. 再逐步接入输入设备或更复杂页面
4. 最后再考虑动画、图片资源和更大的字体
对当前项目来说,最重要的经验是:
- 先把底层 `display` 写屏跑通
- 再把问题收敛到 LVGL
- 最后再处理更高层的 UI 逻辑
这样可以显著降低显示移植的排查成本。
---
## 11. ST7789V 设备树参数详细说明
当前板上显示节点位于:
- [atguigu_mini_keyboard.dts](E:\extra\boards\atguigu\atguigu_mini_keyboard\atguigu_mini_keyboard.dts)
典型配置如下:
```dts
st7789v3: st7789v@0 {
compatible = "sitronix,st7789v";
status = "disabled";
reg = <0>;
mipi-max-frequency = <32000000>;
width = <320>;
height = <172>;
x-offset = <0>;
y-offset = <34>;
vcom = <0x2b>;
gctrl = <0x35>;
vrhs = <0x11>;
vdvs = <0x20>;
mdac = <0x60>;
lcm = <0x2c>;
colmod = <0x55>;
gamma = <0x01>;
porch-param = [ 0c 0c 00 33 33 ];
cmd2en-param = [ 5a 69 02 01 ];
pwctrl1-param = [ a4 a1 ];
pvgam-param = [ d0 00 02 07 0a 28 32 44 42 06 0e 12 14 17 ];
nvgam-param = [ d0 00 02 07 0a 28 31 54 47 0e 1c 17 1b 1e ];
ram-param = [ 00 f0 ];
rgb-param = [ c0 02 14 ];
mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";
};
```
下面按类别说明这些参数的意义。
### 11.1 设备身份与总线绑定
#### `compatible = "sitronix,st7789v"`
作用:
- 告诉 Zephyr 使用 ST7789V 显示驱动
- 绑定文件来自 [sitronix,st7789v.yaml](C:\ncs\v3.2.3\zephyr\dts\bindings\display\sitronix,st7789v.yaml)
- 驱动实现位于 [display_st7789v.c](C:\ncs\v3.2.3\zephyr\drivers\display\display_st7789v.c)
如果这个 `compatible` 不对,后续所有参数即使写对也不会被正确解释。
#### `reg = <0>`
作用:
- 这是该显示器在父 `mipi_dbi` 总线上的片选号/地址索引
- 对 SPI 模式来说,它最终会映射到父 SPI 控制器 `cs-gpios` 数组中的第几个片选
当前值是 `0`,表示使用父 SPI 控制器的第 0 个 CS。
#### `mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE"`
作用:
- 指定 ST7789V 通过 MIPI DBI Type-C 的 4 线 SPI 模式工作
- 命令/数据区分不是靠 9-bit SPI而是靠单独的 `dc-gpios`
这对当前板子很重要,因为板上已经专门引出了 DC GPIO。
#### `mipi-max-frequency = <32000000>`
作用:
- 指定面板接口希望使用的最高 SPI 时钟
- 这个值会进入 `struct spi_config.frequency`
- 最终实际频率还会受底层 SPI 控制器实例上限限制
当前项目中:
- 面板这里写的是 `32 MHz`
- 底层已经把 LCD 切到 `spi3`
- `spi3` 是 nRF52840 上支持 `32 MHz` 的高速 SPIM 实例
因此这个值现在是有效的,不再像早期挂在 `spi2` 时那样被 SoC 侧 `8 MHz` 上限卡住。
---
### 11.2 分辨率与有效显示窗口
#### `width = <320>`
作用:
- Zephyr `display_get_capabilities()` 返回的水平分辨率
- LVGL 也会按这个宽度创建显示对象和渲染缓冲
在当前横屏模式下,宽度已经从竖屏时的 `172` 改为 `320`
#### `height = <172>`
作用:
- Zephyr `display_get_capabilities()` 返回的垂直分辨率
- 决定应用层看到的逻辑屏高
在当前横屏模式下,高度已经从竖屏时的 `320` 改为 `172`
#### `x-offset = <0>`
作用:
- 指定 LCD 实际可视窗口在 ST7789V GRAM 中的列偏移
- 驱动在写入时,会把应用层传入的 `x` 坐标再加上这个偏移
对应驱动代码:
- `st7789v_set_lcd_margins()`
- `st7789v_set_mem_area()`
#### `y-offset = <34>`
作用:
- 指定 LCD 实际可视窗口在 ST7789V GRAM 中的行偏移
- 驱动会把应用层传入的 `y` 坐标再加上这个偏移
为什么横屏时从 `x-offset=34` 变成 `y-offset=34`:
- 这块屏的物理可视区域不是完整的 240x320而是裁剪出来的 `172x320`
- 当通过 `mdac` 做 XY 轴交换后,原来沿 X 方向的裁剪,需要同步转移到 Y 方向
这是横屏改造里最容易遗漏的一点。如果只改 `width/height``mdac`,不改 offset画面通常会出现:
- 偏移错位
- 局部黑边
- 显示越界
---
### 11.3 方向控制
#### `mdac = <0x60>`
作用:
- 对应 ST7789V 的 `MADCTL` 寄存器,即 Memory Data Access Control
- 用于控制:
- X/Y 轴交换
- 左右镜像
- 上下镜像
- RGB/BGR 顺序
对应驱动里的寄存器命令:
- `ST7789V_CMD_MADCTL`
当前值 `0x60` 的核心意义是:
- 打开 `MV`,进行 XY 轴交换
- 再配合镜像位,把竖屏坐标系旋转为横屏
对当前项目来说,`mdac` 是“横竖屏切换的核心参数”。
如果上板后发现:
- 画面已经横过来,但方向反了
- 或左右/上下镜像不对
通常只需要继续调整 `mdac`,而不一定需要再动 `width/height`
---
### 11.4 面板电气与模拟参数
下面这几项大多属于“面板电气初始化参数”,通常来自原厂例程、模组 demo、已有验证过的初始化序列或者靠经验调通。
它们的共同特点是:
- 不建议随便改
- 改错后往往不是“完全不亮”,而是
- 偏色
- 闪烁
- 对比度差
- 稳定性差
- 局部异常
#### `vcom = <0x2b>`
作用:
- VCOM 电压相关设置
- 影响液晶驱动偏置和整体显示稳定性
常见现象:
- 值不合适时,可能出现闪烁、灰阶异常、残影或对比度不自然
#### `gctrl = <0x35>`
作用:
- Gate Control控制面板栅极驱动相关参数
它更接近面板扫描电路的底层设置,一般按已知可工作配置保留。
#### `vrhs = <0x11>`
作用:
- VRH setting电压参考相关参数之一
#### `vdvs = <0x20>`
作用:
- VDV setting和驱动电压微调相关
注意:
- 驱动里只有同时存在 `vrhs``vdvs` 时,才会开启对应的 `VDVVRHEN` 流程
#### `lcm = <0x2c>`
作用:
- LCM control面板控制相关寄存器值
一般与模组硬件特性绑定,不建议脱离原配置单独尝试。
#### `gamma = <0x01>`
作用:
- Gamma curve 选择
- 会影响亮度过渡、灰阶、色彩观感
---
### 11.5 像素格式与颜色相关
#### `colmod = <0x55>`
作用:
- 对应 `COLMOD`,即 Interface Pixel Format
- 决定总线传输的像素位宽
当前值 `0x55` 表示 `RGB565 / 16-bit` 模式,这与当前软件配置完全一致:
- `CONFIG_ST7789V_RGB565=y`
- `CONFIG_LV_Z_BITS_PER_PIXEL=16`
如果这里和软件配置不匹配,常见现象包括:
- 颜色错乱
- 红蓝交换
- 图像看起来像“马赛克”或数据错位
#### `ram-param = [ 00 f0 ]`
作用:
- 对应 `RAMCTRL`
- 控制显示 RAM 访问的一些底层行为
这类参数通常和像素格式、总线模式、厂商推荐初始化序列成组使用。
#### `rgb-param = [ c0 02 14 ]`
作用:
- 对应 `RGBCTRL`
- 设置 RGB 接口/时序相关参数
虽然当前走的是 SPI MIPI DBI不是传统 RGB 并口,但 ST7789V 仍要求这组寄存器初始化。
---
### 11.6 时序与前后肩参数
#### `porch-param = [ 0c 0c 00 33 33 ]`
作用:
- 对应 `PORCTRL`
- 设置 porch也就是显示时序中的前后肩、空白区参数
这些参数会影响:
- 帧时序稳定性
- 扫描边界
- 某些情况下的闪烁或边缘异常
这组值一般与具体模组匹配,建议保留已有验证值。
---
### 11.7 扩展命令与电源控制参数
#### `cmd2en-param = [ 5a 69 02 01 ]`
作用:
- 对应 `CMD2EN`
- 开启 ST7789V 扩展命令页
如果这一步配置错误,后面某些扩展寄存器写入可能根本不会生效。
#### `pwctrl1-param = [ a4 a1 ]`
作用:
- 对应 `PWCTRL1`
- 电源控制相关参数
这会影响面板驱动供电行为和显示稳定性。
---
### 11.8 Gamma 曲线表
#### `pvgam-param = [ d0 00 02 07 0a 28 32 44 42 06 0e 12 14 17 ]`
作用:
- 对应 `PVGAMCTRL`
- Positive Voltage Gamma Control 参数表
#### `nvgam-param = [ d0 00 02 07 0a 28 31 54 47 0e 1c 17 1b 1e ]`
作用:
- 对应 `NVGAMCTRL`
- Negative Voltage Gamma Control 参数表
这两组参数共同决定:
- 亮暗过渡
- 色调倾向
- 灰阶表现
调这些参数通常属于“画质微调”,不是基础 bring-up 阶段的首选手段。除非当前已经能稳定显示,只是观感明显不对,否则不建议优先改这里。
---
### 11.9 这些参数里最关键、最常改的是哪些
在实际移植中,最常需要改的是下面这几项:
- `width`
- `height`
- `x-offset`
- `y-offset`
- `mdac`
- `mipi-max-frequency`
- `colmod`
其中:
- `width/height/x-offset/y-offset/mdac`
主要决定方向、有效显示区域和坐标映射
- `mipi-max-frequency`
主要决定带宽上限
- `colmod`
主要决定像素格式是否与软件配置匹配
而像下面这些:
- `vcom`
- `gctrl`
- `vrhs`
- `vdvs`
- `lcm`
- `gamma`
- `porch-param`
- `cmd2en-param`
- `pwctrl1-param`
- `pvgam-param`
- `nvgam-param`
- `ram-param`
- `rgb-param`
更像是“面板模组初始化模板的一部分”,通常是在已有可工作基础上尽量保持不动。
---
### 11.10 当前项目的实用建议
对当前这块屏来说,后续如果还要继续调整显示方向或显示区域,建议按下面顺序操作:
1. 先改 `mdac`
2. 再配套检查 `width/height`
3. 最后再修 `x-offset/y-offset`
不要一上来同时改所有寄存器,否则很难判断到底是哪一项导致了:
- 图像颠倒
- 画面偏移
- 部分区域黑边
- 写入越界
对当前项目,真正应该高频修改的设备树参数,其实主要就是这 5 项:
- `width`
- `height`
- `x-offset`
- `y-offset`
- `mdac`

View File

@@ -0,0 +1,926 @@
# NCS v3.2.3 集成的 GATT Service 汇总
更新时间2026-04-01Asia/Hong_Kong
## 1. 统计口径
本文面向 `C:\ncs\v3.2.3` 代码树中**应用可直接集成**的 GATT Service。
筛选规则:
-`zephyr/include/zephyr/bluetooth/services/``nrf/include/bluetooth/services/` 中公开头文件为主
-`zephyr/subsys/bluetooth/services/``nrf/subsys/bluetooth/services/` 中实际注册 GATT 服务的实现为准
- 优先整理**服务端**能力,也就是“本机作为 Peripheral/Server 暴露 GATT Service”时的用法
- `*_client``hogp``gattp` 这类主要用于 Central 侧发现/访问远端服务的客户端辅助库,不作为本文主表主体
按这个口径,当前 SDK 中可直接拿来做本地 GATT Service 的模块主要分为两类:
- Zephyr 自带标准服务
- NCS / Nordic 扩展服务
## 2. 先看结论
### 2.1 Zephyr 自带标准服务
| Service | Kconfig | 头文件 | 典型用途 |
| --- | --- | --- | --- |
| Alert Notification Service | `CONFIG_BT_ANS` | `<zephyr/bluetooth/services/ans.h>` | 对手表/手环输出通知类别与未读数 |
| Battery Service | `CONFIG_BT_BAS` | `<zephyr/bluetooth/services/bas.h>` | 暴露电量、电池状态 |
| Current Time Service | `CONFIG_BT_CTS` | `<zephyr/bluetooth/services/cts.h>` | 给客户端提供当前时间/校时 |
| Device Information Service | `CONFIG_BT_DIS` | `<zephyr/bluetooth/services/dis.h>` | 暴露厂商、型号、版本、PnP ID 等静态信息 |
| Heart Rate Service | `CONFIG_BT_HRS` | `<zephyr/bluetooth/services/hrs.h>` | 心率设备 |
| Immediate Alert Service | `CONFIG_BT_IAS` | `<zephyr/bluetooth/services/ias.h>` | 查找设备、远程触发蜂鸣/闪灯 |
| Nordic UART ServiceZephyr 版) | `CONFIG_BT_ZEPHYR_NUS` | `<zephyr/bluetooth/services/nus.h>` | 简单双向串口透传 |
| Object Transfer Service | `CONFIG_BT_OTS` | `<zephyr/bluetooth/services/ots.h>` | 面向对象的数据传输 |
| Tx Power Service | `CONFIG_BT_TPS` | 无独立公共 API 头 | 暴露当前发射功率 |
### 2.2 NCS / Nordic 扩展服务
| Service | Kconfig | 头文件 | 典型用途 |
| --- | --- | --- | --- |
| Bond Management Service | `CONFIG_BT_BMS` | `<bluetooth/services/bms.h>` | 让对端触发删 bond |
| Continuous Glucose Monitoring Service | `CONFIG_BT_CGMS` | `<bluetooth/services/cgms.h>` | 连续血糖监测 |
| Direction and Distance Finding Service | `CONFIG_BT_DDFS` | `<bluetooth/services/ddfs.h>` | 方位/距离测量结果输出 |
| Fast Pair Provider Service | `CONFIG_BT_FAST_PAIR` + `CONFIG_BT_FAST_PAIR_GATT_SERVICE` | `<bluetooth/services/fast_pair/fast_pair.h>` | Google Fast Pair |
| Human Interface Device Service | `CONFIG_BT_HIDS` | `<bluetooth/services/hids.h>` | BLE 键盘、鼠标、输入设备 |
| Latency Service | `CONFIG_BT_LATENCY` | `<bluetooth/services/latency.h>` | 延迟测试/回环 |
| LED Button Service | `CONFIG_BT_LBS` | `<bluetooth/services/lbs.h>` | 示例级 LED/Button 交互 |
| Memfault Diagnostic Service | `CONFIG_BT_MDS` | `<bluetooth/services/mds.h>` | Memfault 诊断数据导出 |
| Nordic Status Message Service | `CONFIG_BT_NSMS` | `<bluetooth/services/nsms.h>` | 暴露一段可读状态文本 |
| Nordic UART ServiceNordic 旧版) | `CONFIG_BT_NUS` | `<bluetooth/services/nus.h>` | NCS 样例常用串口透传 |
| Ranging Service | `CONFIG_BT_RAS` + `CONFIG_BT_RAS_RRSP` | `<bluetooth/services/ras.h>` | Channel Sounding / Ranging 服务端 |
| Running Speed and Cadence Service | `CONFIG_BT_RSCS` | `<bluetooth/services/rscs.h>` | 跑步速度步频 |
| Throughput Service | `CONFIG_BT_THROUGHPUT` | `<bluetooth/services/throughput.h>` | 吞吐测试 |
| Wi-Fi Provisioning Service | `CONFIG_BT_WIFI_PROV` | `<bluetooth/services/wifi_provisioning.h>` | 通过 BLE 给设备配 Wi-Fi |
## 3. 使用方式总规律
大多数服务都遵循类似流程:
1.`prj.conf` 里打开对应 `CONFIG_BT_*`
2. 包含头文件
3. 若服务需要运行时初始化,则在 `bt_enable()` 前后调用对应 `init/register`
4. 连接建立后,通过通知/读写回调/API 更新服务数据
5. 在广告里决定是否放服务 UUID但这不是注册服务的必要条件
有三类例外要先记住:
- **纯 Kconfig 静态服务**:例如 `DIS``TPS`,启用配置后就自动挂到 GATT DB上层几乎没有运行时 API
- **宏定义式实例服务**:例如 `NSMS``Zephyr NUS` 多实例、`HIDS`,需要先用宏定义实例,再初始化或更新
- **复杂协议服务**:例如 `OTS``RAS``Fast Pair`,除了开 Kconfig还要按它自己的状态机/回调/缓冲机制接入
## 4. 逐项说明
## 4.1 Zephyr 标准服务
### 4.1.1 Alert Notification Service, `ANS`
源码位置:
- `zephyr/subsys/bluetooth/services/ans.c`
- `zephyr/include/zephyr/bluetooth/services/ans.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_ANS=y
```
怎么用:
- 先在 Kconfig 里决定支持哪些提醒类别,例如 `CONFIG_BT_ANS_NALRT_CAT_EMAIL=y`
- 运行时如果还想动态设置支持位图,可以调用 `bt_ans_set_new_alert_support_category()``bt_ans_set_unread_support_category()`
- 有新提醒时调用 `bt_ans_notify_new_alert(conn, category, num_new, text)`
- 未读数变化时调用 `bt_ans_set_unread_count(conn, category, unread)`
适合场景:
- 手环、手表、桌面提醒器
- 你只想同步“有提醒/未读数/提醒类别”,而不是完整消息正文
注意点:
- 头文件明确写了 `bt_ans_notify_new_alert()` / `bt_ans_set_unread_count()` 会拿互斥锁,**不要从 BT RX 线程或 System Workqueue 里直接调用**
- 这个服务偏“提醒镜像”,不适合做完整消息中心
### 4.1.2 Battery Service, `BAS`
源码位置:
- `zephyr/subsys/bluetooth/services/bas/`
- `zephyr/include/zephyr/bluetooth/services/bas.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_BAS=y
```
如果要扩展电池状态:
```conf
CONFIG_BT_BAS_BLS=y
CONFIG_BT_BAS_BCS=y
```
怎么用:
- 最基本只需要周期性调用 `bt_bas_set_battery_level(level)`,把 0..100 的电量同步给客户端
- 读取当前缓存值可用 `bt_bas_get_battery_level()`
- 如果启用了扩展状态特征,可继续设置:
- `bt_bas_bls_set_battery_present()`
- `bt_bas_bls_set_battery_charge_state()`
- `bt_bas_bls_set_battery_charge_level()`
- `bt_bas_bls_set_service_required()`
- `bt_bas_bls_set_battery_fault()`
适合场景:
- 键盘、鼠标、耳机、手表、遥控器等所有电池供电外设
对你当前项目的价值:
- 这是 `new_kbd` 最应该启用的标准服务之一
- 你已经有 `ble_battery_module.c`,它天然适合映射到 BAS
### 4.1.3 Current Time Service, `CTS`
源码位置:
- `zephyr/subsys/bluetooth/services/cts.c`
- `zephyr/include/zephyr/bluetooth/services/cts.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_CTS=y
```
可选工具 API
```conf
CONFIG_BT_CTS_HELPER_API=y
```
怎么用:
- 定义 `struct bt_cts_cb`
- 至少实现 `fill_current_cts_time()`,因为服务在被读或发通知时要靠它拿当前时间
- 如果允许对端写时间,再实现 `cts_time_write()`
- 如果关心客户端是否订阅时间更新,实现 `notification_changed()`
- 初始化时调用 `bt_cts_init(&cb)`
- 时间变化后调用 `bt_cts_send_notification(reason)`
最小使用思路:
1. 系统自己维护 RTC / Unix 时间
2. CTS 被读时把当前时间填进 `struct bt_cts_time_format`
3. 如果时间是由手机写入的,就在 `cts_time_write()` 里反写到系统时钟
### 4.1.4 Device Information Service, `DIS`
源码位置:
- `zephyr/subsys/bluetooth/services/dis.c`
- `zephyr/subsys/bluetooth/services/Kconfig.dis`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DIS=y
CONFIG_BT_DIS_MANUF_NAME=y
CONFIG_BT_DIS_MANUF_NAME_STR="Your Company"
CONFIG_BT_DIS_MODEL_NUMBER=y
CONFIG_BT_DIS_MODEL_NUMBER_STR="KBD-01"
CONFIG_BT_DIS_PNP=y
```
怎么用:
- `DIS` 基本是**纯配置型服务**
- 你在 `prj.conf` 里打开需要的特征值并填字符串/ID服务会静态注册
- 常用字段包括:
- 厂商名
- 型号
- 序列号
- FW/HW/SW 版本
- PnP ID
- 医疗设备 UDI
适合场景:
- 几乎所有 BLE 外设都建议启用
对键盘项目的建议:
- 至少填 `Manufacturer Name``Model Number``FW Revision`
- 如果你将来打算做 HID 认证/系统识别,`PnP ID` 也建议补齐
### 4.1.5 Heart Rate Service, `HRS`
源码位置:
- `zephyr/subsys/bluetooth/services/hrs.c`
- `zephyr/include/zephyr/bluetooth/services/hrs.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_HRS=y
```
怎么用:
- 若要处理控制点请求,定义 `struct bt_hrs_cb` 并注册 `bt_hrs_cb_register(&cb)`
- 上报新心率时调用 `bt_hrs_notify(heartrate)`
- 如果支持 Energy Expended 重置,则在 `ctrl_point_write()` 里处理 `BT_HRS_CONTROL_POINT_RESET_ENERGY_EXPANDED_REQ`
适合场景:
- 心率带、健康设备、运动设备
### 4.1.6 Immediate Alert Service, `IAS`
源码位置:
- `zephyr/subsys/bluetooth/services/ias/ias.c`
- `zephyr/include/zephyr/bluetooth/services/ias.h`
- 参考样例:`zephyr/samples/bluetooth/peripheral/src/main.c`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_IAS=y
```
怎么用:
-`BT_IAS_CB_DEFINE(name)` 定义回调对象
- 在回调里实现:
- `no_alert()`
- `mild_alert()`
- `high_alert()`
- 远端写 Alert Level 时,服务会自动回调你
- 如果本机已经在报警,且你想主动停掉当前告警,可调用 `bt_ias_local_alert_stop()`
典型逻辑:
- `mild_alert()` 里短鸣、低频闪烁
- `high_alert()` 里持续蜂鸣或高亮闪烁
- `no_alert()` 里停蜂鸣、灭灯
### 4.1.7 Nordic UART Service, `Zephyr 版 NUS`
源码位置:
- `zephyr/subsys/bluetooth/services/nus/`
- `zephyr/include/zephyr/bluetooth/services/nus.h`
- 参考样例:`zephyr/samples/bluetooth/peripheral_nus`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_ZEPHYR_NUS=y
```
可选:
```conf
CONFIG_BT_ZEPHYR_NUS_DEFAULT_INSTANCE=y
CONFIG_BT_ZEPHYR_NUS_AUTO_START_BLUETOOTH=y
```
怎么用:
- 最简单:使用默认实例
- 定义 `struct bt_nus_cb`
- 注册 `bt_nus_cb_register(&cb, ctx)`
- 对端写 RX 特征值时,你的 `received()` 回调会收到数据
- 要发数据给对端时,调用 `bt_nus_send(conn, data, len)`
高级用法:
- 这个版本支持多实例
- 你可以先用 `BT_NUS_INST_DEFINE(name)` 定义多个 NUS 端点
- 然后对每个实例调用 `bt_nus_inst_cb_register()` / `bt_nus_inst_send()`
建议:
- 新代码如果想做“可扩展串口通道”Zephyr 版 NUS 比 Nordic 旧版更现代
### 4.1.8 Object Transfer Service, `OTS`
源码位置:
- `zephyr/subsys/bluetooth/services/ots/`
- `zephyr/include/zephyr/bluetooth/services/ots.h`
- 参考样例:`zephyr/samples/bluetooth/peripheral_ots`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_OTS=y
CONFIG_BT_L2CAP_DYNAMIC_CHANNEL=y
CONFIG_BT_GATT_DYNAMIC_DB=y
CONFIG_BT_SMP=y
```
按需求补充:
```conf
CONFIG_BT_OTS_DIR_LIST_OBJ=y
CONFIG_BT_OTS_OACP_WRITE_SUPPORT=y
CONFIG_BT_OTS_OACP_PATCH_SUPPORT=y
CONFIG_BT_OTS_OACP_CREATE_SUPPORT=y
CONFIG_BT_OTS_OACP_DELETE_SUPPORT=y
```
怎么用:
1. 先拿一个实例:`bt_ots_free_instance_get()`
2.`struct bt_ots_init_param`,配置支持的 OACP/OLCP Feature 与回调
3. 调用 `bt_ots_init(ots, &ots_init)`
4. 准备对象元数据与对象数据,再用 `bt_ots_obj_add(ots, &param)` 加入对象池
5. 客户端随后可通过 OTS 读/写/选择对象
什么时候值得用:
- 需要在 BLE 上传输“对象”,例如文件、图片、日志、记录块、配置块
- 需要比单个 characteristic 更规范的元数据和对象管理
什么时候不值得用:
- 只是传几字节配置或简单串口透传时,`NUS` 或自定义服务更轻
### 4.1.9 Tx Power Service, `TPS`
源码位置:
- `zephyr/subsys/bluetooth/services/tps.c`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_TPS=y
```
怎么用:
- 这是典型的**无应用层 API**服务
- 打开 `CONFIG_BT_TPS` 后,服务会静态注册
- 客户端读取 `TX Power Level` 特征值时,底层会通过 `bt_conn_le_get_tx_power_level()` 取当前发射功率并返回
适合场景:
- 定位、调试、需要标准化暴露 TX Power 的设备
## 4.2 NCS / Nordic 扩展服务
### 4.2.1 Bond Management Service, `BMS`
源码位置:
- `nrf/subsys/bluetooth/services/bms.c`
- `nrf/include/bluetooth/services/bms.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_BMS=y
```
怎么用:
- 定义 `struct bt_bms_init_params`
- 在里面配置支持哪些删 bond 操作,以及是否需要授权码
- 如果某些删除操作要授权,实现 `struct bt_bms_cb.authorize`
- 调用 `bt_bms_init(&init_params)`
适合场景:
- 设备端希望让手机或维护工具发起“删除本机 bond”
- 做售后恢复、重新配对、共享设备切换用户
对你当前项目的价值:
- 你的键盘已经在做 bond 管理,这个服务值得评估
- 但很多键盘不会直接对外开放 BMS而是自己做按键清配对逻辑
### 4.2.2 Continuous Glucose Monitoring Service, `CGMS`
源码位置:
- `nrf/subsys/bluetooth/services/cgms/`
- `nrf/include/bluetooth/services/cgms.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_CGMS=y
```
怎么用:
-`struct bt_cgms_init_param`
- 传感器类型
- 采样位置
- session 运行时长
- 初始通信间隔
- 回调
- 调用 `bt_cgms_init(&init)`
- 每有一条新血糖测量值,就调用 `bt_cgms_measurement_add(measurement)`
适合场景:
- 连续血糖监测设备
### 4.2.3 Direction and Distance Finding Service, `DDFS`
源码位置:
- `nrf/subsys/bluetooth/services/ddfs.c`
- `nrf/include/bluetooth/services/ddfs.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DDFS=y
```
怎么用:
-`struct bt_ddfs_init_params`
- 初始 features
- `struct bt_ddfs_cb` 回调
- 调用 `bt_ddfs_init(&init)`
- 应用拿到测距/方位结果后,用以下 API 通知客户端:
- `bt_ddfs_distance_measurement_notify()`
- `bt_ddfs_azimuth_measurement_notify()`
- `bt_ddfs_elevation_measurement_notify()`
回调的意义:
- `dm_ranging_mode_set()`:对端改测距模式时通知应用
- `dm_config_read()`:对端读取配置时由应用填充配置
- `*_notification_config_changed()`:通知开关变化
适合场景:
- 方向查找、距离估计、定位试验平台
### 4.2.4 Fast Pair Provider Service, `FPS`
源码位置:
- `nrf/subsys/bluetooth/services/fast_pair/fp_gatt_service.c`
- `nrf/include/bluetooth/services/fast_pair/fast_pair.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_FAST_PAIR=y
CONFIG_BT_FAST_PAIR_GATT_SERVICE=y
```
怎么用:
- 这不是通用 GATT 服务,而是 Google Fast Pair 生态专用服务
- 正常接入方式不是自己操作 characteristic而是走高层 API
- 典型流程:
1. `bt_enable()`
2. `settings_load()`
3. 注册必要回调
4. 调用 `bt_fast_pair_enable()`
- 运行时常用 API
- `bt_fast_pair_set_pairing_mode()`
- `bt_fast_pair_battery_set()`
- `bt_fast_pair_info_cb_register()`
适合场景:
- 要做经过 Fast Pair 生态认证的耳机、输入设备、Tag 类设备
不适合场景:
- 普通 BLE 外设
- 只想做自定义配对体验
### 4.2.5 Human Interface Device Service, `HIDS`
源码位置:
- `nrf/subsys/bluetooth/services/hids.c`
- `nrf/include/bluetooth/services/hids.h`
- 参考样例:
- `nrf/samples/bluetooth/peripheral_hids_keyboard`
- `nrf/samples/bluetooth/peripheral_hids_mouse`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_HIDS=y
CONFIG_BT_HIDS_MAX_CLIENT_COUNT=2
CONFIG_BT_HIDS_DEFAULT_PERM_RW_ENCRYPT=y
```
怎么用:
1.`BT_HIDS_DEF(hids_obj, ...)` 定义一个 HID 服务实例
2. 准备 `struct bt_hids_init_param`
- HID 信息
- 输入/输出/特征报告组
- Report Map
- 协议模式回调
- Control Point 回调
- Boot Keyboard/Mouse 回调
- `is_kb` / `is_mouse`
3. 调用 `bt_hids_init(&hids_obj, &init_param)`
4. 连接回调里调用:
- `bt_hids_connected(&hids_obj, conn)`
- `bt_hids_disconnected(&hids_obj, conn)`
5. 发报告时调用:
- 通用输入报告:`bt_hids_inp_rep_send()`
- Boot Keyboard`bt_hids_boot_kb_inp_rep_send()`
- Boot Mouse`bt_hids_boot_mouse_inp_rep_send()`
对键盘项目的意义:
- 如果你走标准 HID over GATT 键盘路线,这是最关键的服务实现
- 比起自定义 NUSHIDS 才是操作系统原生识别键盘的正路
### 4.2.6 Latency Service
源码位置:
- `nrf/subsys/bluetooth/services/latency.c`
- `nrf/include/bluetooth/services/latency.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_LATENCY=y
```
怎么用:
- 定义一个 `struct bt_latency latency`
- 可选定义 `struct bt_latency_cb`
- 调用 `bt_latency_init(&latency, &cb)`
- 对端对 Latency Characteristic 发写请求时,会回调 `latency_request()`
适合场景:
- 链路时延测量
- 实验室验证,不是典型产品服务
### 4.2.7 LED Button Service, `LBS`
源码位置:
- `nrf/subsys/bluetooth/services/lbs.c`
- `nrf/include/bluetooth/services/lbs.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_LBS=y
```
怎么用:
- 定义 `struct bt_lbs_cb`
- `led_cb`:远端写 LED 特征值时回调
- `button_cb`:远端读 Button 特征值时回调
- 初始化:`bt_lbs_init(&callbacks)`
- 按键状态变化时调用 `bt_lbs_send_button_state(button_state)`
适合场景:
- 教学、验证链路、最小双向 GATT 交互 demo
不建议:
- 直接当成量产产品协议
### 4.2.8 Memfault Diagnostic Service, `MDS`
源码位置:
- `nrf/subsys/bluetooth/services/mds.c`
- `nrf/include/bluetooth/services/mds.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_MDS=y
```
怎么用:
- 这个服务主要为 Memfault 诊断导出服务
- 启用后服务静态注册
- 如果想控制谁能访问诊断数据,在 `bt_enable()` 前调用 `bt_mds_cb_register(&cb)`
- 关键回调是 `access_enable(conn)`,用来决定连接方是否有权限访问 MDS 数据
适合场景:
- 你已经集成 Memfault并希望通过 BLE 导出诊断信息
### 4.2.9 Nordic Status Message Service, `NSMS`
源码位置:
- `nrf/subsys/bluetooth/services/nsms.c`
- `nrf/include/bluetooth/services/nsms.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_NSMS=y
```
怎么用:
- 这个服务不是 `init()` 风格,而是**宏定义实例**
-`BT_NSMS_DEF(nsms_obj, "Status", BT_NSMS_SECURITY_LEVEL_ENCRYPT, "idle", 64)` 定义一个服务实例
- 之后运行时调用 `bt_nsms_set_status(&nsms_obj, "connected")` 更新状态文本
特点:
- 适合快速暴露一段人类可读状态字符串
- 支持安全级别配置
- 支持多实例
适合场景:
- 开发调试状态
- 设备工作模式简报
### 4.2.10 Nordic UART Service, `Nordic 旧版 NUS`
源码位置:
- `nrf/subsys/bluetooth/services/nus.c`
- `nrf/include/bluetooth/services/nus.h`
- 参考样例:`nrf/samples/bluetooth/peripheral_uart`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_NUS=y
```
可选:
```conf
CONFIG_BT_NUS_AUTHEN=y
```
怎么用:
- 定义 `struct bt_nus_cb`
- 调用 `bt_nus_init(&callbacks)`
-`received()` 回调里处理远端写进来的数据
- 调用 `bt_nus_send(conn, data, len)` 给远端发通知
- `bt_nus_get_mtu(conn)` 可拿到当前可用有效负载大小
和 Zephyr 版 NUS 的区别:
- Nordic 旧版更接近历史 NCS 示例代码
- API 更简单,通常单实例
- 如果项目已经基于 `peripheral_uart` 一类示例写的,通常继续沿用这个版本更省事
注意:
- **不要同时把 Zephyr NUS 和 Nordic 旧版 NUS 当成同一个业务通道一起开**,它们 UUID 相同,容易造成概念和实现混乱
### 4.2.11 Ranging Service, `RAS`
源码位置:
- `nrf/subsys/bluetooth/services/ras/rrsp/ras_rrsp.c`
- `nrf/include/bluetooth/services/ras.h`
启用方法:
如果本机要作为**服务端/响应端**暴露 Ranging Service
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_RAS=y
CONFIG_BT_RAS_RRSP=y
```
怎么用:
- 服务端 RRSP 角色启用后会注册标准 Ranging Service
- 每个连接通常要先调用 `bt_ras_rrsp_alloc(conn)` 关联上下文
- 通过 `bt_ras_rd_buffer_cb_register()` 监听 ranging data buffer 生命周期
- 当有新的测距数据可提供时,使用数据缓冲 API
- `bt_ras_rd_buffer_ready_check()`
- `bt_ras_rd_buffer_claim()`
- `bt_ras_rd_buffer_release()`
- `bt_ras_rd_buffer_bytes_pull()`
- 连接释放时调用 `bt_ras_rrsp_free(conn)`
补充说明:
- 同一个头文件里还提供 `RREQ` API那是**客户端/请求端**使用的,不是本地服务端注册逻辑
适合场景:
- BLE Channel Sounding / Ranging 实验与产品验证
### 4.2.12 Running Speed and Cadence Service, `RSCS`
源码位置:
- `nrf/subsys/bluetooth/services/rscs.c`
- `nrf/include/bluetooth/services/rscs.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_RSCS=y
```
怎么用:
-`struct bt_rscs_init_params`
- `features`
- 支持的位置列表
- 当前位置
- 事件处理函数
- 控制点回调
- 调用 `bt_rscs_init(&init)`
- 生成新测量值时调用 `bt_rscs_measurement_send(conn, &measurement)`
控制点回调负责:
- `set_cumulative()`
- `calibration()`
- `update_loc()`
适合场景:
- 跑步传感器、脚环、步频速度设备
### 4.2.13 Throughput Service
源码位置:
- `nrf/subsys/bluetooth/services/throughput.c`
- `nrf/include/bluetooth/services/throughput.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_THROUGHPUT=y
```
怎么用:
- 定义 `struct bt_throughput throughput`
- 定义 `struct bt_throughput_cb`
- 调用 `bt_throughput_init(&throughput, &cb)`
如果本机作为服务端:
- 服务会被注册为一个测试用 Throughput Service
- 对端读/写该特征值时,你会在回调里拿到吞吐统计信息
如果本机作为客户端测试远端吞吐:
-`bt_gatt_dm` 发现远端 Throughput Service
- 调用 `bt_throughput_handles_assign(dm, &throughput)`
- 然后用 `bt_throughput_read()` / `bt_throughput_write()` 发测试流量
适合场景:
- 链路性能压测
- MTU / DLE / PHY 参数实验
### 4.2.14 Wi-Fi Provisioning Service
源码位置:
- `nrf/subsys/bluetooth/services/wifi_prov/wifi_prov_ble.c`
- `nrf/include/bluetooth/services/wifi_provisioning.h`
- 配套核心库:`nrf/include/net/wifi_prov_core/wifi_prov_core.h`
启用方法:
```conf
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_WIFI_PROV=y
```
怎么用:
- 这个 BLE GATT 服务本身是 Wi-Fi Provisioning Core 的传输承载层
- 你真正要初始化的是 `wifi_prov_core`
- 典型流程:
1. 调用 `wifi_prov_init()`
2. 广告里带上 Provisioning Service UUID
3. 手机/配置工具向 Control Point 写 provisioning 请求
4. 核心库通过 `wifi_prov_send_rsp()` / `wifi_prov_send_result()` 把结果经 BLE 返回
适合场景:
- 设备本身有 Wi-Fi但没有屏幕/键盘,需要靠手机配网
## 5. 哪些是客户端辅助库,不是本地服务端
下面这些模块也在 `services` 目录里,但它们主要是**Central 侧访问远端服务**用的,不是“本机注册一个 GATT Service”
- `ams_client`
- `ancs_client`
- `bas_client`
- `cts_client`
- `dfu_smp`
- `gattp`
- `hogp`
- `hrs_client`
- `ias` 的 client API 部分
- `latency_client`
- `nus_client`
- `ots` 的 client API 部分
- `ras``RREQ` API 部分
如果你需要,我可以下一步单独再整理一份“**这些 client 库分别怎么发现远端服务、怎么订阅、怎么读写**”。
## 6. 对 `new_kbd` 项目的直接建议
如果目标是 BLE 键盘,优先级基本如下:
### 第一优先级
- `HIDS`:标准键盘主服务
- `BAS`:电量上报
- `DIS`:厂商/型号/版本
### 第二优先级
- `BMS`:如果你想让主机侧触发清配对
- `IAS` / `Proximity` 类思路:如果要做“找键盘”
- `TPS`:若你想暴露标准发射功率
### 通常不建议直接用于键盘主链路
- `NUS`:适合调试、配置通道,不适合作为系统键盘输入主协议
- `OTS`:太重
- `LBS`:示例用途
- `Throughput` / `Latency`:测试用途
## 7. 参考代码路径
你后面如果要继续落到工程实现,最值得先看的参考代码是:
- `C:\ncs\v3.2.3\nrf\samples\bluetooth\peripheral_hids_keyboard`
- `C:\ncs\v3.2.3\nrf\samples\bluetooth\peripheral_uart`
- `C:\ncs\v3.2.3\zephyr\samples\bluetooth\peripheral_nus`
- `C:\ncs\v3.2.3\zephyr\samples\bluetooth\peripheral_ots`
如果你要,我下一步可以继续给你补一份“**把这些 Service 映射到 `new_kbd` 当前代码结构的落地建议**”,直接对应你现在的 `ble_bond_module.c``ble_battery_module.c` 和后续 HIDS 模块拆分方式。

View File

@@ -0,0 +1,118 @@
# new_kbd 项目 Nordic NCS 官方知识索引
## 说明
- 生成时间2026-03-30
- 文档来源:仅收录 `docs.nordicsemi.com` 上可直接访问的 Nordic 官方文档
- 版本基线:按项目当前环境优先核对 `ncs-3.2.3`
- 项目芯片基线:`atguigu_mini_keyboard/nrf52840`
- 芯片依据:项目板级文件 [atguigu_mini_keyboard.yaml](E:/extra/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.yaml) 与 [atguigu_mini_keyboard.dts](E:/extra/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.dts) 明确基于 `nrf52840`
- 收录范围:仅收录 `E:\projects\new_kbd` 实际使用到的 NCS 官方知识,以及 Nordic 站点中同时存在的 Zephyr 镜像文档
- 不收录原则:如果没有核到 Nordic 官方页面,则不写入本索引
- 本版校验方式:使用浏览器渲染后再判定,凡标题或正文渲染为 `Error 404 - Page Not Found` / `ERROR CODE: 404` 的页面,均视为无效并移除
## NCS 托管的 Zephyr 文档镜像
### 构建与配置
- [应用开发总览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/develop/application/index.html)
- [CMake 构建系统NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/cmake/index.html)
- [Kconfig 配置系统NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/kconfig/index.html)
- [Sysbuild 系统构建NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/sysbuild/index.html)
### 设备树与设备模型
- [设备树入门NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/intro.html)
- [设备树 HOWTONCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/howtos.html)
- [设备树 C/C++ API 用法NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/api-usage.html)
- [zephyr,user 自定义节点NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/zephyr-user-node.html)
### 内核与并发
- [工作队列与延迟工作NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/kernel/services/threads/workqueue.html)
- [原子操作NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/kernel/services/other/atomic.html)
- [自旋锁 APINCS 镜像)](https://docs.nordicsemi.com/bundle/zephyr-apis-3.2.3/page/spinlock_8h.html)
- [时钟、超时与 uptimeNCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/kernel/services/timing/clocks.html)
### 日志与存储
- [日志子系统NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/services/logging/index.html)
- [Settings 设置子系统NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/services/storage/settings/index.html)
- [NVS 非易失存储NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/services/storage/nvs/nvs.html)
- [Flash Map 闪存映射NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/services/storage/flash_map/flash_map.html)
### 蓝牙 Low Energy
- [Bluetooth GAP 概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/bluetooth/api/gap.html)
- [Bluetooth 连接管理NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/bluetooth/api/connection_mgmt.html)
- [Bluetooth GATT 概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/bluetooth/api/gatt.html)
- [Bluetooth 标准服务总览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/bluetooth/api/services.html)
### USB 与 HID
- [USB 设备栈Device Stack NextNCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/usb/device_next/usb_device.html)
- [USB HID 设备 APINCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/usb/device_next/api/usbd_hid_device.html)
- [HID 通用定义与报告描述符NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/usb/api/hid.html)
### 外设驱动
- [ADC 外设概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/adc.html)
- [GPIO 外设概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/gpio.html)
- [I2C 外设概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/i2c.html)
- [传感器子系统概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/sensor/index.html)
- [LED 外设概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/led.html)
### 显示与 GUI
- [显示子系统概览NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/hardware/peripherals/display/index.html)
- [PWM LEDs 设备树绑定NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/api/bindings/led/pwm-leds.html)
- [GPIO LEDs 设备树绑定NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/api/bindings/led/gpio-leds.html)
- [LVGL 官方示例入口NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/samples/modules/lvgl/lvgl.html)
### 通用工具宏与字节序
- [sys/byteorder 字节序 APINCS 镜像)](https://docs.nordicsemi.com/bundle/zephyr-apis-3.2.3/page/sys_2byteorder_8h.html)
- [spinlock 文件参考NCS API 镜像)](https://docs.nordicsemi.com/bundle/zephyr-apis-3.2.3/page/spinlock_8h.html)
## Partition Manager 与存储布局
- [Partition Manager](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/scripts/partition_manager/partition_manager.html)
## CAF 总览与基础设施
- [Common Application Framework (CAF) 总览](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/caf_overview.html)
- [Application Event Manager](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/others/app_event_manager.html)
- [Application Event Manager Profiler Tracer](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/others/app_event_manager_profiler_tracer.html)
## CAF 模块
- [CAF Buttons 模块](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/buttons.html)
- [CAF LEDs 模块](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/leds.html)
- [CAF BLE Advertising 模块](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/ble_adv.html)
- [CAF BLE State 模块](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/ble_state.html)
- [CAF Power Manager 模块](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/libraries/caf/power_manager.html)
- [Settings Loader](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/nrf/applications/nrf_desktop/doc/settings_loader.html)
## CAF API / 辅助定义
- [led_effect API](https://docs.nordicsemi.com/bundle/nrf-apis-3.2.3/page/group_led_effect_CAF.html)
## 芯片与 Nordic 外设相关
- [nRF52840 驱动索引](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/nrf52840_drivers.html)
- [nRF52840 DK 用户指南](https://docs.nordicsemi.com/bundle/ug_nrf52840_dk)
- [Nordic nRF USBD 设备树绑定NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/api/bindings/usb/nordic_nrf-usbd.html)
- [nrf_qdec HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_qdec.html)
- [nrf_saadc HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_saadc.html)
- [nrf_usbd HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_usbd.html)
- [nrf_gpio HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_gpio.html)
- [nrf_pwm HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_pwm.html)
- [nrf_spi HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_spi.html)
- [nrf_twim HAL / 寄存器 API](https://docs.nordicsemi.com/bundle/nrfx-apis-3.2.3/page/group_nrf_twim.html)
## 备注
- `pm_static.yml` 相关内容,本次仅核到 Nordic 官方的 Partition Manager 页面;未单独核到专门以 `pm_static.yml` 命名的独立官方页面,因此这里索引到 Partition Manager 官方文档
- Zephyr 侧已经收录的知识,只要在 Nordic 站点存在并通过浏览器渲染校验,本索引也一并收录
- 芯片相关知识本次按项目实际硬件基线 `nrf52840` 收录,没有扩展到未使用的其他 Nordic SoC
- 当前 Nordic 站点里,很多 `zephyr-apis-3.2.3``nrf-apis-3.2.3` 与部分 CAF 事件页虽然返回 `200`,但浏览器渲染后是 `Error 404 - Page Not Found`,因此已从本索引移除

View File

@@ -0,0 +1,96 @@
# new_kbd 项目 Zephyr 官方知识索引
## 说明
- 生成时间2026-03-30
- 扫描范围:`src/``inc/``prj.conf``sysbuild.conf``app.overlay``pm_static.yml`
- 收录原则:仅收录 `E:\projects\new_kbd` 当前实际用到、且已确认页面真实存在的 Zephyr 官方文档
- 链接范围:`https://docs.zephyrproject.org/latest/``https://docs.zephyrproject.org/apidoc/latest/``https://docs.zephyrproject.org/latest/doxygen/html/`
- 不收录范围Nordic NCS/CAF/App Event Manager/Partition Manager 文档,以及项目自定义驱动或项目私有协议说明
- Nordic NCS 官方补充索引见:[new_kbd 项目 Nordic NCS 官方知识索引](./nordic_ncs_官方知识索引.md)
## 构建与配置
- [应用开发总览](https://docs.zephyrproject.org/latest/develop/application/index.html)
- [CMake 构建系统](https://docs.zephyrproject.org/latest/build/cmake/index.html)
- [Kconfig 配置系统](https://docs.zephyrproject.org/latest/build/kconfig/index.html)
- [Sysbuild 系统构建](https://docs.zephyrproject.org/latest/build/sysbuild/index.html)
## 设备树与设备模型
- [设备树入门](https://docs.zephyrproject.org/latest/build/dts/intro.html)
- [设备树 HOWTO](https://docs.zephyrproject.org/latest/build/dts/howtos.html)
- [设备树 C/C++ API 用法](https://docs.zephyrproject.org/latest/build/dts/api-usage.html)
- [zephyr,user 自定义节点](https://docs.zephyrproject.org/latest/build/dts/zephyr-user-node.html)
- [设备模型 API](https://docs.zephyrproject.org/apidoc/latest/group__device__model.html)
- [设备树通用标识符 API](https://docs.zephyrproject.org/apidoc/latest/group__devicetree-generic-id.html)
## 内核与并发
- [工作队列与延迟工作](https://docs.zephyrproject.org/latest/kernel/services/threads/workqueue.html)
- [原子操作](https://docs.zephyrproject.org/latest/kernel/services/other/atomic.html)
- [自旋锁 API](https://docs.zephyrproject.org/apidoc/latest/spinlock_8h.html)
- [时钟、超时与 uptime](https://docs.zephyrproject.org/latest/kernel/services/timing/clocks.html)
## 日志与存储
- [日志子系统](https://docs.zephyrproject.org/latest/services/logging/index.html)
- [Settings 设置子系统](https://docs.zephyrproject.org/latest/services/storage/settings/index.html)
- [NVS 非易失存储](https://docs.zephyrproject.org/latest/services/storage/nvs/nvs.html)
- [Flash Map 闪存映射](https://docs.zephyrproject.org/latest/services/storage/flash_map/flash_map.html)
## 蓝牙 Low Energy
- [Bluetooth GAP 概览](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/gap.html)
- [Bluetooth GAP API](https://docs.zephyrproject.org/apidoc/latest/group__bt__gap.html)
- [Bluetooth 连接管理](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/connection_mgmt.html)
- [Bluetooth 连接管理 API](https://docs.zephyrproject.org/apidoc/latest/group__bt__conn.html)
- [Bluetooth GATT 概览](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/gatt.html)
- [Bluetooth GATT Server API](https://docs.zephyrproject.org/apidoc/latest/group__bt__gatt__server.html)
- [Bluetooth UUID API](https://docs.zephyrproject.org/apidoc/latest/group__bt__uuid.html)
- [Bluetooth Battery Service (BAS) API](https://docs.zephyrproject.org/apidoc/latest/group__bt__bas.html)
- [Bluetooth 标准服务总览](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/services.html)
- [BLE HID 外设官方示例](https://docs.zephyrproject.org/latest/samples/bluetooth/peripheral_hids/README.html)
## USB 与 HID
- [USB 设备栈Device Stack Next](https://docs.zephyrproject.org/latest/connectivity/usb/device_next/usb_device.html)
- [USB Device API](https://docs.zephyrproject.org/apidoc/latest/group__usbd__api.html)
- [USB HID 设备 API](https://docs.zephyrproject.org/latest/connectivity/usb/device_next/api/usbd_hid_device.html)
- [USB HID Device API 参考](https://docs.zephyrproject.org/apidoc/latest/group__usbd__hid__device.html)
- [HID 通用定义与报告描述符](https://docs.zephyrproject.org/latest/connectivity/usb/api/hid.html)
- [zephyr,hid-device 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/usb/zephyr%2Chid-device.html)
## 外设驱动
- [ADC 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/adc.html)
- [ADC API](https://docs.zephyrproject.org/apidoc/latest/group__adc__interface.html)
- [GPIO 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/gpio.html)
- [GPIO API](https://docs.zephyrproject.org/apidoc/latest/group__gpio__interface.html)
- [I2C 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/i2c.html)
- [传感器子系统概览](https://docs.zephyrproject.org/latest/hardware/peripherals/sensor/index.html)
- [传感器 API](https://docs.zephyrproject.org/apidoc/latest/group__sensor__interface.html)
- [LED 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/led.html)
- [LED API](https://docs.zephyrproject.org/apidoc/latest/group__led__interface.html)
- [Nordic nRF QDEC 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/sensor/nordic%2Cnrf-qdec.html)
## 显示与 GUI
- [显示子系统概览](https://docs.zephyrproject.org/latest/hardware/peripherals/display/index.html)
- [MIPI-DBI SPI 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/mipi-dbi/zephyr%2Cmipi-dbi-spi.html)
- [ST7789V 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/display/sitronix%2Cst7789v.html)
- [PWM LEDs 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/led/pwm-leds.html)
- [GPIO LEDs 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/led/gpio-leds.html)
- [LVGL 官方示例入口](https://docs.zephyrproject.org/latest/samples/modules/lvgl/lvgl.html)
## 通用工具宏与字节序
- [sys/util 通用工具宏 API](https://docs.zephyrproject.org/apidoc/latest/group__sys-util.html)
- [sys/byteorder 字节序 API](https://docs.zephyrproject.org/latest/doxygen/html/sys_2byteorder_8h.html)
## 未纳入本索引的项目项
- `zephyr/drivers/power/ip5306.h``CONFIG_IP5306`:当前项目使用的是自定义驱动/自定义绑定Zephyr 官方 latest 站点未核到对应官方页面,因此未纳入
- `pm_static.yml` 对应的 Partition Manager 规划:属于 Nordic NCS 范畴,不属于 Zephyr 官方 docs 站点范围Nordic 官方索引见 [nordic_ncs_官方知识索引.md](./nordic_ncs_官方知识索引.md)
- `CAF``App Event Manager``settings_loader``ble_state``ble_common_event``power_manager``buttons_def.h` 等:属于 Nordic NCS/CAF 文档范围,不属于本 Zephyr 官方索引Nordic 官方索引见 [nordic_ncs_官方知识索引.md](./nordic_ncs_官方知识索引.md)
- 项目私有协议与实现,如时间同步私有 GATT 服务、主机 HID 命令协议、显示主题持久化逻辑等:只索引其依赖的 Zephyr 通用能力,不索引项目私有设计本身

34
inc/buttons_def.h Normal file
View File

@@ -0,0 +1,34 @@
/*
* CAF buttons 矩阵引脚定义
*
* 设计说明:
* - 本文件被 CAF buttons 模块通过 CONFIG_CAF_BUTTONS_DEF_PATH 直接包含;
* - 行列引脚顺序必须与板级 DTS 中 my_keyboard 的 row-gpios/col-gpios 保持一致;
* - key_id 的行列编号完全基于这里的数组下标,不依赖 input-keymap 节点。
*/
#include <caf/gpio_pins.h>
/*
* 该符号用于保证配置文件只被链接一次:
* 若被重复包含到多个编译单元,会在链接阶段报重复定义,避免静默错配。
*/
const struct {} buttons_def_include_once;
/* 列引脚:对应 atguigu_mini_keyboard.dts 中 my_keyboard/col-gpios 顺序。 */
static const struct gpio_pin col[] = {
{ .port = 0, .pin = 5 },
{ .port = 0, .pin = 6 },
{ .port = 0, .pin = 26 },
{ .port = 0, .pin = 30 },
};
/* 行引脚:对应 atguigu_mini_keyboard.dts 中 my_keyboard/row-gpios 顺序。 */
static const struct gpio_pin row[] = {
{ .port = 0, .pin = 15 },
{ .port = 0, .pin = 7 },
{ .port = 0, .pin = 12 },
{ .port = 0, .pin = 4 },
{ .port = 1, .pin = 9 },
{ .port = 0, .pin = 8 },
};

View File

@@ -0,0 +1,16 @@
#ifndef HID_HOST_COMMAND_PROTOCOL_H__
#define HID_HOST_COMMAND_PROTOCOL_H__
#include <stdint.h>
#define HID_HOST_CMD_DATA_SIZE 8U
#define HID_HOST_CMD_OUTPUT_PAYLOAD_SIZE (1U + HID_HOST_CMD_DATA_SIZE)
#define HID_HOST_CMD_ACK_PAYLOAD_SIZE 1U
#define HID_HOST_CMD_ID_THEME_COLOR 0x01U
#define HID_HOST_CMD_ID_TIME_SYNC 0x02U
#define HID_HOST_CMD_THEME_PARAM_SIZE 3U
#define HID_HOST_CMD_TIME_SYNC_PARAM_SIZE 8U
#endif /* HID_HOST_COMMAND_PROTOCOL_H__ */

9
inc/hid_host_transport.h Normal file
View File

@@ -0,0 +1,9 @@
#ifndef HID_HOST_TRANSPORT_H__
#define HID_HOST_TRANSPORT_H__
enum hid_host_transport {
HID_HOST_TRANSPORT_USB = 0,
HID_HOST_TRANSPORT_BLE,
};
#endif /* HID_HOST_TRANSPORT_H__ */

43
inc/hid_keymap_def.h Normal file
View File

@@ -0,0 +1,43 @@
/*
* HID keymap for current new_kbd numeric keypad layout.
* 说明:
* - 本文件仿照 nrf_desktop 的 hid_keymap_def.h 组织方式;
* - 仅由 keyboard_module.c 包含一次(通过 APP_HID_KEYMAP_DEF_PATH
* - 条目必须按 key_id 升序排列(按 KEY_ID(col, row) 计算后的数值顺序)。
*/
#include <caf/key_id.h>
/*
* 防止该定义文件被多处 include 导致重复符号。
* 约定仅由 keyboard_module.c 包含一次。
*/
const struct {} hid_keymap_def_include_once;
static const struct hid_keymap hid_keymap[] = {
/* col 0 */
{ KEY_ID(0, 1), 0x0053, REPORT_ID_KEYBOARD }, /* Num Lock */
{ KEY_ID(0, 2), 0x005F, REPORT_ID_KEYBOARD }, /* Keypad 7 */
{ KEY_ID(0, 3), 0x005C, REPORT_ID_KEYBOARD }, /* Keypad 4 */
{ KEY_ID(0, 4), 0x0059, REPORT_ID_KEYBOARD }, /* Keypad 1 */
{ KEY_ID(0, 5), 0x0062, REPORT_ID_KEYBOARD }, /* Keypad 0 */
/* col 1 */
{ KEY_ID(1, 1), 0x0054, REPORT_ID_KEYBOARD }, /* Keypad / */
{ KEY_ID(1, 2), 0x0060, REPORT_ID_KEYBOARD }, /* Keypad 8 */
{ KEY_ID(1, 3), 0x005D, REPORT_ID_KEYBOARD }, /* Keypad 5 */
{ KEY_ID(1, 4), 0x005A, REPORT_ID_KEYBOARD }, /* Keypad 2 */
{ KEY_ID(1, 5), 0x0063, REPORT_ID_KEYBOARD }, /* Keypad . */
/* col 2 */
{ KEY_ID(2, 1), 0x0055, REPORT_ID_KEYBOARD }, /* Keypad * */
{ KEY_ID(2, 2), 0x0061, REPORT_ID_KEYBOARD }, /* Keypad 9 */
{ KEY_ID(2, 3), 0x005E, REPORT_ID_KEYBOARD }, /* Keypad 6 */
{ KEY_ID(2, 4), 0x005B, REPORT_ID_KEYBOARD }, /* Keypad 3 */
/* col 3 */
{ KEY_ID(3, 0), 0x00E2, REPORT_ID_CONSUMER }, /* Mute */
{ KEY_ID(3, 1), 0x0056, REPORT_ID_KEYBOARD }, /* Keypad - */
{ KEY_ID(3, 3), 0x0057, REPORT_ID_KEYBOARD }, /* Keypad + */
{ KEY_ID(3, 5), 0x0058, REPORT_ID_KEYBOARD }, /* Keypad Enter */
};

161
inc/hid_report_descriptor.h Normal file
View File

@@ -0,0 +1,161 @@
#ifndef HID_REPORT_DESCRIPTOR_H_
#define HID_REPORT_DESCRIPTOR_H_
#include <zephyr/usb/class/usbd_hid.h>
#include "hid_host_command_protocol.h"
/* 与 HID Report Map 对齐的 Report ID。 */
enum {
REPORT_ID_KEYBOARD = 1,
REPORT_ID_CONSUMER = 3,
REPORT_ID_VENDOR = 4,
REPORT_ID_VENDOR_CMD = 5,
};
#define HID_KBD_USAGE_MAX 0x00E7U
#define HID_KBD_MOD_COUNT 8U
#define HID_KBD_BITMAP_BITS (HID_KBD_USAGE_MAX + 1U)
#define HID_KBD_BITMAP_SIZE ((HID_KBD_BITMAP_BITS + 7U) / 8U)
#define HID_KBD_PAYLOAD_SIZE (1U + HID_KBD_BITMAP_SIZE)
#define HID_BOOT_KBD_PAYLOAD_SIZE 8U
#define HID_CONSUMER_PAYLOAD_SIZE 2U
#define HID_VENDOR_PAYLOAD_SIZE HID_KBD_PAYLOAD_SIZE
#define HID_VENDOR_ACK_PAYLOAD_SIZE HID_HOST_CMD_ACK_PAYLOAD_SIZE
#define HID_KBD_LED_PAYLOAD_SIZE 1U
#define HID_FULL_REPORT_SIZE(payload) (1U + (payload))
/*
* HID_USAGE_PAGE() 只支持 1 字节 Usage Page。
* Vendor Defined Page(0xFF00) 需要 2 字节编码,因此在本地补一个 16 位版本,
* 避免在描述符里混用裸字节,后续维护时可以一眼看出字段语义。
*/
#define HID_USAGE_PAGE16(page_lsb, page_msb) \
HID_ITEM(HID_ITEM_TAG_USAGE_PAGE, HID_ITEM_TYPE_GLOBAL, 2), page_lsb, page_msb
/*
* 键盘(NKRO) + Consumer + Vendor 的复合 Report 描述符:
* - USB Report 接口和 BLE HIDS Report Map 统一使用这份定义,
* 避免两边手写常量后长期演进出现不一致。
* - Report ID 0x04 继续复用 NKRO payload承载私有状态/遮罩语义。
* - Report ID 0x05 预留给“主机命令 + 设备 ACK”通道
* - Output payload 固定 9 字节:[cmd(1) | data(8)]
* - Input payload 固定 1 字节:[cmd]
*/
#define HID_DESC_KEYBOARD_NKRO_CONSUMER() \
{ \
/* Generic Desktop 页:声明这是一个 Keyboard Application 集合。 */ \
HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP), \
HID_USAGE(HID_USAGE_GEN_DESKTOP_KEYBOARD), \
HID_COLLECTION(HID_COLLECTION_APPLICATION), \
HID_REPORT_ID(REPORT_ID_KEYBOARD), \
\
/* Keyboard/Keypad 页:先定义 8bit Modifier再定义 232bit NKRO 位图。 */ \
HID_USAGE_PAGE(HID_USAGE_GEN_DESKTOP_KEYPAD), \
HID_USAGE_MIN8(0xE0), \
HID_USAGE_MAX8(0xE7), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX8(1), \
HID_REPORT_SIZE(1), \
HID_REPORT_COUNT(8), \
HID_INPUT(0x02), \
HID_USAGE_MIN8(0x00), \
HID_USAGE_MAX8(0xE7), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX8(1), \
HID_REPORT_SIZE(1), \
HID_REPORT_COUNT(HID_KBD_BITMAP_BITS), \
HID_INPUT(0x02), \
\
/* Report 协议下键盘 LED 输出NumLock/CapsLock/ScrollLock/Compose/Kana。 */ \
HID_USAGE_PAGE(0x08U), \
HID_USAGE_MIN8(0x01), \
HID_USAGE_MAX8(0x05), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX8(1), \
HID_REPORT_SIZE(1), \
HID_REPORT_COUNT(5), \
HID_OUTPUT(0x02), \
/* 补齐到 1 字节3bit padding标记为常量。 */ \
HID_REPORT_SIZE(3), \
HID_REPORT_COUNT(1), \
HID_OUTPUT(0x01), \
HID_END_COLLECTION, \
\
/* Consumer 页:使用 16bit Usage 承载多媒体按键(音量/播放/亮度等)。 */ \
HID_USAGE_PAGE(0x0CU), \
HID_USAGE(0x01U), \
HID_COLLECTION(HID_COLLECTION_APPLICATION), \
HID_REPORT_ID(REPORT_ID_CONSUMER), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX16(0xEA, 0x00), \
HID_USAGE_MIN16(0x00, 0x00), \
HID_USAGE_MAX16(0xEA, 0x00), \
HID_REPORT_SIZE(16), \
HID_REPORT_COUNT(1), \
HID_INPUT(0x00), \
HID_END_COLLECTION, \
\
/* Vendor 页:双向传输完整键盘状态/屏蔽遮罩payload 结构与 NKRO 一致。 */ \
HID_USAGE_PAGE16(0x00, 0xFF), \
HID_USAGE(0x02U), \
HID_COLLECTION(HID_COLLECTION_APPLICATION), \
HID_REPORT_ID(REPORT_ID_VENDOR), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX16(0xFF, 0x00), \
HID_REPORT_SIZE(8), \
HID_REPORT_COUNT(HID_VENDOR_PAYLOAD_SIZE), \
HID_USAGE(0x02U), \
HID_INPUT(0x02), \
HID_REPORT_COUNT(HID_VENDOR_PAYLOAD_SIZE), \
HID_USAGE(0x02U), \
HID_OUTPUT(0x02), \
HID_END_COLLECTION, \
\
/* Vendor 页(0xFF01):主机命令写入 + 设备 ACK 返回。 */ \
HID_USAGE_PAGE16(0x01, 0xFF), \
HID_USAGE(0x05U), \
HID_COLLECTION(HID_COLLECTION_APPLICATION), \
HID_REPORT_ID(REPORT_ID_VENDOR_CMD), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX16(0xFF, 0x00), \
HID_REPORT_SIZE(8), \
HID_REPORT_COUNT(HID_VENDOR_ACK_PAYLOAD_SIZE), \
HID_USAGE(0x05U), \
HID_INPUT(0x02), \
HID_REPORT_COUNT(HID_HOST_CMD_OUTPUT_PAYLOAD_SIZE), \
HID_USAGE(0x05U), \
HID_OUTPUT(0x02), \
HID_END_COLLECTION, \
}
/*
* RAW HID 的固定 64 字节输入/输出描述符。
* 设计意图:
* - 采用 Vendor Defined(0xFF00) 页,避免与标准键盘/多媒体语义冲突;
* - IN/OUT 都固定 64 字节,便于固件与上位机用定长帧做双向透传;
* - 不使用 Report ID接口只承载一个 RAW Report减少主机端解析分支。
*/
#define HID_DESC_RAW_64() \
{ \
/* Vendor Defined 页(0xFF00):供厂商私有协议传输,不绑定标准 HID 语义。 */ \
HID_USAGE_PAGE16(0x00, 0xFF), \
HID_USAGE(0x01), \
HID_COLLECTION(HID_COLLECTION_APPLICATION), \
HID_LOGICAL_MIN8(0), \
HID_LOGICAL_MAX16(0xFF, 0x00), \
HID_REPORT_SIZE(8), \
\
/* 输入页:定义 64 字节 Input 报文Data|Var|Abs(0x02) 与原描述符一致。 */ \
HID_REPORT_COUNT(0x40), \
HID_USAGE(0x01), \
HID_INPUT(0x02), \
\
/* 输出页:定义 64 字节 Output 报文,与输入长度对齐,简化双向协议。 */ \
HID_REPORT_COUNT(0x40), \
HID_USAGE(0x01), \
HID_OUTPUT(0x02), \
HID_END_COLLECTION, \
}
#endif

29
inc/led_state.h Normal file
View File

@@ -0,0 +1,29 @@
#ifndef NEW_KBD_LED_STATE_H__
#define NEW_KBD_LED_STATE_H__
#include <zephyr/types.h>
/* 模块内系统状态:只用于本项目的 LED 映射,不对外暴露协议语义。 */
enum led_ble_state {
LED_BLE_STATE_OFF = 0,
LED_BLE_STATE_WAIT_RECONNECT,
LED_BLE_STATE_PAIRING,
LED_BLE_STATE_CONNECTED,
LED_BLE_STATE_COUNT,
};
enum led_num_lock_state {
LED_NUM_LOCK_STATE_OFF = 0,
LED_NUM_LOCK_STATE_ON,
LED_NUM_LOCK_STATE_COUNT,
};
enum led_id_new_kbd {
LED_ID_NUM_LOCK = 0,
LED_ID_BLE_STATE,
LED_ID_COUNT,
};
#define LED_UNAVAILABLE 0xFF
#endif /* NEW_KBD_LED_STATE_H__ */

39
inc/led_state_def.h Normal file
View File

@@ -0,0 +1,39 @@
#include "led_state.h"
#include <caf/led_effect.h>
/*
* 该文件仅被 led_state_module.c 包含一次,用于定义:
* 1) 逻辑 LED 到 CAF LED 实例编号映射;
* 2) 每个逻辑状态对应的 LED 效果。
*/
const struct {} led_state_def_include_once;
/*
* CAF LED 实例编号来源于 DTS 中 status=okay 的 gpio-leds 顺序:
* - led_0 -> 0Num Lock
* - led_1 -> 1BLE 状态)
*/
static const uint8_t led_map[LED_ID_COUNT] = {
[LED_ID_NUM_LOCK] = 0,
[LED_ID_BLE_STATE] = 1,
};
/* Num Lock 指示:灭=关闭,常亮=开启。 */
static const struct led_effect led_num_lock_state_effect[LED_NUM_LOCK_STATE_COUNT] = {
[LED_NUM_LOCK_STATE_OFF] = LED_EFFECT_LED_OFF(),
[LED_NUM_LOCK_STATE_ON] = LED_EFFECT_LED_ON(LED_COLOR(255, 255, 255)),
};
/*
* BLE 指示灯策略:
* - OFF: 熄灭USB 连接或 BLE 非活动模式)
* - WAIT_RECONNECT: 慢闪1s toggle
* - PAIRING: 快闪0.5s toggle
* - CONNECTED: 常亮
*/
static const struct led_effect led_ble_state_effect[LED_BLE_STATE_COUNT] = {
[LED_BLE_STATE_OFF] = LED_EFFECT_LED_OFF(),
[LED_BLE_STATE_WAIT_RECONNECT] = LED_EFFECT_LED_BLINK(1000, LED_COLOR(255, 255, 255)),
[LED_BLE_STATE_PAIRING] = LED_EFFECT_LED_BLINK(500, LED_COLOR(255, 255, 255)),
[LED_BLE_STATE_CONNECTED] = LED_EFFECT_LED_ON(LED_COLOR(255, 255, 255)),
};

17
inc/settings_loader_def.h Normal file
View File

@@ -0,0 +1,17 @@
/*
* Defines modules that must reach READY before CAF settings_loader
* calls settings_load().
*/
/* Enforce single inclusion in the final link unit. */
const struct {} settings_loader_def_include_once;
#include <caf/events/module_state_event.h>
static inline void get_req_modules(struct module_flags *mf)
{
module_flags_set_bit(mf, MODULE_IDX(main));
#ifdef CONFIG_CAF_BLE_STATE
module_flags_set_bit(mf, MODULE_IDX(ble_state));
#endif
}

69
inc/time_manager.h Normal file
View File

@@ -0,0 +1,69 @@
#ifndef TIME_MANAGER_H__
#define TIME_MANAGER_H__
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* 时间同步来源保持传输无关:
* - BLE/USB/手动设置都复用同一套枚举;
* - 后续如果新增其他同步链路,只需要补枚举值,不需要改事件语义。
*/
enum time_sync_source {
TIME_SYNC_SOURCE_NONE = 0,
TIME_SYNC_SOURCE_BLE,
TIME_SYNC_SOURCE_USB,
TIME_SYNC_SOURCE_MANUAL,
TIME_SYNC_SOURCE_HID,
};
/*
* 时间同步更新载荷:
* - utc_ms 统一使用 UTC 毫秒时间戳,避免内部状态受本地时区影响;
* - timezone_min 记录“显示层”所需的时区偏移,当前不拆 DST
* - accuracy_ms 允许上位机表达这次校时的可信度,未知时传 0 即可。
*/
struct time_sync_update {
uint64_t utc_ms;
int16_t timezone_min;
uint32_t accuracy_ms;
enum time_sync_source source;
};
/*
* 时间快照用于提供给显示、日志或后续 USB/调试接口:
* - synchronized=true 表示当前开机周期内已经收到有效校时;
* - has_persisted_time=true 仅表示 flash 里存过一次历史校时,不代表当前时间仍然可信;
* - ready=false 表示 time_manager 还没等到 settings_loader 完成初始化。
*/
struct time_manager_snapshot {
uint64_t utc_ms;
int16_t timezone_min;
uint32_t accuracy_ms;
enum time_sync_source source;
bool ready;
bool synchronized;
bool has_persisted_time;
};
/* 返回当前模块是否已经完成初始化,供同步入口快速拒绝“过早写入”。 */
bool time_manager_is_ready(void);
/*
* 获取当前时间快照:
* - 返回 0snapshot 已填充;
* - 返回 -EINVAL参数为空
* - 返回 -EAGAIN模块未 ready
* - 返回 -ENODATA当前开机周期尚未完成有效校时。
*/
int time_manager_get_snapshot(struct time_manager_snapshot *snapshot);
#ifdef __cplusplus
}
#endif
#endif /* TIME_MANAGER_H__ */

33
inc/time_sync_protocol.h Normal file
View File

@@ -0,0 +1,33 @@
#ifndef TIME_SYNC_PROTOCOL_H__
#define TIME_SYNC_PROTOCOL_H__
#include <stdint.h>
#include <zephyr/sys/util.h>
/*
* 统一定义时间同步协议帧格式,方便 BLE/USB 两条链路共享:
*
* byte 0 : version
* byte 1 : flags
* byte 2-3 : timezone_min (little-endian, int16)
* byte 4-11: utc_ms (little-endian, uint64)
* byte 12-15: accuracy_ms (little-endian, uint32)
*/
#define TIME_SYNC_PROTOCOL_VERSION 1U
#define TIME_SYNC_PROTOCOL_PAYLOAD_SIZE 16U
#define TIME_SYNC_PROTOCOL_OFFSET_VERSION 0U
#define TIME_SYNC_PROTOCOL_OFFSET_FLAGS 1U
#define TIME_SYNC_PROTOCOL_OFFSET_TIMEZONE 2U
#define TIME_SYNC_PROTOCOL_OFFSET_UTC_MS 4U
#define TIME_SYNC_PROTOCOL_OFFSET_ACCURACY_MS 12U
/*
* 预留 flags 字段:
* - 当前版本只要求时区字段有效;
* - 后续如果要加 DST、闰秒或来源质量扩展可以继续复用这个字节。
*/
#define TIME_SYNC_PROTOCOL_FLAG_TIMEZONE_VALID BIT(0)
#endif /* TIME_SYNC_PROTOCOL_H__ */

View File

@@ -0,0 +1,5 @@
zephyr_library()
zephyr_library_sources_ifdef(CONFIG_IP5306 drivers/power/ip5306.c)
zephyr_library_sources_ifdef(CONFIG_IP5306 drivers/power/ip5306_keepalive_sw.c)
zephyr_library_sources_ifdef(CONFIG_IP5306_KEEPALIVE_HW_NRF drivers/power/ip5306_keepalive_nrf.c)
zephyr_include_directories(include)

5
modules/ip5306/Kconfig Normal file
View File

@@ -0,0 +1,5 @@
menu "IP5306 Module"
rsource "drivers/power/Kconfig"
endmenu

View File

@@ -0,0 +1,17 @@
config IP5306
bool "Injoinic IP5306 PMIC driver"
depends on I2C && GPIO
default n
help
Enable IP5306 PMIC driver over I2C.
config IP5306_KEEPALIVE_HW_NRF
bool "Enable nRF keepalive HW offload backend"
depends on IP5306 && SOC_FAMILY_NORDIC_NRF
select NRFX_GPIOTE
select NRFX_GPPI
select NRFX_RTC2
default y
help
Enable low-power keepalive offload backend using RTC2 + GPIOTE + GPPI.
Runtime selection is controlled by devicetree property keepalive-offload.

View File

@@ -0,0 +1,152 @@
#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <zephyr/device.h>
#include <zephyr/drivers/power/ip5306.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include "ip5306_priv.h"
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
#include <soc_nrf_common.h>
#define IP5306_CFG_KEEPALIVE_PSEL_INIT(inst) \
.keepalive_psel = NRF_DT_GPIOS_TO_PSEL_OR(DT_DRV_INST(inst), keepalive_gpios, 0),
#else
#define IP5306_CFG_KEEPALIVE_PSEL_INIT(inst)
#endif
LOG_MODULE_REGISTER(ip5306, LOG_LEVEL_INF);
#define DT_DRV_COMPAT injoinic_ip5306
#define IP5306_REG_READ0 0x70
#define IP5306_REG_READ1 0x71
#define IP5306_STATUS_BIT BIT(3)
static int ip5306_read_reg(const struct device *dev, uint8_t reg, uint8_t *val)
{
const struct ip5306_config *cfg = dev->config;
return i2c_reg_read_byte_dt(&cfg->i2c, reg, val);
}
static int ip5306_get_status_bit(const struct device *dev, uint8_t reg, bool *flag)
{
uint8_t value = 0U;
int ret;
if (flag == NULL) {
return -EINVAL;
}
ret = ip5306_read_reg(dev, reg, &value);
if (ret != 0) {
return ret;
}
*flag = ((value & IP5306_STATUS_BIT) != 0U);
return 0;
}
static int ip5306_api_is_charging(const struct device *dev, bool *charging)
{
return ip5306_get_status_bit(dev, IP5306_REG_READ0, charging);
}
static int ip5306_api_is_charge_full(const struct device *dev, bool *full)
{
return ip5306_get_status_bit(dev, IP5306_REG_READ1, full);
}
static void ip5306_keepalive_start(const struct device *dev)
{
struct ip5306_data *data = dev->data;
const struct ip5306_config *cfg = dev->config;
if (!cfg->has_keepalive_gpio || (data->keepalive_interval_ms == 0U)) {
data->backend = IP5306_KEEPALIVE_BACKEND_NONE;
return;
}
/*
* 选择策略:
* 1) DTS 显式请求硬件 offload 时,先尝试硬件后端;
* 2) 若硬件依赖不可用或资源被占用,则告警后回退软件后端。
*/
if (cfg->keepalive_offload) {
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
int ret = ip5306_keepalive_hw_nrf_start(dev);
if (ret == 0) {
LOG_INF("Keepalive backend=HW(nRF), pulse=%ums interval=%ums",
data->keepalive_pulse_ms, data->keepalive_interval_ms);
return;
}
LOG_WRN("HW keepalive unavailable (%d), fallback to SW backend", ret);
#else
LOG_WRN("HW keepalive requested but HW backend is not built, fallback to SW backend");
#endif
}
ip5306_keepalive_sw_start(dev);
LOG_INF("Keepalive backend=SW, pulse=%ums interval=%ums",
data->keepalive_pulse_ms, data->keepalive_interval_ms);
}
static int ip5306_init(const struct device *dev)
{
const struct ip5306_config *cfg = dev->config;
struct ip5306_data *data = dev->data;
if (!i2c_is_ready_dt(&cfg->i2c)) {
return -ENODEV;
}
if (cfg->has_keepalive_gpio) {
if (!gpio_is_ready_dt(&cfg->keepalive_gpio)) {
return -ENODEV;
}
if (gpio_pin_configure_dt(&cfg->keepalive_gpio, GPIO_OUTPUT_INACTIVE) != 0) {
return -EIO;
}
}
data->keepalive_high = false;
data->dev = dev;
data->backend = IP5306_KEEPALIVE_BACKEND_NONE;
data->keepalive_pulse_ms = (cfg->keepalive_pulse_ms != 0U) ?
cfg->keepalive_pulse_ms : IP5306_KEEPALIVE_DEFAULT_PULSE_MS;
data->keepalive_interval_ms = (cfg->keepalive_interval_ms != 0U) ?
cfg->keepalive_interval_ms : IP5306_KEEPALIVE_DEFAULT_INTERVAL_MS;
ip5306_keepalive_start(dev);
return 0;
}
static const struct ip5306_driver_api ip5306_api = {
.is_charging = ip5306_api_is_charging,
.is_charge_full = ip5306_api_is_charge_full,
};
#define IP5306_DEFINE(inst) \
static struct ip5306_data ip5306_data_##inst; \
static const struct ip5306_config ip5306_cfg_##inst = { \
.i2c = I2C_DT_SPEC_INST_GET(inst), \
.keepalive_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, keepalive_gpios, {0}), \
IP5306_CFG_KEEPALIVE_PSEL_INIT(inst) \
.keepalive_pulse_ms = DT_INST_PROP(inst, keepalive_pulse_ms), \
.keepalive_interval_ms = DT_INST_PROP(inst, keepalive_interval_ms), \
.has_keepalive_gpio = DT_INST_NODE_HAS_PROP(inst, keepalive_gpios), \
.keepalive_offload = DT_INST_PROP_OR(inst, keepalive_offload, false), \
}; \
DEVICE_DT_INST_DEFINE(inst, ip5306_init, NULL, &ip5306_data_##inst, \
&ip5306_cfg_##inst, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEVICE, \
&ip5306_api);
DT_INST_FOREACH_STATUS_OKAY(IP5306_DEFINE)

View File

@@ -0,0 +1,198 @@
#include <errno.h>
#include <hal/nrf_rtc.h>
#include <nrfx_gpiote.h>
#include <nrfx_rtc.h>
#include <helpers/nrfx_gppi.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include "ip5306_priv.h"
LOG_MODULE_DECLARE(ip5306, LOG_LEVEL_INF);
#define IP5306_KEEPALIVE_RTC_FREQUENCY_HZ 32768U
#define IP5306_KEEPALIVE_RTC_CC_HIGH 0U
#define IP5306_KEEPALIVE_RTC_CC_LOW 1U
#define IP5306_KEEPALIVE_RTC_CC_PERIOD 2U
#define IP5306_KEEPALIVE_MIN_PERIOD_TICKS 3U
/* nRF52 单核场景默认使用 GPIOTE0 + RTC2。 */
static nrfx_gpiote_t ip5306_keepalive_gpiote = NRFX_GPIOTE_INSTANCE(NRF_GPIOTE_INST_GET(0));
static const nrfx_rtc_t ip5306_keepalive_rtc = NRFX_RTC_INSTANCE(2);
static bool ip5306_keepalive_hw_claimed;
static void ip5306_keepalive_rtc_handler(nrfx_rtc_int_type_t int_type)
{
/*
* 这里故意不处理任何中断逻辑:
* 本模块的保活脉冲由 RTC event -> GPPI/PPI -> GPIOTE task 的硬件链路完成,
* 不依赖线程或 ISR 参与,从而减少 CPU 唤醒次数、降低功耗。
*/
ARG_UNUSED(int_type);
}
static uint32_t ip5306_keepalive_ms_to_rtc_ticks(uint32_t time_ms)
{
/* 四舍五入到最近 tick降低长时间运行时累计漂移。 */
uint64_t ticks = ((uint64_t)time_ms * IP5306_KEEPALIVE_RTC_FREQUENCY_HZ + 500U) / 1000U;
return (uint32_t)MAX(ticks, 1U);
}
int ip5306_keepalive_hw_nrf_start(const struct device *dev)
{
struct ip5306_data *data = dev->data;
const struct ip5306_config *cfg = dev->config;
const bool active_high = ((cfg->keepalive_gpio.dt_flags & GPIO_ACTIVE_LOW) == 0U);
const uint32_t period_ticks = ip5306_keepalive_ms_to_rtc_ticks(data->keepalive_interval_ms);
uint32_t high_ticks = ip5306_keepalive_ms_to_rtc_ticks(data->keepalive_pulse_ms);
int ret;
if (ip5306_keepalive_hw_claimed) {
return -EBUSY;
}
if (period_ticks < IP5306_KEEPALIVE_MIN_PERIOD_TICKS) {
return -EINVAL;
}
if (high_ticks >= (period_ticks - 1U)) {
high_ticks = period_ticks - 1U;
}
if (!nrfx_gpiote_init_check(&ip5306_keepalive_gpiote)) {
ret = nrfx_gpiote_init(&ip5306_keepalive_gpiote, NRFX_GPIOTE_DEFAULT_CONFIG_IRQ_PRIORITY);
if ((ret != 0) && (ret != -EALREADY)) {
return ret;
}
}
ret = nrfx_gpiote_channel_alloc(&ip5306_keepalive_gpiote, &data->keepalive_gpiote_channel);
if (ret != 0) {
/* GPIOTE 通道不足通常意味着被其他驱动占用。 */
LOG_ERR("Failed to allocate GPIOTE channel");
return ret;
}
const nrfx_gpiote_output_config_t output_config = {
.drive = NRF_GPIO_PIN_S0S1,
.input_connect = NRF_GPIO_PIN_INPUT_DISCONNECT,
.pull = NRF_GPIO_PIN_NOPULL,
};
const nrfx_gpiote_task_config_t task_config = {
.task_ch = data->keepalive_gpiote_channel,
.polarity = NRF_GPIOTE_POLARITY_TOGGLE,
.init_val = active_high ? NRF_GPIOTE_INITIAL_VALUE_LOW : NRF_GPIOTE_INITIAL_VALUE_HIGH,
};
ret = nrfx_gpiote_output_configure(&ip5306_keepalive_gpiote, cfg->keepalive_psel,
&output_config, &task_config);
if (ret != 0) {
goto err_free_channel;
}
nrfx_gpiote_out_task_enable(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
nrfx_gpiote_out_task_force(&ip5306_keepalive_gpiote, cfg->keepalive_psel, active_high ? 0U : 1U);
/*
* RTC2 初始化前先检查是否已被外部占用:
* 若占用则直接返回 -EBUSY让上层走软件保活回退。
*/
if (nrfx_rtc_init_check(&ip5306_keepalive_rtc)) {
ret = -EBUSY;
goto err_disable_task;
}
const nrfx_rtc_config_t rtc_cfg = NRFX_RTC_DEFAULT_CONFIG;
ret = nrfx_rtc_init(&ip5306_keepalive_rtc, &rtc_cfg, ip5306_keepalive_rtc_handler);
if ((ret != 0) && (ret != -EALREADY)) {
goto err_disable_task;
}
nrfx_rtc_disable(&ip5306_keepalive_rtc);
nrfx_rtc_counter_clear(&ip5306_keepalive_rtc);
/*
* RTC 角色:时间基准,按 compare 点产生事件。
* - CC0 到点 -> 产生 COMPARE0 event脉冲上升沿触发点
* - CC1 到点 -> 产生 COMPARE1 event脉冲下降沿触发点
* - CC2 到点 -> 触发周期边界,配合 SHORT 实现自动循环
*/
ret = nrfx_rtc_cc_set(&ip5306_keepalive_rtc, IP5306_KEEPALIVE_RTC_CC_HIGH, period_ticks - high_ticks - 10U, false);
if (ret != 0) {
goto err_disable_task;
}
ret = nrfx_rtc_cc_set(&ip5306_keepalive_rtc, IP5306_KEEPALIVE_RTC_CC_LOW, period_ticks - 10U, false);
if (ret != 0) {
goto err_disable_task;
}
ret = nrfx_rtc_cc_set(&ip5306_keepalive_rtc, IP5306_KEEPALIVE_RTC_CC_PERIOD, period_ticks, false);
if (ret != 0) {
goto err_disable_task;
}
const uint32_t set_task = active_high ?
nrfx_gpiote_set_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel) :
nrfx_gpiote_clr_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
const uint32_t clr_task = active_high ?
nrfx_gpiote_clr_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel) :
nrfx_gpiote_set_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
const uint32_t high_event = nrfx_rtc_event_address_get(&ip5306_keepalive_rtc, NRF_RTC_EVENT_COMPARE_0);
const uint32_t low_event = nrfx_rtc_event_address_get(&ip5306_keepalive_rtc, NRF_RTC_EVENT_COMPARE_1);
const uint32_t period_event = nrfx_rtc_event_address_get(&ip5306_keepalive_rtc, NRF_RTC_EVENT_COMPARE_2);
const uint32_t clear_task = nrfx_rtc_task_address_get(&ip5306_keepalive_rtc, NRF_RTC_TASK_CLEAR);
/*
* GPPI/PPI 角色:事件路由器。
* 将 RTC compare event 直接连接到 GPIOTE task不经过线程/ISR。
*/
ret = nrfx_gppi_conn_alloc(high_event, set_task, &data->keepalive_set_handle);
if (ret != 0) {
goto err_disable_task;
}
ret = nrfx_gppi_conn_alloc(low_event, clr_task, &data->keepalive_clr_handle);
if (ret != 0) {
nrfx_gppi_conn_free(high_event, set_task, data->keepalive_set_handle);
goto err_disable_task;
}
/*
* 某些 SoC/SDK 组合下 RTC SHORT 宏不可用,因此这里用第三条 GPPI 链路实现“周期自动清零”。
* 这样仍保持纯硬件闭环COMPARE2 event -> RTC CLEAR task不需要 CPU/ISR 参与。
*/
ret = nrfx_gppi_conn_alloc(period_event, clear_task, &data->keepalive_period_handle);
if (ret != 0) {
nrfx_gppi_conn_free(low_event, clr_task, data->keepalive_clr_handle);
nrfx_gppi_conn_free(high_event, set_task, data->keepalive_set_handle);
goto err_disable_task;
}
/*
* GPIOTE 角色GPIO 执行器。
* 收到 task 后立即对目标 pin 执行 SET/CLR输出保活脉冲波形。
*/
nrfx_gppi_conn_enable(data->keepalive_set_handle);
nrfx_gppi_conn_enable(data->keepalive_clr_handle);
nrfx_gppi_conn_enable(data->keepalive_period_handle);
nrfx_rtc_enable(&ip5306_keepalive_rtc);
ip5306_keepalive_hw_claimed = true;
data->backend = IP5306_KEEPALIVE_BACKEND_HW_NRF;
data->dev = dev;
return 0;
err_disable_task:
nrfx_gpiote_out_task_disable(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
err_free_channel:
(void)nrfx_gpiote_channel_free(&ip5306_keepalive_gpiote, data->keepalive_gpiote_channel);
return ret;
}

View File

@@ -0,0 +1,54 @@
#include <zephyr/logging/log.h>
#include <zephyr/sys/util.h>
#include "ip5306_priv.h"
LOG_MODULE_DECLARE(ip5306, LOG_LEVEL_INF);
void ip5306_keepalive_sw_work_handler(struct k_work *work)
{
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
struct ip5306_data *data = CONTAINER_OF(dwork, struct ip5306_data, keepalive_work);
const struct ip5306_config *cfg = data->dev->config;
int ret;
if ((data->backend != IP5306_KEEPALIVE_BACKEND_SW) || (data->keepalive_interval_ms == 0U))
{
return;
}
if (!data->keepalive_high)
{
ret = gpio_pin_set_dt(&cfg->keepalive_gpio, GPIO_OUTPUT_ACTIVE);
if (ret != 0)
{
LOG_ERR("SW keepalive set high failed: %d", ret);
(void)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_interval_ms));
return;
}
data->keepalive_high = true;
(void)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_pulse_ms));
}
else
{
ret = gpio_pin_set_dt(&cfg->keepalive_gpio, GPIO_OUTPUT_INACTIVE);
if (ret != 0)
{
LOG_ERR("SW keepalive set low failed: %d", ret);
}
data->keepalive_high = false;
(void)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_interval_ms));
}
}
int ip5306_keepalive_sw_start(const struct device *dev)
{
struct ip5306_data *data = dev->data;
data->backend = IP5306_KEEPALIVE_BACKEND_SW;
data->dev = dev;
k_work_init_delayable(&data->keepalive_work, ip5306_keepalive_sw_work_handler);
return (int)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_interval_ms));
}

View File

@@ -0,0 +1,58 @@
#ifndef IP5306_KEEPALIVE_PRIV_H_
#define IP5306_KEEPALIVE_PRIV_H_
#include <stdbool.h>
#include <stdint.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/util.h>
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
#include <helpers/nrfx_gppi.h>
#endif
#define IP5306_KEEPALIVE_DEFAULT_PULSE_MS 100U
#define IP5306_KEEPALIVE_DEFAULT_INTERVAL_MS 20000U
enum ip5306_keepalive_backend {
IP5306_KEEPALIVE_BACKEND_NONE,
IP5306_KEEPALIVE_BACKEND_SW,
IP5306_KEEPALIVE_BACKEND_HW_NRF,
};
struct ip5306_config {
struct i2c_dt_spec i2c;
struct gpio_dt_spec keepalive_gpio;
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
uint32_t keepalive_psel;
#endif
uint32_t keepalive_pulse_ms;
uint32_t keepalive_interval_ms;
bool has_keepalive_gpio;
bool keepalive_offload;
};
struct ip5306_data {
struct k_work_delayable keepalive_work;
bool keepalive_high;
uint32_t keepalive_pulse_ms;
uint32_t keepalive_interval_ms;
enum ip5306_keepalive_backend backend;
const struct device *dev;
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
uint8_t keepalive_gpiote_channel;
nrfx_gppi_handle_t keepalive_set_handle;
nrfx_gppi_handle_t keepalive_clr_handle;
nrfx_gppi_handle_t keepalive_period_handle;
#endif
};
int ip5306_keepalive_sw_start(const struct device *dev);
int ip5306_keepalive_hw_nrf_start(const struct device *dev);
#endif /* IP5306_KEEPALIVE_PRIV_H_ */

View File

@@ -0,0 +1,33 @@
title: Injoinic IP5306 PMIC
description: |
Injoinic IP5306 power management IC accessed over I2C.
This binding exposes two status bits and an optional KEY pin keepalive
pulse output.
compatible: "injoinic,ip5306"
include: i2c-device.yaml
properties:
keepalive-gpios:
type: phandle-array
description: |
GPIO connected to KEY pin. The driver sends periodic pulses on this pin
to keep boost output alive.
keepalive-pulse-ms:
type: int
default: 100
description: Pulse width in milliseconds for each keepalive pulse.
keepalive-interval-ms:
type: int
default: 20000
description: Interval in milliseconds between keepalive pulses.
keepalive-offload:
type: boolean
description: |
Prefer hardware keepalive offload backend when available on this SoC.
If not set, driver always uses software keepalive backend.

View File

@@ -0,0 +1 @@
injoinic Injoinic

View File

@@ -0,0 +1,48 @@
#ifndef ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_
#define ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_
#include <stdbool.h>
#include <errno.h>
#include <zephyr/device.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef int (*ip5306_get_flag_t)(const struct device *dev, bool *flag);
struct ip5306_driver_api {
ip5306_get_flag_t is_charging;
ip5306_get_flag_t is_charge_full;
};
static inline int ip5306_is_charging(const struct device *dev, bool *charging)
{
const struct ip5306_driver_api *api =
(const struct ip5306_driver_api *)dev->api;
if (api == NULL || api->is_charging == NULL) {
return -ENOSYS;
}
return api->is_charging(dev, charging);
}
static inline int ip5306_is_charge_full(const struct device *dev, bool *full)
{
const struct ip5306_driver_api *api =
(const struct ip5306_driver_api *)dev->api;
if (api == NULL || api->is_charge_full == NULL) {
return -ENOSYS;
}
return api->is_charge_full(dev, full);
}
#ifdef __cplusplus
}
#endif
#endif /* ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_ */

View File

@@ -0,0 +1,5 @@
build:
cmake: .
kconfig: Kconfig
settings:
dts_root: .

33
pm_static.yml Normal file
View File

@@ -0,0 +1,33 @@
mcuboot:
address: 0x0
end_address: 0x10000
region: flash_primary
size: 0x10000
mcuboot_pad:
address: 0x10000
end_address: 0x10200
region: flash_primary
size: 0x200
app:
address: 0x10200
end_address: 0xf8000
region: flash_primary
size: 0xe7e00
mcuboot_primary:
address: 0x10000
end_address: 0xf8000
orig_span: &id001
- mcuboot_pad
- app
region: flash_primary
size: 0xe8000
span: *id001
settings_storage:
address: 0xf8000
end_address: 0x100000
region: flash_primary
size: 0x8000

118
prj.conf
View File

@@ -1,3 +1,119 @@
CONFIG_CAF=y
CONFIG_HEAP_MEM_POOL_SIZE=2048
CONFIG_LOG=y
CONFIG_LOG=y
CONFIG_ASSERT=y
CONFIG_ASSERT_VERBOSE=y
CONFIG_RESET_ON_FATAL_ERROR=n
CONFIG_FAULT_DUMP=2
CONFIG_SIZE_OPTIMIZATIONS=y
CONFIG_ZCBOR=n
CONFIG_BOOTLOADER_MCUBOOT=n
CONFIG_MCUMGR=n
CONFIG_MCUMGR_TRANSPORT_BT=n
CONFIG_MCUMGR_GRP_IMG=n
CONFIG_MCUMGR_GRP_OS=n
CONFIG_IMG_MANAGER=n
CONFIG_STREAM_FLASH=n
CONFIG_RETAINED_MEM=y
CONFIG_RETENTION=y
CONFIG_RETENTION_BOOT_MODE=y
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_SMP=y
CONFIG_BT_PERIPHERAL_PREF_TIMEOUT=400
CONFIG_BT_DEVICE_NAME="new_kbd"
CONFIG_BT_DEVICE_APPEARANCE=961
CONFIG_BT_MAX_CONN=1
CONFIG_BT_MAX_PAIRED=4
CONFIG_BT_ID_MAX=4
CONFIG_SETTINGS=y
CONFIG_SETTINGS_NVS=y
CONFIG_NVS=y
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_BT_SETTINGS=y
CONFIG_CAF_BLE_STATE=y
CONFIG_CAF_BLE_ADV=y
CONFIG_CAF_BLE_ADV_FAST_ADV=y
CONFIG_CAF_BLE_ADV_FAST_INT_MIN=0x0030
CONFIG_CAF_BLE_ADV_FAST_INT_MAX=0x0060
CONFIG_CAF_BLE_ADV_FAST_ADV_TIMEOUT=180
CONFIG_CAF_MODULE_SUSPEND_EVENTS=y
CONFIG_CAF_SETTINGS_LOADER=y
CONFIG_BT_ADV_PROV_FLAGS=y
CONFIG_BT_ADV_PROV_GAP_APPEARANCE=y
CONFIG_BT_ADV_PROV_DEVICE_NAME=y
CONFIG_BT_ADV_PROV_DEVICE_NAME_PAIRING_MODE_ONLY=n
CONFIG_BT_ADV_PROV_SWIFT_PAIR=y
CONFIG_BT_HIDS=y
CONFIG_BT_CONN_CTX=y
CONFIG_BT_GATT_POOL=y
CONFIG_BT_GATT_CHRC_POOL_SIZE=16
CONFIG_BT_GATT_UUID16_POOL_SIZE=24
CONFIG_BT_HIDS_ATTR_MAX=40
CONFIG_BT_HIDS_INPUT_REP_MAX=4
CONFIG_BT_HIDS_OUTPUT_REP_MAX=3
CONFIG_BT_HIDS_FEATURE_REP_MAX=0
CONFIG_BT_BAS=y
CONFIG_BT_DIS=y
CONFIG_BT_DIS_PNP=y
CONFIG_BT_DIS_PNP_VID_SRC=2
CONFIG_BT_DIS_PNP_VID=0x1209
CONFIG_BT_DIS_PNP_PID=0x0001
CONFIG_USB_DEVICE_STACK_NEXT=y
CONFIG_USBD_HID_SUPPORT=y
CONFIG_UDC_BUF_POOL_SIZE=8192
CONFIG_UDC_BUF_COUNT=32
CONFIG_USBD_MAX_UDC_MSG=20
CONFIG_USBD_MSG_SLAB_COUNT=16
CONFIG_LED=y
CONFIG_LED_GPIO=y
CONFIG_CAF_LEDS=y
CONFIG_CAF_LEDS_GPIO=y
CONFIG_CAF_LEDS_PM_EVENTS=y
CONFIG_CAF_POWER_MANAGER=y
CONFIG_CAF_POWER_MANAGER_TIMEOUT=30
CONFIG_CAF_POWER_MANAGER_ERROR_TIMEOUT=10
CONFIG_REBOOT=y
CONFIG_CAF_KEEP_ALIVE_EVENTS=y
CONFIG_CAF_BUTTONS=y
CONFIG_CAF_BUTTONS_DEF_PATH="buttons_def.h"
CONFIG_CAF_BUTTONS_SCAN_INTERVAL=5
CONFIG_CAF_BUTTONS_DEBOUNCE_INTERVAL=10
CONFIG_ADC=y
CONFIG_I2C=y
CONFIG_IP5306=y
CONFIG_SENSOR=y
CONFIG_DISPLAY=y
CONFIG_MIPI_DBI=y
CONFIG_ST7789V=y
CONFIG_LVGL=y
CONFIG_LV_CONF_MINIMAL=y
CONFIG_LV_COLOR_16_SWAP=y
CONFIG_LV_BUILD_EXAMPLES=n
CONFIG_LV_BUILD_DEMOS=n
CONFIG_LV_USE_LABEL=y
CONFIG_LV_USE_FLEX=y
CONFIG_LV_USE_FONT_COMPRESSED=y
CONFIG_LV_TXT_ENC_UTF8=y
CONFIG_LV_Z_AUTO_INIT=y
CONFIG_LV_Z_VDB_SIZE=25
CONFIG_LV_Z_BITS_PER_PIXEL=16
CONFIG_LV_Z_LVGL_MUTEX=y
CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE=y
CONFIG_LV_Z_LVGL_WORKQUEUE_STACK_SIZE=8192
CONFIG_LV_Z_FLUSH_THREAD=y
CONFIG_LV_Z_DOUBLE_VDB=y
CONFIG_LV_Z_MEM_POOL_SIZE=16384
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096

View File

@@ -0,0 +1,33 @@
#include "battery_status_event.h"
static void log_battery_status_event(const struct app_event_header *aeh)
{
const struct battery_status_event *event = cast_battery_status_event(aeh);
APP_EVENT_MANAGER_LOG(aeh,
"flags=0x%02x charging=%u full=%u soc=%u",
event->flags,
BATTERY_STATUS_IS_CHARGING(event->flags),
BATTERY_STATUS_IS_FULL(event->flags),
event->soc);
}
static void profile_battery_status_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct battery_status_event *event = cast_battery_status_event(aeh);
nrf_profiler_log_encode_uint8(buf, event->flags);
nrf_profiler_log_encode_uint8(buf, event->soc);
}
APP_EVENT_INFO_DEFINE(battery_status_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("flags", "soc"),
profile_battery_status_event);
APP_EVENT_TYPE_DEFINE(battery_status_event,
log_battery_status_event,
&battery_status_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,61 @@
#ifndef BATTERY_STATUS_EVENT_H
#define BATTERY_STATUS_EVENT_H
#include <stdbool.h>
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
struct battery_status_event
{
struct app_event_header header;
uint8_t flags;
uint8_t soc;
};
#define BATTERY_STATUS_FLAG_CHARGING (1U << 0)
#define BATTERY_STATUS_FLAG_FULL (1U << 1)
#define BATTERY_STATUS_IS_CHARGING(flags) (((flags) & BATTERY_STATUS_FLAG_CHARGING) != 0U)
#define BATTERY_STATUS_IS_FULL(flags) (((flags) & BATTERY_STATUS_FLAG_FULL) != 0U)
APP_EVENT_TYPE_DECLARE(battery_status_event);
static inline void battery_status_event_submit(bool charging, bool full, uint8_t soc)
{
struct battery_status_event *event = new_battery_status_event();
event->flags = 0U;
if (charging) {
event->flags |= BATTERY_STATUS_FLAG_CHARGING;
}
if (full) {
event->flags |= BATTERY_STATUS_FLAG_FULL;
}
event->soc = soc;
APP_EVENT_SUBMIT(event);
}
static inline uint8_t battery_status_event_get_flags(const struct battery_status_event *event)
{
return event->flags;
}
static inline bool battery_status_event_is_charging(const struct battery_status_event *event)
{
return BATTERY_STATUS_IS_CHARGING(event->flags);
}
static inline bool battery_status_event_is_full(const struct battery_status_event *event)
{
return BATTERY_STATUS_IS_FULL(event->flags);
}
static inline uint8_t battery_status_event_get_soc(const struct battery_status_event *event)
{
return event->soc;
}
#endif

18
src/events/config_event.c Normal file
View File

@@ -0,0 +1,18 @@
#include "config_event.h"
static void log_config_event(const struct app_event_header *aeh)
{
const struct config_event *event = cast_config_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "status:%u %s rcpt:%02x id:%02x",
event->status,
event->is_request ? "req" : "rsp",
event->recipient,
event->event_id);
}
APP_EVENT_TYPE_DEFINE(config_event,
log_config_event,
NULL,
APP_EVENT_FLAGS_CREATE(
APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

54
src/events/config_event.h Normal file
View File

@@ -0,0 +1,54 @@
/*
* Lightweight config event used for local module configuration.
*/
#ifndef NEW_KBD_CONFIG_EVENT_H__
#define NEW_KBD_CONFIG_EVENT_H__
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Keep the same local recipient as nRF Desktop config channel. */
#define CFG_CHAN_RECIPIENT_LOCAL 0x00
/* Event ID field layout (compatible with nRF Desktop config_event encoding). */
#define MOD_FIELD_POS 4
#define MOD_FIELD_SIZE 4
#define MOD_FIELD_MASK BIT_MASK(MOD_FIELD_SIZE)
#define MOD_FIELD_GET(id) (((id) >> MOD_FIELD_POS) & MOD_FIELD_MASK)
#define OPT_FIELD_POS 0
#define OPT_FIELD_SIZE 4
#define OPT_FIELD_MASK BIT_MASK(OPT_FIELD_SIZE)
#define OPT_FIELD_GET(id) (((id) >> OPT_FIELD_POS) & OPT_FIELD_MASK)
#define OPT_ID_GET(opt) ((opt) - 1U)
enum config_status {
CONFIG_STATUS_SET = 0,
CONFIG_STATUS_FETCH,
CONFIG_STATUS_SUCCESS,
CONFIG_STATUS_REJECT,
};
struct config_event {
struct app_event_header header;
uint16_t transport_id;
bool is_request;
uint8_t event_id;
uint8_t recipient;
uint8_t status;
struct event_dyndata dyndata;
};
APP_EVENT_TYPE_DYNDATA_DECLARE(config_event);
#ifdef __cplusplus
}
#endif
#endif /* NEW_KBD_CONFIG_EVENT_H__ */

View File

@@ -0,0 +1,31 @@
#include "display_theme_event.h"
static void log_display_theme_event(const struct app_event_header *aeh)
{
const struct display_theme_event *event = cast_display_theme_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "rgb=(%u,%u,%u)",
event->red, event->green, event->blue);
}
static void profile_display_theme_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct display_theme_event *event = cast_display_theme_event(aeh);
nrf_profiler_log_encode_uint8(buf, event->red);
nrf_profiler_log_encode_uint8(buf, event->green);
nrf_profiler_log_encode_uint8(buf, event->blue);
}
APP_EVENT_INFO_DEFINE(display_theme_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("red", "green", "blue"),
profile_display_theme_event);
APP_EVENT_TYPE_DEFINE(display_theme_event,
log_display_theme_event,
&display_theme_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,30 @@
#ifndef DISPLAY_THEME_EVENT_H__
#define DISPLAY_THEME_EVENT_H__
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
struct display_theme_event {
struct app_event_header header;
uint8_t red;
uint8_t green;
uint8_t blue;
};
APP_EVENT_TYPE_DECLARE(display_theme_event);
static inline void display_theme_event_submit(uint8_t red,
uint8_t green,
uint8_t blue)
{
struct display_theme_event *event = new_display_theme_event();
event->red = red;
event->green = green;
event->blue = blue;
APP_EVENT_SUBMIT(event);
}
#endif /* DISPLAY_THEME_EVENT_H__ */

View File

@@ -0,0 +1,26 @@
#include "hid_boot_event.h"
static void log_hid_boot_event(const struct app_event_header *aeh)
{
const struct hid_boot_event *event = cast_hid_boot_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "payload_len=%u", event->dyndata.size);
}
static void profile_hid_boot_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_boot_event *event = cast_hid_boot_event(aeh);
nrf_profiler_log_encode_uint16(buf, event->dyndata.size);
}
APP_EVENT_INFO_DEFINE(hid_boot_event,
ENCODE(NRF_PROFILER_ARG_U16),
ENCODE("len"),
profile_hid_boot_event);
APP_EVENT_TYPE_DEFINE(hid_boot_event,
log_hid_boot_event,
&hid_boot_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,45 @@
#ifndef HID_BOOT_EVENT_H__
#define HID_BOOT_EVENT_H__
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
/*
* HID Boot 输入报告事件:
* - dyndata 仅包含 boot keyboard payload
* - 不包含 report_id消费方固定按 boot keyboard 语义处理。
*/
struct hid_boot_event {
struct app_event_header header;
struct event_dyndata dyndata;
};
APP_EVENT_TYPE_DYNDATA_DECLARE(hid_boot_event);
static inline void hid_boot_event_submit(const uint8_t *data, size_t size)
{
struct hid_boot_event *event = new_hid_boot_event(size);
if ((size > 0U) && (data != NULL)) {
memcpy(event->dyndata.data, data, size);
}
APP_EVENT_SUBMIT(event);
}
static inline const uint8_t *hid_boot_event_get_data(const struct hid_boot_event *event)
{
return event->dyndata.data;
}
static inline size_t hid_boot_event_get_size(const struct hid_boot_event *event)
{
return event->dyndata.size;
}
#endif /* HID_BOOT_EVENT_H__ */

View File

@@ -0,0 +1,29 @@
#include "hid_host_ack_event.h"
static void log_hid_host_ack_event(const struct app_event_header *aeh)
{
const struct hid_host_ack_event *event = cast_hid_host_ack_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "transport=%u cmd=0x%02x",
event->transport, event->cmd);
}
static void profile_hid_host_ack_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_host_ack_event *event = cast_hid_host_ack_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->transport);
nrf_profiler_log_encode_uint8(buf, event->cmd);
}
APP_EVENT_INFO_DEFINE(hid_host_ack_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("transport", "cmd"),
profile_hid_host_ack_event);
APP_EVENT_TYPE_DEFINE(hid_host_ack_event,
log_hid_host_ack_event,
&hid_host_ack_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,29 @@
#ifndef HID_HOST_ACK_EVENT_H__
#define HID_HOST_ACK_EVENT_H__
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#include "hid_host_transport.h"
struct hid_host_ack_event {
struct app_event_header header;
enum hid_host_transport transport;
uint8_t cmd;
};
APP_EVENT_TYPE_DECLARE(hid_host_ack_event);
static inline void hid_host_ack_event_submit(enum hid_host_transport transport,
uint8_t cmd)
{
struct hid_host_ack_event *event = new_hid_host_ack_event();
event->transport = transport;
event->cmd = cmd;
APP_EVENT_SUBMIT(event);
}
#endif /* HID_HOST_ACK_EVENT_H__ */

View File

@@ -0,0 +1,36 @@
#include "hid_host_command_error_event.h"
static void log_hid_host_command_error_event(const struct app_event_header *aeh)
{
const struct hid_host_command_error_event *event =
cast_hid_host_command_error_event(aeh);
APP_EVENT_MANAGER_LOG(aeh,
"transport=%u cmd=0x%02x reason=%u",
event->transport,
event->cmd,
event->reason);
}
static void profile_hid_host_command_error_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_host_command_error_event *event =
cast_hid_host_command_error_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->transport);
nrf_profiler_log_encode_uint8(buf, event->cmd);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->reason);
}
APP_EVENT_INFO_DEFINE(hid_host_command_error_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("transport", "cmd", "reason"),
profile_hid_host_command_error_event);
APP_EVENT_TYPE_DEFINE(hid_host_command_error_event,
log_hid_host_command_error_event,
&hid_host_command_error_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,41 @@
#ifndef HID_HOST_COMMAND_ERROR_EVENT_H__
#define HID_HOST_COMMAND_ERROR_EVENT_H__
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#include "hid_host_transport.h"
enum hid_host_command_error_reason {
HID_HOST_COMMAND_ERROR_UNKNOWN_CMD = 0,
HID_HOST_COMMAND_ERROR_INVALID_LENGTH,
HID_HOST_COMMAND_ERROR_INVALID_PARAM,
HID_HOST_COMMAND_ERROR_NOT_READY,
};
struct hid_host_command_error_event {
struct app_event_header header;
enum hid_host_transport transport;
enum hid_host_command_error_reason reason;
uint8_t cmd;
};
APP_EVENT_TYPE_DECLARE(hid_host_command_error_event);
static inline void hid_host_command_error_event_submit(
enum hid_host_transport transport,
uint8_t cmd,
enum hid_host_command_error_reason reason)
{
struct hid_host_command_error_event *event =
new_hid_host_command_error_event();
event->transport = transport;
event->cmd = cmd;
event->reason = reason;
APP_EVENT_SUBMIT(event);
}
#endif /* HID_HOST_COMMAND_ERROR_EVENT_H__ */

View File

@@ -0,0 +1,47 @@
#include "hid_host_command_event.h"
static const char *transport_name(enum hid_host_transport transport)
{
switch (transport) {
case HID_HOST_TRANSPORT_USB:
return "usb";
case HID_HOST_TRANSPORT_BLE:
return "ble";
default:
return "unknown";
}
}
static void log_hid_host_command_event(const struct app_event_header *aeh)
{
const struct hid_host_command_event *event =
cast_hid_host_command_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "transport=%s cmd=0x%02x data_len=%u",
transport_name(event->transport),
event->cmd,
event->data_len);
}
static void profile_hid_host_command_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_host_command_event *event =
cast_hid_host_command_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->transport);
nrf_profiler_log_encode_uint8(buf, event->cmd);
nrf_profiler_log_encode_uint8(buf, event->data_len);
}
APP_EVENT_INFO_DEFINE(hid_host_command_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("transport", "cmd", "data_len"),
profile_hid_host_command_event);
APP_EVENT_TYPE_DEFINE(hid_host_command_event,
log_hid_host_command_event,
&hid_host_command_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,44 @@
#ifndef HID_HOST_COMMAND_EVENT_H__
#define HID_HOST_COMMAND_EVENT_H__
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#include <zephyr/sys/util.h>
#include "hid_host_command_protocol.h"
#include "hid_host_transport.h"
struct hid_host_command_event {
struct app_event_header header;
enum hid_host_transport transport;
uint8_t cmd;
uint8_t data_len;
uint8_t data[HID_HOST_CMD_DATA_SIZE];
};
APP_EVENT_TYPE_DECLARE(hid_host_command_event);
static inline void hid_host_command_event_submit(enum hid_host_transport transport,
uint8_t cmd,
const uint8_t *data,
size_t data_len)
{
struct hid_host_command_event *event = new_hid_host_command_event();
size_t copy_len = MIN(data_len, (size_t)HID_HOST_CMD_DATA_SIZE);
event->transport = transport;
event->cmd = cmd;
event->data_len = (uint8_t)copy_len;
memset(event->data, 0, sizeof(event->data));
if ((copy_len > 0U) && (data != NULL)) {
memcpy(event->data, data, copy_len);
}
APP_EVENT_SUBMIT(event);
}
#endif /* HID_HOST_COMMAND_EVENT_H__ */

View File

@@ -0,0 +1,34 @@
#include "hid_protocol_event.h"
static const char *const hid_protocol_name[] = {
[HID_PROTO_BOOT] = "BOOT",
[HID_PROTO_REPORT] = "REPORT",
};
static void log_hid_protocol_event(const struct app_event_header *aeh)
{
const struct hid_protocol_event *event = cast_hid_protocol_event(aeh);
__ASSERT_NO_MSG(event->protocol < ARRAY_SIZE(hid_protocol_name));
APP_EVENT_MANAGER_LOG(aeh, "protocol=%s",
hid_protocol_name[event->protocol]);
}
static void profile_hid_protocol_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_protocol_event *event = cast_hid_protocol_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->protocol);
}
APP_EVENT_INFO_DEFINE(hid_protocol_event,
ENCODE(NRF_PROFILER_ARG_U8),
ENCODE("protocol"),
profile_hid_protocol_event);
APP_EVENT_TYPE_DEFINE(hid_protocol_event,
log_hid_protocol_event,
&hid_protocol_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,49 @@
#ifndef HID_PROTOCOL_EVENT_H__
#define HID_PROTOCOL_EVENT_H__
#include <stdbool.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
enum hid_protocol_type {
HID_PROTO_BOOT = 0,
HID_PROTO_REPORT,
};
/*
* HID 传输层在收到主机 set_protocol 请求后上报该事件。
*/
struct hid_protocol_event {
struct app_event_header header;
enum hid_protocol_type protocol;
};
APP_EVENT_TYPE_DECLARE(hid_protocol_event);
static inline void hid_protocol_event_submit(enum hid_protocol_type protocol)
{
struct hid_protocol_event *event = new_hid_protocol_event();
event->protocol = protocol;
APP_EVENT_SUBMIT(event);
}
static inline enum hid_protocol_type hid_protocol_event_get_protocol(
const struct hid_protocol_event *event)
{
return event->protocol;
}
static inline bool hid_protocol_event_is_boot_protocol(const struct hid_protocol_event *event)
{
return event->protocol == HID_PROTO_BOOT;
}
static inline bool hid_protocol_event_is_report_protocol(const struct hid_protocol_event *event)
{
return event->protocol == HID_PROTO_REPORT;
}
#endif /* HID_PROTOCOL_EVENT_H__ */

View File

@@ -0,0 +1,42 @@
#include "hid_report_event.h"
static void log_hid_report_event(const struct app_event_header *aeh)
{
const struct hid_report_event *event = cast_hid_report_event(aeh);
uint8_t report_id = 0x00;
uint16_t payload_len = 0U;
if (event->dyndata.size >= 1U) {
report_id = event->dyndata.data[0];
payload_len = event->dyndata.size - 1U;
}
APP_EVENT_MANAGER_LOG(aeh, "report_id=0x%02x payload_len=%u",
report_id,
payload_len);
}
static void profile_hid_report_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_report_event *event = cast_hid_report_event(aeh);
uint8_t report_id = 0x00;
if (event->dyndata.size >= 1U) {
report_id = event->dyndata.data[0];
}
nrf_profiler_log_encode_uint8(buf, report_id);
nrf_profiler_log_encode_uint16(buf, event->dyndata.size);
}
APP_EVENT_INFO_DEFINE(hid_report_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U16),
ENCODE("report_id", "len"),
profile_hid_report_event);
APP_EVENT_TYPE_DEFINE(hid_report_event,
log_hid_report_event,
&hid_report_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,45 @@
#ifndef HID_REPORT_EVENT_H__
#define HID_REPORT_EVENT_H__
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
/*
* HID Report 输入报告事件:
* - dyndata 编码为 [report_id | payload]
* - 仅承载 Report protocol 报文。
*/
struct hid_report_event {
struct app_event_header header;
struct event_dyndata dyndata;
};
APP_EVENT_TYPE_DYNDATA_DECLARE(hid_report_event);
static inline void hid_report_event_submit(const uint8_t *data, size_t size)
{
struct hid_report_event *event = new_hid_report_event(size);
if ((size > 0U) && (data != NULL)) {
memcpy(event->dyndata.data, data, size);
}
APP_EVENT_SUBMIT(event);
}
static inline const uint8_t *hid_report_event_get_data(const struct hid_report_event *event)
{
return event->dyndata.data;
}
static inline size_t hid_report_event_get_size(const struct hid_report_event *event)
{
return event->dyndata.size;
}
#endif /* HID_REPORT_EVENT_H__ */

View File

@@ -0,0 +1,29 @@
#include "hid_tx_done_event.h"
static void log_hid_tx_done_event(const struct app_event_header *aeh)
{
const struct hid_tx_done_event *event = cast_hid_tx_done_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "kind=%u success=%u",
event->kind, event->success);
}
static void profile_hid_tx_done_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_tx_done_event *event = cast_hid_tx_done_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->kind);
nrf_profiler_log_encode_uint8(buf, event->success ? 1U : 0U);
}
APP_EVENT_INFO_DEFINE(hid_tx_done_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8),
ENCODE("kind", "success"),
profile_hid_tx_done_event);
APP_EVENT_TYPE_DEFINE(hid_tx_done_event,
log_hid_tx_done_event,
&hid_tx_done_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,28 @@
#ifndef HID_TX_DONE_EVENT_H__
#define HID_TX_DONE_EVENT_H__
#include <stdbool.h>
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#include "hid_tx_event.h"
struct hid_tx_done_event {
struct app_event_header header;
enum hid_tx_kind kind;
bool success;
};
APP_EVENT_TYPE_DECLARE(hid_tx_done_event);
static inline void hid_tx_done_event_submit(enum hid_tx_kind kind, bool success)
{
struct hid_tx_done_event *event = new_hid_tx_done_event();
event->kind = kind;
event->success = success;
APP_EVENT_SUBMIT(event);
}
#endif /* HID_TX_DONE_EVENT_H__ */

49
src/events/hid_tx_event.c Normal file
View File

@@ -0,0 +1,49 @@
#include "hid_tx_event.h"
static void log_hid_tx_event(const struct app_event_header *aeh)
{
const struct hid_tx_event *event = cast_hid_tx_event(aeh);
uint8_t report_id = 0x00;
uint16_t payload_len = event->dyndata.size;
if ((event->kind == HID_TX_KIND_REPORT) && (event->dyndata.size >= 1U)) {
report_id = event->dyndata.data[0];
payload_len = event->dyndata.size - 1U;
}
APP_EVENT_MANAGER_LOG(aeh,
"kind=%u route=%u report_id=0x%02x payload_len=%u",
event->kind,
event->route,
report_id,
payload_len);
}
static void profile_hid_tx_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_tx_event *event = cast_hid_tx_event(aeh);
uint8_t report_id = 0x00;
if ((event->kind == HID_TX_KIND_REPORT) && (event->dyndata.size >= 1U)) {
report_id = event->dyndata.data[0];
}
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->kind);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->route);
nrf_profiler_log_encode_uint8(buf, report_id);
nrf_profiler_log_encode_uint16(buf, event->dyndata.size);
}
APP_EVENT_INFO_DEFINE(hid_tx_event,
ENCODE(NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U8,
NRF_PROFILER_ARG_U16),
ENCODE("kind", "route", "report_id", "len"),
profile_hid_tx_event);
APP_EVENT_TYPE_DEFINE(hid_tx_event,
log_hid_tx_event,
&hid_tx_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

69
src/events/hid_tx_event.h Normal file
View File

@@ -0,0 +1,69 @@
#ifndef HID_TX_EVENT_H__
#define HID_TX_EVENT_H__
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
enum hid_tx_kind {
HID_TX_KIND_BOOT = 0,
HID_TX_KIND_REPORT,
};
enum hid_tx_route {
HID_TX_ROUTE_AUTO = 0,
HID_TX_ROUTE_USB,
HID_TX_ROUTE_BLE,
};
struct hid_tx_event {
struct app_event_header header;
enum hid_tx_kind kind;
enum hid_tx_route route;
struct event_dyndata dyndata;
};
APP_EVENT_TYPE_DYNDATA_DECLARE(hid_tx_event);
static inline void hid_tx_event_submit_routed(enum hid_tx_kind kind,
enum hid_tx_route route,
const uint8_t *data,
size_t size)
{
struct hid_tx_event *event = new_hid_tx_event(size);
event->kind = kind;
event->route = route;
if ((size > 0U) && (data != NULL)) {
memcpy(event->dyndata.data, data, size);
}
APP_EVENT_SUBMIT(event);
}
static inline void hid_tx_event_submit(enum hid_tx_kind kind,
const uint8_t *data,
size_t size)
{
hid_tx_event_submit_routed(kind, HID_TX_ROUTE_AUTO, data, size);
}
static inline const uint8_t *hid_tx_event_get_data(const struct hid_tx_event *event)
{
return event->dyndata.data;
}
static inline size_t hid_tx_event_get_size(const struct hid_tx_event *event)
{
return event->dyndata.size;
}
static inline enum hid_tx_route hid_tx_event_get_route(const struct hid_tx_event *event)
{
return event->route;
}
#endif /* HID_TX_EVENT_H__ */

View File

@@ -0,0 +1,26 @@
#include "hid_vendor_mask_event.h"
static void log_hid_vendor_mask_event(const struct app_event_header *aeh)
{
const struct hid_vendor_mask_event *event = cast_hid_vendor_mask_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "len=%u", event->dyndata.size);
}
static void profile_hid_vendor_mask_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct hid_vendor_mask_event *event = cast_hid_vendor_mask_event(aeh);
nrf_profiler_log_encode_uint16(buf, event->dyndata.size);
}
APP_EVENT_INFO_DEFINE(hid_vendor_mask_event,
ENCODE(NRF_PROFILER_ARG_U16),
ENCODE("len"),
profile_hid_vendor_mask_event);
APP_EVENT_TYPE_DEFINE(hid_vendor_mask_event,
log_hid_vendor_mask_event,
&hid_vendor_mask_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,39 @@
#ifndef HID_VENDOR_MASK_EVENT_H__
#define HID_VENDOR_MASK_EVENT_H__
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
struct hid_vendor_mask_event {
struct app_event_header header;
struct event_dyndata dyndata;
};
APP_EVENT_TYPE_DYNDATA_DECLARE(hid_vendor_mask_event);
static inline void hid_vendor_mask_event_submit(const uint8_t *data, size_t size)
{
struct hid_vendor_mask_event *event = new_hid_vendor_mask_event(size);
if ((size > 0U) && (data != NULL)) {
memcpy(event->dyndata.data, data, size);
}
APP_EVENT_SUBMIT(event);
}
static inline const uint8_t *hid_vendor_mask_event_get_data(const struct hid_vendor_mask_event *event)
{
return event->dyndata.data;
}
static inline size_t hid_vendor_mask_event_get_size(const struct hid_vendor_mask_event *event)
{
return event->dyndata.size;
}
#endif /* HID_VENDOR_MASK_EVENT_H__ */

View File

@@ -0,0 +1,28 @@
#include "keyboard_led_event.h"
static void log_keyboard_led_event(const struct app_event_header *aeh)
{
const struct keyboard_led_event *event =
cast_keyboard_led_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "mask=0x%02x", event->led_mask);
}
static void profile_keyboard_led_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct keyboard_led_event *event =
cast_keyboard_led_event(aeh);
nrf_profiler_log_encode_uint8(buf, event->led_mask);
}
APP_EVENT_INFO_DEFINE(keyboard_led_event,
ENCODE(NRF_PROFILER_ARG_U8),
ENCODE("led_mask"),
profile_keyboard_led_event);
APP_EVENT_TYPE_DEFINE(keyboard_led_event,
log_keyboard_led_event,
&keyboard_led_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,73 @@
#ifndef KEYBOARD_LED_EVENT_H__
#define KEYBOARD_LED_EVENT_H__
#include <stdbool.h>
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
/*
* 键盘 LED 事件:
* - 由 USB/BLE HID 接收主机输出报告后上报;
* - 仅保留主机下发的原始 LED mask消费方按需解码。
*/
struct keyboard_led_event {
struct app_event_header header;
uint8_t led_mask;
};
#define KEYBOARD_LED_MASK_NUM_LOCK (1U << 0)
#define KEYBOARD_LED_MASK_CAPS_LOCK (1U << 1)
#define KEYBOARD_LED_MASK_SCROLL_LOCK (1U << 2)
#define KEYBOARD_LED_MASK_COMPOSE (1U << 3)
#define KEYBOARD_LED_MASK_KANA (1U << 4)
#define KEYBOARD_LED_NUM_LOCK(mask) (((mask) & KEYBOARD_LED_MASK_NUM_LOCK) != 0U)
#define KEYBOARD_LED_CAPS_LOCK(mask) (((mask) & KEYBOARD_LED_MASK_CAPS_LOCK) != 0U)
#define KEYBOARD_LED_SCROLL_LOCK(mask) (((mask) & KEYBOARD_LED_MASK_SCROLL_LOCK) != 0U)
#define KEYBOARD_LED_COMPOSE(mask) (((mask) & KEYBOARD_LED_MASK_COMPOSE) != 0U)
#define KEYBOARD_LED_KANA(mask) (((mask) & KEYBOARD_LED_MASK_KANA) != 0U)
APP_EVENT_TYPE_DECLARE(keyboard_led_event);
static inline void keyboard_led_event_submit(uint8_t led_mask)
{
struct keyboard_led_event *event = new_keyboard_led_event();
event->led_mask = led_mask;
APP_EVENT_SUBMIT(event);
}
static inline uint8_t keyboard_led_event_get_mask(const struct keyboard_led_event *event)
{
return event->led_mask;
}
static inline bool keyboard_led_event_is_num_lock_on(const struct keyboard_led_event *event)
{
return KEYBOARD_LED_NUM_LOCK(event->led_mask);
}
static inline bool keyboard_led_event_is_caps_lock_on(const struct keyboard_led_event *event)
{
return KEYBOARD_LED_CAPS_LOCK(event->led_mask);
}
static inline bool keyboard_led_event_is_scroll_lock_on(const struct keyboard_led_event *event)
{
return KEYBOARD_LED_SCROLL_LOCK(event->led_mask);
}
static inline bool keyboard_led_event_is_compose_on(const struct keyboard_led_event *event)
{
return KEYBOARD_LED_COMPOSE(event->led_mask);
}
static inline bool keyboard_led_event_is_kana_on(const struct keyboard_led_event *event)
{
return KEYBOARD_LED_KANA(event->led_mask);
}
#endif /* KEYBOARD_LED_EVENT_H__ */

34
src/events/mode_event.c Normal file
View File

@@ -0,0 +1,34 @@
#include "mode_event.h"
static const char *const mode_name[] = {
[MODE_TYPE_USB] = "USB",
[MODE_TYPE_BLE] = "BLE",
[MODE_TYPE_2G4] = "2.4G",
};
static void log_mode_event(const struct app_event_header *aeh)
{
const struct mode_event *event = cast_mode_event(aeh);
__ASSERT_NO_MSG(event->mode_type < MODE_TYPE_COUNT);
APP_EVENT_MANAGER_LOG(aeh, "mode=%s(%u)", mode_name[event->mode_type], event->mode_type);
}
static void profile_mode_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct mode_event *event = cast_mode_event(aeh);
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->mode_type);
}
APP_EVENT_INFO_DEFINE(mode_event,
ENCODE(NRF_PROFILER_ARG_U8),
ENCODE("mode"),
profile_mode_event);
APP_EVENT_TYPE_DEFINE(mode_event,
log_mode_event,
&mode_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

53
src/events/mode_event.h Normal file
View File

@@ -0,0 +1,53 @@
#ifndef MODE_EVENT_H
#define MODE_EVENT_H
#include <stdbool.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
typedef enum
{
MODE_TYPE_USB,
MODE_TYPE_BLE,
MODE_TYPE_2G4,
MODE_TYPE_COUNT,
} mode_type_t;
struct mode_event
{
struct app_event_header header;
mode_type_t mode_type;
};
APP_EVENT_TYPE_DECLARE(mode_event);
static inline void mode_event_submit(mode_type_t mode)
{
struct mode_event *event = new_mode_event();
event->mode_type = mode;
APP_EVENT_SUBMIT(event);
}
static inline mode_type_t mode_event_get_mode(const struct mode_event *event)
{
return event->mode_type;
}
static inline bool mode_event_is_usb(const struct mode_event *event)
{
return event->mode_type == MODE_TYPE_USB;
}
static inline bool mode_event_is_ble(const struct mode_event *event)
{
return event->mode_type == MODE_TYPE_BLE;
}
static inline bool mode_event_is_2g4(const struct mode_event *event)
{
return event->mode_type == MODE_TYPE_2G4;
}
#endif

View File

@@ -0,0 +1,26 @@
#include "qdec_step_event.h"
static void log_qdec_step_event(const struct app_event_header *aeh)
{
const struct qdec_step_event *event = cast_qdec_step_event(aeh);
APP_EVENT_MANAGER_LOG(aeh, "step=%d", event->step);
}
static void profile_qdec_step_event(struct log_event_buf *buf,
const struct app_event_header *aeh)
{
const struct qdec_step_event *event = cast_qdec_step_event(aeh);
nrf_profiler_log_encode_int8(buf, event->step);
}
APP_EVENT_INFO_DEFINE(qdec_step_event,
ENCODE(NRF_PROFILER_ARG_S8),
ENCODE("step"),
profile_qdec_step_event);
APP_EVENT_TYPE_DEFINE(qdec_step_event,
log_qdec_step_event,
&qdec_step_event_info,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,36 @@
#ifndef QDEC_STEP_EVENT_H__
#define QDEC_STEP_EVENT_H__
#include <stdint.h>
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
struct qdec_step_event {
struct app_event_header header;
int8_t step;
};
APP_EVENT_TYPE_DECLARE(qdec_step_event);
static inline void qdec_step_event_submit(int8_t step)
{
struct qdec_step_event *event;
__ASSERT((step == 1) || (step == -1), "qdec step event must be +/-1");
if (step == 0) {
return;
}
event = new_qdec_step_event();
event->step = step;
APP_EVENT_SUBMIT(event);
}
static inline int8_t qdec_step_event_get_step(const struct qdec_step_event *event)
{
return event->step;
}
#endif /* QDEC_STEP_EVENT_H__ */

View File

@@ -0,0 +1,39 @@
#include "time_sync_event.h"
/* 统一输出来源字符串,便于日志快速确认是哪条链路在校时。 */
static const char *time_sync_source_name(enum time_sync_source source)
{
switch (source) {
case TIME_SYNC_SOURCE_NONE:
return "none";
case TIME_SYNC_SOURCE_BLE:
return "ble";
case TIME_SYNC_SOURCE_USB:
return "usb";
case TIME_SYNC_SOURCE_MANUAL:
return "manual";
case TIME_SYNC_SOURCE_HID:
return "hid";
default:
return "unknown";
}
}
/* 事件日志聚焦关键信息:来源、时区和 UTC 毫秒时间戳。 */
static void log_time_sync_event(const struct app_event_header *aeh)
{
const struct time_sync_event *event = cast_time_sync_event(aeh);
const struct time_sync_update *update = time_sync_event_get_update(event);
APP_EVENT_MANAGER_LOG(aeh,
"src=%s tz=%d utc_ms=%llu acc=%u",
time_sync_source_name(update->source),
update->timezone_min,
(unsigned long long)update->utc_ms,
update->accuracy_ms);
}
APP_EVENT_TYPE_DEFINE(time_sync_event,
log_time_sync_event,
NULL,
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));

View File

@@ -0,0 +1,35 @@
#ifndef TIME_SYNC_EVENT_H__
#define TIME_SYNC_EVENT_H__
#include <app_event_manager.h>
#include <app_event_manager_profiler_tracer.h>
#include "time_manager.h"
/*
* time_sync_event 是“同步入口 -> time_manager”的统一事件
* - BLE/USB/本地设置都提交同一类事件;
* - time_manager 是唯一消费者,也是唯一能修改运行时钟状态的模块。
*/
struct time_sync_event {
struct app_event_header header;
struct time_sync_update update;
};
APP_EVENT_TYPE_DECLARE(time_sync_event);
static inline void time_sync_event_submit(const struct time_sync_update *update)
{
struct time_sync_event *event = new_time_sync_event();
event->update = *update;
APP_EVENT_SUBMIT(event);
}
static inline const struct time_sync_update *time_sync_event_get_update(
const struct time_sync_event *event)
{
return &event->update;
}
#endif /* TIME_SYNC_EVENT_H__ */

View File

@@ -0,0 +1,392 @@
#include <errno.h>
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/power/ip5306.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/pm/device.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/sys/util.h>
#include <app_event_manager.h>
#include <caf/events/power_event.h>
#include <caf/events/power_manager_event.h>
#define MODULE battery
#include <caf/events/module_state_event.h>
#include "battery_status_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE);
#define BATTERY_SENSE_NODE DT_NODELABEL(battery_sense)
#define BATTERY_SAMPLE_INTERVAL_MS 1000
#define BATTERY_MV_WINDOW_SIZE 10
/*
* SOC 估算策略:
* 这里采用线性估算,默认把 3.30V 映射为 0%4.20V 映射为 100%。
* 若后续实测曲线和电压分压比例不同,只需调整两个阈值即可。
*/
#define BATTERY_EMPTY_MV 3300
#define BATTERY_FULL_MV 4100
static const struct device *const ip5306_dev = DEVICE_DT_GET(DT_NODELABEL(ip5306));
static const struct device *const battery_sensor_dev = DEVICE_DT_GET(BATTERY_SENSE_NODE);
/*
* 板级电源采样结果:
* - 由 board provider 负责给出“原始但可用”的充电状态与电压值;
* - 本模块基于这些采样结果做滤波、SOC 估算和事件发布。
*/
struct board_power_sample
{
int32_t voltage_mv;
bool charging;
bool full;
};
/*
* 对外上报状态:
* - 保持现有 battery_status_event 语义不变;
* - 只承载业务层需要的 charging/full/soc 三元组。
*/
struct battery_status
{
bool charging;
bool full;
uint8_t soc;
};
struct battery_filter_state
{
int32_t window[BATTERY_MV_WINDOW_SIZE];
int64_t sum;
size_t count;
size_t index;
};
/*
* 模块上下文:
* - sample_work 周期性拉取 board power sample
* - filter 负责平滑电池电压;
* - last_status 用于抑制重复事件;
* - pm_restrict_level 跟踪当前对 power manager 的限制等级。
*/
struct battery_ctx
{
struct k_work_delayable sample_work;
atomic_t active;
struct battery_status last_status;
bool has_last_status;
struct battery_filter_state filter;
enum power_manager_level pm_restrict_level;
};
static struct battery_ctx battery = {
.pm_restrict_level = POWER_MANAGER_LEVEL_MAX,
};
/* 线性 SOC 估算:把平滑后的电池电压映射到 0~100%。 */
static uint8_t soc_from_mv(int32_t mv)
{
if (mv <= BATTERY_EMPTY_MV) {
return 0;
}
if (mv >= BATTERY_FULL_MV) {
return 100;
}
int32_t soc = ((mv - BATTERY_EMPTY_MV) * 100) / (BATTERY_FULL_MV - BATTERY_EMPTY_MV);
return (uint8_t)CLAMP(soc, 0, 100);
}
/* 初始化/恢复时清空滤波器,避免旧样本影响新一轮估算。 */
static void battery_filter_reset(void)
{
battery.filter.sum = 0;
battery.filter.count = 0;
battery.filter.index = 0;
}
/*
* 将最新电压样本写入固定窗口平均滤波器。
* 返回值始终是“窗口平均后的电池电压”,供上层做 SOC 估算。
*/
static int32_t battery_filter_apply(int32_t voltage_mv)
{
if (battery.filter.count < BATTERY_MV_WINDOW_SIZE) {
battery.filter.window[battery.filter.index] = voltage_mv;
battery.filter.sum += voltage_mv;
battery.filter.count++;
} else {
battery.filter.sum -= battery.filter.window[battery.filter.index];
battery.filter.window[battery.filter.index] = voltage_mv;
battery.filter.sum += voltage_mv;
}
battery.filter.index = (battery.filter.index + 1U) % BATTERY_MV_WINDOW_SIZE;
return (int32_t)(battery.filter.sum / (int64_t)battery.filter.count);
}
/*
* 控制 board-provided battery_sense sensor 的供电状态。
* 这里不直接操纵 GPIO而是走 sensor 的 PM action让 power-gpios
* 与 ADC runtime PM 都由 voltage-divider 驱动统一管理。
*/
static int board_power_monitor_set_voltage_sensor_enabled(bool enable)
{
return pm_device_action_run(battery_sensor_dev,
enable ? PM_DEVICE_ACTION_RESUME :
PM_DEVICE_ACTION_SUSPEND);
}
/* 从 battery_sense 读取一次当前电池电压(单位 mV。 */
static int board_power_monitor_read_voltage_mv(int32_t *voltage_mv)
{
struct sensor_value value;
int err = sensor_sample_fetch(battery_sensor_dev);
if (err) {
LOG_WRN("sensor_sample_fetch(battery) failed (err=%d)", err);
return err;
}
err = sensor_channel_get(battery_sensor_dev, SENSOR_CHAN_VOLTAGE, &value);
if (err) {
LOG_WRN("sensor_channel_get(battery) failed (err=%d)", err);
return err;
}
*voltage_mv = (int32_t)sensor_value_to_milli(&value);
return 0;
}
/* 从 IP5306 读取一次充电态与满电态。 */
static int board_power_monitor_read_charge_state(bool *charging, bool *full)
{
int err = ip5306_is_charging(ip5306_dev, charging);
if (err) {
LOG_WRN("ip5306_is_charging failed (err=%d)", err);
return err;
}
err = ip5306_is_charge_full(ip5306_dev, full);
if (err) {
LOG_WRN("ip5306_is_charge_full failed (err=%d)", err);
return err;
}
return 0;
}
/*
* 聚合一次完整的 board power sample
* 1) 先读 PMIC 状态;
* 2) 再读 battery_sense 电压;
* 3) 最后对电压做窗口平均,输出稳定值。
*/
static int board_power_monitor_collect_sample(struct board_power_sample *sample)
{
int32_t voltage_mv;
int err = board_power_monitor_read_charge_state(&sample->charging, &sample->full);
if (err) {
return err;
}
err = board_power_monitor_read_voltage_mv(&voltage_mv);
if (err) {
return err;
}
sample->voltage_mv = battery_filter_apply(voltage_mv);
return 0;
}
/* 将 board sample 映射成对外 battery status。 */
static void battery_status_from_sample(const struct board_power_sample *sample,
struct battery_status *status)
{
status->charging = sample->charging;
status->full = sample->full;
status->soc = soc_from_mv(sample->voltage_mv);
}
/* 统一封装 battery_status_event 发布,隔离事件总线细节。 */
static void publish_battery_status_event(const struct battery_status *status)
{
battery_status_event_submit(status->charging, status->full, status->soc);
}
/* 判断本轮状态是否值得上报,避免重复事件淹没总线。 */
static bool battery_status_changed(const struct battery_status *lhs,
const struct battery_status *rhs)
{
return (lhs->charging != rhs->charging) ||
(lhs->full != rhs->full) ||
(lhs->soc != rhs->soc);
}
/*
* 电源限制策略:
* - 充电线插入charging=true时限制到 ALIVE禁止自动休眠
* - 非充电时恢复到 SUSPENDED允许系统进入挂起但不进入 OFF。
*/
static void update_power_restrict_by_charging(bool charging)
{
enum power_manager_level target = charging ?
POWER_MANAGER_LEVEL_ALIVE : POWER_MANAGER_LEVEL_SUSPENDED;
if (battery.pm_restrict_level == target)
return;
battery.pm_restrict_level = target;
power_manager_restrict(MODULE_IDX(MODULE), target);
}
/*
* 启停采样:
* - enable=true 时恢复 battery_sense等待前端稳定后开始周期采样
* - enable=false 时停止 work 并挂起 battery_sense避免持续耗电。
*/
static void battery_sampling_set_enabled(bool enable)
{
atomic_set(&battery.active, enable);
int err = board_power_monitor_set_voltage_sensor_enabled(enable);
if (err) {
LOG_WRN("board_power_monitor_set_voltage_sensor_enabled(%d) failed (err=%d)",
enable, err);
}
if (enable) {
/* 延迟开始采样,等待板上采样前端和分压网络稳定。 */
k_work_reschedule(&battery.sample_work, K_MSEC(2000));
} else {
(void)k_work_cancel_delayable(&battery.sample_work);
}
}
/* 周期性读取 board power sample并在需要时上报业务状态。 */
static void battery_sample_fn(struct k_work *work)
{
ARG_UNUSED(work);
if (!atomic_get(&battery.active)) {
return;
}
struct board_power_sample sample;
struct battery_status status;
int err = board_power_monitor_collect_sample(&sample);
if (err) {
goto out_reschedule;
}
battery_status_from_sample(&sample, &status);
update_power_restrict_by_charging(status.charging);
if (!battery.has_last_status ||
battery_status_changed(&status, &battery.last_status)) {
battery.last_status = status;
battery.has_last_status = true;
publish_battery_status_event(&status);
}
out_reschedule:
k_work_reschedule(&battery.sample_work, K_MSEC(BATTERY_SAMPLE_INTERVAL_MS));
}
/* 初始化 board power monitor consumer并拉起首轮采样。 */
static int battery_module_init(void)
{
if (!device_is_ready(ip5306_dev)) {
LOG_ERR("IP5306 device not ready");
return -ENODEV;
}
if (!device_is_ready(battery_sensor_dev)) {
LOG_ERR("Battery sense device not ready");
return -ENODEV;
}
/* 默认非充电态允许进入 SUSPENDED但禁止进入 OFF。 */
update_power_restrict_by_charging(false);
k_work_init_delayable(&battery.sample_work, battery_sample_fn);
battery.has_last_status = false;
battery_filter_reset();
atomic_set(&battery.active, false);
battery_sampling_set_enabled(true);
return 0;
}
/* 响应系统挂起:停止采样,并把本模块切到 STANDBY。 */
static void battery_module_suspend(void)
{
if (!atomic_get(&battery.active)) {
/* 已经处于挂起态,避免重复上报 STANDBY 造成 power_down 循环。 */
return;
}
battery_sampling_set_enabled(false);
module_set_state(MODULE_STATE_STANDBY);
}
/* 响应系统唤醒:恢复电压传感器并重启周期采样。 */
static void battery_module_resume(void)
{
if (atomic_get(&battery.active)) {
return;
}
battery_sampling_set_enabled(true);
module_set_state(MODULE_STATE_READY);
}
/* 仅处理模块 ready 和系统电源状态事件,保持模块职责单一。 */
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
const struct module_state_event *event = cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
int err = battery_module_init();
if (err) {
module_set_state(MODULE_STATE_ERROR);
} else {
module_set_state(MODULE_STATE_READY);
}
}
return false;
}
if (is_power_down_event(aeh))
{
battery_module_suspend();
return false;
}
if (is_wake_up_event(aeh))
{
battery_module_resume();
return false;
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event);
APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);

View File

@@ -0,0 +1,85 @@
#include <app_event_manager.h>
#define MODULE ble_adv_ctrl
#include <caf/events/module_state_event.h>
#include <caf/events/module_suspend_event.h>
#include "mode_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
/*
* 该模块负责把“模式选择”转换成对 ble_adv 的挂起/恢复请求:
* - BLE 模式:恢复 ble_adv允许广播。
* - USB/2.4G 模式:挂起 ble_adv禁止广播。
*
* 说明:
* - 这里是控制层,不直接操作 bt_le_adv_start/stop
* - 实际广播执行仍由 CAF 的 ble_adv 模块处理。
*/
static bool ble_adv_suspended = true;
static void send_ble_adv_ctrl_req(bool suspend)
{
if (suspend) {
struct module_suspend_req_event *event = new_module_suspend_req_event();
event->sink_module_id = MODULE_ID(ble_adv);
event->src_module_id = MODULE_ID(MODULE);
APP_EVENT_SUBMIT(event);
} else {
struct module_resume_req_event *event = new_module_resume_req_event();
event->sink_module_id = MODULE_ID(ble_adv);
event->src_module_id = MODULE_ID(MODULE);
APP_EVENT_SUBMIT(event);
}
}
static bool handle_mode_event(const struct mode_event *event)
{
bool new_suspend = (event->mode_type != MODE_TYPE_BLE);
if (new_suspend == ble_adv_suspended) {
return false;
}
ble_adv_suspended = new_suspend;
send_ble_adv_ctrl_req(ble_adv_suspended);
LOG_INF("BLE advertising %s by mode %u",
ble_adv_suspended ? "suspended" : "resumed",
event->mode_type);
return false;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
const struct module_state_event *event = cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
/*
* 上电默认先请求挂起,避免在 mode_switch 首次采样前出现短暂误广播。
*/
send_ble_adv_ctrl_req(true);
module_set_state(MODULE_STATE_READY);
}
return false;
}
if (is_mode_event(aeh)) {
return handle_mode_event(cast_mode_event(aeh));
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE(MODULE, mode_event);

View File

@@ -0,0 +1,51 @@
#include <zephyr/bluetooth/services/bas.h>
#include <app_event_manager.h>
#define MODULE ble_battery
#include <caf/events/module_state_event.h>
#include "battery_status_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
static bool handle_battery_status_event(const struct battery_status_event *event)
{
uint8_t battery_level = battery_status_event_get_soc(event);
int err = bt_bas_set_battery_level(battery_level);
if (err) {
LOG_ERR("bt_bas_set_battery_level failed: %d", err);
}
return false;
}
static bool handle_module_state_event(const struct module_state_event *event)
{
if (!check_state(event, MODULE_ID(ble_state), MODULE_STATE_READY)) {
return false;
}
module_set_state(MODULE_STATE_READY);
return false;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_battery_status_event(aeh)) {
return handle_battery_status_event(cast_battery_status_event(aeh));
}
if (is_module_state_event(aeh)) {
return handle_module_state_event(cast_module_state_event(aeh));
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, battery_status_event);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);

View File

@@ -0,0 +1,583 @@
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/settings/settings.h>
#include <errno.h>
#include <app_event_manager.h>
#define MODULE ble_bond
#include <caf/events/module_state_event.h>
#include <caf/events/ble_common_event.h>
#include <caf/events/power_event.h>
#include "config_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
/* Application-visible options carried by config_event. */
enum ble_bond_cfg_opt {
BLE_BOND_CFG_PEER_SELECT = 0,
BLE_BOND_CFG_PEER_ERASE,
BLE_BOND_CFG_PEER_ERASE_ALL,
BLE_BOND_CFG_OPT_COUNT
};
/* Module ID in config_event event_id[7:4]. Keep stable for host side tooling. */
#define BLE_BOND_CFG_MODULE_ID 0x01
#define PEER_ID_KEY "peer_id"
#define BT_LUT_KEY "bt_lut"
enum ble_bond_state {
BLE_BOND_STATE_DISABLED,
BLE_BOND_STATE_IDLE,
BLE_BOND_STATE_STANDBY,
};
BUILD_ASSERT(CONFIG_BT_ID_MAX >= 2, "Need at least one resettable identity");
#define APP_PEER_COUNT (CONFIG_BT_ID_MAX - 1)
#define BLE_BOND_SLOT_COUNT 3
BUILD_ASSERT(BLE_BOND_SLOT_COUNT <= APP_PEER_COUNT,
"BLE slot count exceeds available Bluetooth identities");
struct ble_bond_storage {
uint8_t bt_stack_id_lut[APP_PEER_COUNT];
bool bt_stack_id_lut_valid;
uint8_t cur_peer_id;
bool cur_peer_id_valid;
};
struct ble_bond_ctx {
enum ble_bond_state state;
struct ble_bond_storage storage;
bool auto_switch_in_progress;
};
static struct ble_bond_ctx bond = {
.state = BLE_BOND_STATE_DISABLED,
};
static const char *state_name(enum ble_bond_state s)
{
switch (s) {
case BLE_BOND_STATE_DISABLED:
return "DISABLED";
case BLE_BOND_STATE_IDLE:
return "IDLE";
case BLE_BOND_STATE_STANDBY:
return "STANDBY";
default:
return "UNKNOWN";
}
}
static uint8_t get_bt_stack_peer_id(uint8_t app_id)
{
__ASSERT_NO_MSG(app_id < BLE_BOND_SLOT_COUNT);
return bond.storage.bt_stack_id_lut[app_id];
}
static int store_peer_id(uint8_t peer_id)
{
char key[] = MODULE_NAME "/" PEER_ID_KEY;
return settings_save_one(key, &peer_id, sizeof(peer_id));
}
static int store_bt_stack_id_lut(void)
{
char key[] = MODULE_NAME "/" BT_LUT_KEY;
return settings_save_one(key,
bond.storage.bt_stack_id_lut,
sizeof(bond.storage.bt_stack_id_lut));
}
static void submit_peer_op_event(enum peer_operation op, uint8_t app_id)
{
struct ble_peer_operation_event *event = new_ble_peer_operation_event();
event->op = op;
event->bt_app_id = app_id;
event->bt_stack_id = get_bt_stack_peer_id(app_id);
APP_EVENT_SUBMIT(event);
}
static void init_bt_stack_id_lut(void)
{
for (size_t i = 0; i < ARRAY_SIZE(bond.storage.bt_stack_id_lut); i++) {
/* Keep id 0 (BT_ID_DEFAULT) untouched for safe reset/unpair flow. */
bond.storage.bt_stack_id_lut[i] = i + 1;
}
}
static bool storage_data_is_valid(void)
{
if (!bond.storage.cur_peer_id_valid || !bond.storage.bt_stack_id_lut_valid) {
LOG_WRN("Stored data invalid: peer_valid=%d lut_valid=%d",
bond.storage.cur_peer_id_valid, bond.storage.bt_stack_id_lut_valid);
return false;
}
if (bond.storage.cur_peer_id >= BLE_BOND_SLOT_COUNT) {
LOG_WRN("Stored peer id out of range: peer_id=%u max=%u",
bond.storage.cur_peer_id, BLE_BOND_SLOT_COUNT - 1);
return false;
}
for (size_t i = 0; i < ARRAY_SIZE(bond.storage.bt_stack_id_lut); i++) {
if ((bond.storage.bt_stack_id_lut[i] == BT_ID_DEFAULT) ||
(bond.storage.bt_stack_id_lut[i] >= CONFIG_BT_ID_MAX)) {
LOG_WRN("Stored LUT invalid at idx=%u value=%u",
(uint32_t)i, bond.storage.bt_stack_id_lut[i]);
return false;
}
}
return true;
}
static int settings_set(const char *key, size_t len_rd,
settings_read_cb read_cb, void *cb_arg)
{
ssize_t rc;
if (!strcmp(key, PEER_ID_KEY)) {
if (len_rd != sizeof(bond.storage.cur_peer_id)) {
LOG_WRN("Settings '%s' size mismatch: got=%u expect=%u",
PEER_ID_KEY, (uint32_t)len_rd, sizeof(bond.storage.cur_peer_id));
bond.storage.cur_peer_id_valid = false;
return 0;
}
rc = read_cb(cb_arg, &bond.storage.cur_peer_id, sizeof(bond.storage.cur_peer_id));
bond.storage.cur_peer_id_valid = (rc == sizeof(bond.storage.cur_peer_id));
if (!bond.storage.cur_peer_id_valid) {
LOG_WRN("Settings '%s' read failed: rc=%d", PEER_ID_KEY, (int)rc);
}
} else if (!strcmp(key, BT_LUT_KEY)) {
if (len_rd != sizeof(bond.storage.bt_stack_id_lut)) {
LOG_WRN("Settings '%s' size mismatch: got=%u expect=%u",
BT_LUT_KEY, (uint32_t)len_rd, sizeof(bond.storage.bt_stack_id_lut));
bond.storage.bt_stack_id_lut_valid = false;
return 0;
}
rc = read_cb(cb_arg,
bond.storage.bt_stack_id_lut,
sizeof(bond.storage.bt_stack_id_lut));
bond.storage.bt_stack_id_lut_valid = (rc == sizeof(bond.storage.bt_stack_id_lut));
if (!bond.storage.bt_stack_id_lut_valid) {
LOG_WRN("Settings '%s' read failed: rc=%d", BT_LUT_KEY, (int)rc);
}
}
return 0;
}
SETTINGS_STATIC_HANDLER_DEFINE(ble_bond, MODULE_NAME, NULL, settings_set, NULL, NULL);
static int load_identities(void)
{
bt_addr_le_t addrs[CONFIG_BT_ID_MAX];
size_t count = ARRAY_SIZE(addrs);
bt_id_get(addrs, &count);
LOG_INF("Identity count before ensure: %u / %u", (uint32_t)count, CONFIG_BT_ID_MAX);
for (; count < CONFIG_BT_ID_MAX; count++) {
int err = bt_id_create(NULL, NULL);
if (err < 0) {
LOG_ERR("Cannot create identity (err:%d)", err);
return err;
}
LOG_INF("Created identity idx=%u", (uint32_t)count);
}
return 0;
}
static void disconnect_le_conn_cb(struct bt_conn *conn, void *user_data)
{
(void)user_data;
int err = bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
if (!err) {
LOG_INF("Disconnect LE peer for slot switch");
} else if (err == -ENOTCONN) {
LOG_INF("LE peer already disconnected during slot switch");
} else {
LOG_WRN("Failed to disconnect LE peer for slot switch err=%d", err);
}
}
static void mark_le_conn_found_cb(struct bt_conn *conn, void *user_data)
{
bool *found = user_data;
ARG_UNUSED(conn);
*found = true;
}
static bool has_any_le_conn(void)
{
bool found = false;
bt_conn_foreach(BT_CONN_TYPE_LE, mark_le_conn_found_cb, &found);
return found;
}
struct peer_bond_lookup {
const bt_addr_le_t *peer_addr;
bool found;
};
static void peer_bond_lookup_cb(const struct bt_bond_info *info, void *user_data)
{
struct peer_bond_lookup *lookup = user_data;
if (!bt_addr_le_cmp(&info->addr, lookup->peer_addr)) {
lookup->found = true;
}
}
static bool slot_has_peer_bond(uint8_t app_id, const bt_addr_le_t *peer_addr)
{
struct peer_bond_lookup lookup = {
.peer_addr = peer_addr,
};
bt_foreach_bond(get_bt_stack_peer_id(app_id), peer_bond_lookup_cb, &lookup);
return lookup.found;
}
static bool find_peer_owner_slot(const bt_addr_le_t *peer_addr, uint8_t *owner_slot)
{
for (uint8_t slot = 0; slot < BLE_BOND_SLOT_COUNT; slot++) {
if (slot_has_peer_bond(slot, peer_addr)) {
*owner_slot = slot;
return true;
}
}
return false;
}
static int select_peer(uint8_t peer_id)
{
if (peer_id >= BLE_BOND_SLOT_COUNT) {
return -EINVAL;
}
uint8_t previous_peer_id = bond.storage.cur_peer_id;
if (bond.storage.cur_peer_id_valid && (previous_peer_id == peer_id)) {
LOG_INF("Peer slot already selected: slot=%u stack_id=%u",
peer_id, get_bt_stack_peer_id(peer_id));
return 0;
}
bond.storage.cur_peer_id = peer_id;
bond.storage.cur_peer_id_valid = true;
int err = store_peer_id(bond.storage.cur_peer_id);
if (err) {
LOG_ERR("Failed to store peer_id=%u (err:%d)", bond.storage.cur_peer_id, err);
return err;
}
submit_peer_op_event(PEER_OPERATION_SELECTED, bond.storage.cur_peer_id);
bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_le_conn_cb, NULL);
return 0;
}
static bool handle_ble_peer_event(const struct ble_peer_event *event)
{
if (event->state == PEER_STATE_CONNECTED) {
const bt_addr_le_t *peer_addr = bt_conn_get_dst(event->id);
uint8_t owner_slot;
if (!peer_addr) {
return false;
}
if (find_peer_owner_slot(peer_addr, &owner_slot) &&
(owner_slot != bond.storage.cur_peer_id)) {
char addr_str[BT_ADDR_LE_STR_LEN];
int err;
bt_addr_le_to_str(peer_addr, addr_str, sizeof(addr_str));
LOG_INF("Peer %s belongs to slot=%u, auto-switch from slot=%u",
addr_str, owner_slot, bond.storage.cur_peer_id);
bond.auto_switch_in_progress = true;
err = select_peer(owner_slot);
if (err) {
bond.auto_switch_in_progress = false;
LOG_ERR("Auto-switch to slot=%u failed err=%d",
owner_slot, err);
module_set_state(MODULE_STATE_ERROR);
}
}
return false;
}
if (event->state == PEER_STATE_DISCONNECTED) {
if (bond.auto_switch_in_progress) {
bond.auto_switch_in_progress = false;
LOG_INF("Auto-switch disconnect complete, waiting for reconnect on slot=%u",
bond.storage.cur_peer_id);
}
return false;
}
if (event->state != PEER_STATE_SECURED) {
return false;
}
struct bt_conn_info info;
int err = bt_conn_get_info(event->id, &info);
if (err) {
LOG_ERR("Cannot get conn info for secured peer err=%d", err);
module_set_state(MODULE_STATE_ERROR);
return false;
}
uint8_t expected_stack_id = get_bt_stack_peer_id(bond.storage.cur_peer_id);
if (info.id == expected_stack_id) {
LOG_INF("Secured peer matches selected slot=%u stack_id=%u",
bond.storage.cur_peer_id,
expected_stack_id);
return false;
}
LOG_INF("Disconnect peer on old id=%u expected=%u selected_slot=%u",
info.id,
expected_stack_id,
bond.storage.cur_peer_id);
err = bt_conn_disconnect(event->id, BT_HCI_ERR_REMOTE_USER_TERM_CONN);
if (err && (err != -ENOTCONN)) {
LOG_ERR("Cannot disconnect peer on old id err=%d", err);
module_set_state(MODULE_STATE_ERROR);
}
return false;
}
static int erase_peer(uint8_t app_id)
{
uint8_t stack_id = get_bt_stack_peer_id(app_id);
int err;
/* Tell ble_adv to restart advertising session for this identity. */
submit_peer_op_event(PEER_OPERATION_ERASE_ADV, app_id);
err = bt_unpair(stack_id, NULL);
if (err) {
LOG_ERR("Cannot unpair id %u (err:%d)", stack_id, err);
return err;
}
err = bt_id_reset(stack_id, NULL, NULL);
if (err < 0) {
LOG_ERR("Cannot reset id %u (err:%d)", stack_id, err);
return err;
}
submit_peer_op_event(PEER_OPERATION_ERASED, app_id);
return 0;
}
static int erase_all_peers(void)
{
for (uint8_t i = 0; i < BLE_BOND_SLOT_COUNT; i++) {
int err = erase_peer(i);
if (err) {
return err;
}
}
return 0;
}
static bool handle_config_event(const struct config_event *event)
{
if (!event->is_request || (event->recipient != CFG_CHAN_RECIPIENT_LOCAL)) {
return false;
}
if (MOD_FIELD_GET(event->event_id) != BLE_BOND_CFG_MODULE_ID) {
return false;
}
struct config_event *rsp = new_config_event(0);
rsp->transport_id = event->transport_id;
rsp->is_request = false;
rsp->event_id = event->event_id;
rsp->recipient = event->recipient;
rsp->status = CONFIG_STATUS_REJECT;
if (event->status == CONFIG_STATUS_SET) {
uint8_t opt_field = OPT_FIELD_GET(event->event_id);
uint8_t opt_id = (opt_field == 0) ? UINT8_MAX : OPT_ID_GET(opt_field);
switch (opt_id) {
case BLE_BOND_CFG_PEER_SELECT:
if (event->dyndata.size >= 1) {
uint8_t peer_id = event->dyndata.data[0];
if (!select_peer(peer_id)) {
rsp->status = CONFIG_STATUS_SUCCESS;
}
}
break;
case BLE_BOND_CFG_PEER_ERASE:
if (!erase_peer(bond.storage.cur_peer_id)) {
rsp->status = CONFIG_STATUS_SUCCESS;
}
break;
case BLE_BOND_CFG_PEER_ERASE_ALL:
if (!erase_all_peers()) {
rsp->status = CONFIG_STATUS_SUCCESS;
}
break;
default:
break;
}
} else if (event->status == CONFIG_STATUS_FETCH) {
uint8_t opt_field = OPT_FIELD_GET(event->event_id);
uint8_t opt_id = (opt_field == 0) ? UINT8_MAX : OPT_ID_GET(opt_field);
if (opt_id == BLE_BOND_CFG_PEER_SELECT) {
struct config_event *rsp_data = new_config_event(1);
rsp_data->transport_id = event->transport_id;
rsp_data->is_request = false;
rsp_data->event_id = event->event_id;
rsp_data->recipient = event->recipient;
rsp_data->status = CONFIG_STATUS_SUCCESS;
rsp_data->dyndata.data[0] = bond.storage.cur_peer_id;
APP_EVENT_SUBMIT(rsp_data);
return true;
}
}
APP_EVENT_SUBMIT(rsp);
return true;
}
static int init_after_settings_loaded(void)
{
int err = load_identities();
if (err) {
LOG_ERR("Identity initialization failed: %d", err);
return err;
}
if (!storage_data_is_valid()) {
LOG_WRN("Stored BLE bond data invalid, reinitializing defaults");
bond.storage.cur_peer_id = 0;
bond.storage.cur_peer_id_valid = true;
init_bt_stack_id_lut();
bond.storage.bt_stack_id_lut_valid = true;
err = store_peer_id(bond.storage.cur_peer_id);
if (err) {
LOG_ERR("Failed to store peer_id=%u (err:%d)",
bond.storage.cur_peer_id, err);
return -EIO;
}
err = store_bt_stack_id_lut();
if (err) {
LOG_ERR("Failed to store bt_stack_id_lut (err:%d)", err);
return -EIO;
}
}
bond.state = BLE_BOND_STATE_IDLE;
LOG_INF("ble_bond init done: state=%s peer_id=%u stack_id=%u",
state_name(bond.state),
bond.storage.cur_peer_id,
get_bt_stack_peer_id(bond.storage.cur_peer_id));
submit_peer_op_event(PEER_OPERATION_SELECTED, bond.storage.cur_peer_id);
module_set_state(MODULE_STATE_READY);
return 0;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
const struct module_state_event *event = cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(settings_loader), MODULE_STATE_READY) &&
(bond.state == BLE_BOND_STATE_DISABLED)) {
LOG_INF("settings_loader ready, starting ble_bond init");
int err = init_after_settings_loaded();
if (err) {
LOG_ERR("ble_bond init failed (err:%d), state=%s",
err, state_name(bond.state));
module_set_state(MODULE_STATE_ERROR);
}
}
return false;
}
if (is_power_down_event(aeh)) {
if (bond.state == BLE_BOND_STATE_IDLE) {
bond.state = BLE_BOND_STATE_STANDBY;
module_set_state(MODULE_STATE_OFF);
}
return false;
}
if (is_wake_up_event(aeh)) {
if (bond.state == BLE_BOND_STATE_STANDBY) {
bond.state = BLE_BOND_STATE_IDLE;
module_set_state(MODULE_STATE_READY);
/*
* If a LE link survived suspend, keep it untouched. CAF ble_adv
* treats PEER_OPERATION_SELECTED as a real identity switch and
* will disconnect the current peer. If no link exists, re-emit
* the selection so advertising resumes on the selected slot.
*/
if (!has_any_le_conn()) {
submit_peer_op_event(PEER_OPERATION_SELECTED,
bond.storage.cur_peer_id);
}
}
return false;
}
if (is_ble_peer_event(aeh)) {
return handle_ble_peer_event(cast_ble_peer_event(aeh));
}
if (is_config_event(aeh)) {
return handle_config_event(cast_config_event(aeh));
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE(MODULE, power_down_event);
APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);
APP_EVENT_SUBSCRIBE(MODULE, ble_peer_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, config_event);

View File

@@ -0,0 +1,404 @@
#include <bluetooth/services/hids.h>
#include <stdint.h>
#include <app_event_manager.h>
#define MODULE ble_hid
#include <caf/events/module_state_event.h>
#include <caf/events/ble_common_event.h>
#include "hid_protocol_event.h"
#include "hid_host_command_event.h"
#include "hid_report_descriptor.h"
#include "hid_tx_done_event.h"
#include "hid_tx_event.h"
#include "hid_vendor_mask_event.h"
#include "keyboard_led_event.h"
#include "mode_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define INPUT_REPORT_COUNT 4
#define OUTPUT_REPORT_COUNT 3
BT_HIDS_DEF(hids_obj, INPUT_REPORT_COUNT, OUTPUT_REPORT_COUNT, 0);
struct ble_hid_link {
struct bt_conn *conn;
enum bt_hids_pm protocol_mode;
};
struct ble_hid_policy {
bool ble_mode_selected;
};
struct ble_hid_led_state {
bool valid;
uint8_t led_mask;
};
struct ble_hid_ctx {
struct ble_hid_link link;
struct ble_hid_policy policy;
struct ble_hid_led_state led;
};
static struct ble_hid_ctx ble_hid = {
.link.protocol_mode = BT_HIDS_PM_REPORT,
};
static bool ble_hid_is_connected(void)
{
return ble_hid.link.conn != NULL;
}
static bool ble_hid_should_handle_tx_event(const struct hid_tx_event *event)
{
switch (hid_tx_event_get_route(event)) {
case HID_TX_ROUTE_AUTO:
return ble_hid.policy.ble_mode_selected;
case HID_TX_ROUTE_BLE:
return true;
case HID_TX_ROUTE_USB:
default:
return false;
}
}
static bool ble_hid_is_boot_mode(void)
{
return ble_hid.link.protocol_mode == BT_HIDS_PM_BOOT;
}
static bool ble_hid_is_report_mode(void)
{
return ble_hid.link.protocol_mode == BT_HIDS_PM_REPORT;
}
static void publish_hid_protocol_event(enum hid_protocol_type protocol)
{
hid_protocol_event_submit(protocol);
}
/* 主机 LED 输出报告变化时才上报,避免重复事件淹没总线。 */
static void publish_num_lock_state_from_led_mask(uint8_t led_mask)
{
if (ble_hid.led.valid && (ble_hid.led.led_mask == led_mask)) {
return;
}
ble_hid.led.valid = true;
ble_hid.led.led_mask = led_mask;
keyboard_led_event_submit(led_mask);
}
static void pm_evt_handler(enum bt_hids_pm_evt evt, struct bt_conn *conn)
{
ARG_UNUSED(conn);
switch (evt) {
case BT_HIDS_PM_EVT_BOOT_MODE_ENTERED:
ble_hid.link.protocol_mode = BT_HIDS_PM_BOOT;
LOG_INF("HIDS protocol: boot");
if (ble_hid_is_connected()) {
publish_hid_protocol_event(HID_PROTO_BOOT);
}
break;
case BT_HIDS_PM_EVT_REPORT_MODE_ENTERED:
ble_hid.link.protocol_mode = BT_HIDS_PM_REPORT;
LOG_INF("HIDS protocol: report");
if (ble_hid_is_connected()) {
publish_hid_protocol_event(HID_PROTO_REPORT);
}
break;
default:
break;
}
}
static void report_notify_handler(enum bt_hids_notify_evt evt)
{
ARG_UNUSED(evt);
}
static void boot_keyboard_notif_handler(enum bt_hids_notify_evt evt)
{
ARG_UNUSED(evt);
}
static void boot_keyboard_output_report_handler(struct bt_hids_rep *rep,
struct bt_conn *conn,
bool write)
{
ARG_UNUSED(conn);
if (!write || !rep || (rep->size == 0) || !rep->data) {
return;
}
publish_num_lock_state_from_led_mask(rep->data[0]);
LOG_DBG("Boot KB out report 0x%02x", rep->data[0]);
}
static void keyboard_output_report_handler(struct bt_hids_rep *rep,
struct bt_conn *conn,
bool write)
{
ARG_UNUSED(conn);
if (!write || !rep || !rep->data || (rep->size < HID_KBD_LED_PAYLOAD_SIZE)) {
return;
}
publish_num_lock_state_from_led_mask(rep->data[0]);
LOG_DBG("Report KB out report 0x%02x", rep->data[0]);
}
static void vendor_output_report_handler(struct bt_hids_rep *rep,
struct bt_conn *conn,
bool write)
{
ARG_UNUSED(conn);
if (!write || !rep || !rep->data || (rep->size != HID_VENDOR_PAYLOAD_SIZE)) {
return;
}
hid_vendor_mask_event_submit(rep->data, rep->size);
LOG_INF("Vendor mask updated over BLE len=%u", rep->size);
}
static void vendor_cmd_output_report_handler(struct bt_hids_rep *rep,
struct bt_conn *conn,
bool write)
{
ARG_UNUSED(conn);
if (!write || !rep || !rep->data ||
(rep->size != HID_HOST_CMD_OUTPUT_PAYLOAD_SIZE)) {
return;
}
hid_host_command_event_submit(HID_HOST_TRANSPORT_BLE,
rep->data[0],
&rep->data[1],
rep->size - 1U);
LOG_INF("Vendor cmd updated over BLE cmd=0x%02x len=%u",
rep->data[0], rep->size);
}
static int hids_service_init(void)
{
static const uint8_t report_map[] = HID_DESC_KEYBOARD_NKRO_CONSUMER();
struct bt_hids_init_param init_param = { 0 };
struct bt_hids_inp_rep *input_report = &init_param.inp_rep_group_init.reports[0];
struct bt_hids_outp_feat_rep *output_report = &init_param.outp_rep_group_init.reports[0];
init_param.info.bcd_hid = 0x0101;
init_param.info.b_country_code = 0x00;
init_param.info.flags = BT_HIDS_REMOTE_WAKE | BT_HIDS_NORMALLY_CONNECTABLE;
init_param.rep_map.data = report_map;
init_param.rep_map.size = sizeof(report_map);
input_report[0].id = REPORT_ID_KEYBOARD;
input_report[0].size = HID_KBD_PAYLOAD_SIZE;
input_report[0].handler = report_notify_handler;
input_report[1].id = REPORT_ID_CONSUMER;
input_report[1].size = HID_CONSUMER_PAYLOAD_SIZE;
input_report[1].handler = report_notify_handler;
input_report[2].id = REPORT_ID_VENDOR;
input_report[2].size = HID_VENDOR_PAYLOAD_SIZE;
input_report[2].handler = report_notify_handler;
input_report[3].id = REPORT_ID_VENDOR_CMD;
input_report[3].size = HID_VENDOR_ACK_PAYLOAD_SIZE;
input_report[3].handler = report_notify_handler;
output_report[0].id = REPORT_ID_KEYBOARD;
output_report[0].size = HID_KBD_LED_PAYLOAD_SIZE;
output_report[0].handler = keyboard_output_report_handler;
output_report[1].id = REPORT_ID_VENDOR;
output_report[1].size = HID_VENDOR_PAYLOAD_SIZE;
output_report[1].handler = vendor_output_report_handler;
output_report[2].id = REPORT_ID_VENDOR_CMD;
output_report[2].size = HID_HOST_CMD_OUTPUT_PAYLOAD_SIZE;
output_report[2].handler = vendor_cmd_output_report_handler;
init_param.inp_rep_group_init.cnt = INPUT_REPORT_COUNT;
init_param.outp_rep_group_init.cnt = OUTPUT_REPORT_COUNT;
init_param.pm_evt_handler = pm_evt_handler;
init_param.is_kb = true;
init_param.boot_kb_notif_handler = boot_keyboard_notif_handler;
init_param.boot_kb_outp_rep_handler = boot_keyboard_output_report_handler;
return bt_hids_init(&hids_obj, &init_param);
}
static void handle_ble_peer_event(const struct ble_peer_event *event)
{
switch (event->state) {
case PEER_STATE_CONNECTED:
__ASSERT_NO_MSG(ble_hid.link.conn == NULL);
ble_hid.link.conn = event->id;
if (bt_hids_connected(&hids_obj, ble_hid.link.conn)) {
LOG_WRN("bt_hids_connected failed");
}
break;
case PEER_STATE_DISCONNECTED:
if (ble_hid.link.conn == event->id) {
if (bt_hids_disconnected(&hids_obj, ble_hid.link.conn)) {
LOG_WRN("bt_hids_disconnected failed");
}
ble_hid.link.conn = NULL;
}
break;
default:
break;
}
}
static bool handle_hid_tx_event(const struct hid_tx_event *event)
{
if (!ble_hid_should_handle_tx_event(event)) {
return false;
}
if (!ble_hid_is_connected()) {
hid_tx_done_event_submit(event->kind, false);
return false;
}
if (event->kind == HID_TX_KIND_BOOT) {
const uint8_t *payload = hid_tx_event_get_data(event);
size_t payload_len = hid_tx_event_get_size(event);
int err;
if (!ble_hid_is_boot_mode()) {
hid_tx_done_event_submit(HID_TX_KIND_BOOT, false);
return false;
}
if (payload_len != HID_BOOT_KBD_PAYLOAD_SIZE) {
LOG_WRN("Invalid boot keyboard payload len=%u", payload_len);
hid_tx_done_event_submit(HID_TX_KIND_BOOT, false);
return false;
}
err = bt_hids_boot_kb_inp_rep_send(&hids_obj, ble_hid.link.conn,
payload, payload_len, NULL);
if (err) {
LOG_WRN("BLE HID boot send failed err=%d", err);
}
hid_tx_done_event_submit(HID_TX_KIND_BOOT, (err == 0));
return false;
}
if (!ble_hid_is_report_mode()) {
hid_tx_done_event_submit(HID_TX_KIND_REPORT, false);
return false;
}
const uint8_t *data = hid_tx_event_get_data(event);
size_t data_len = hid_tx_event_get_size(event);
uint8_t report_id;
const uint8_t *payload;
size_t payload_len;
if (data_len < 1U) {
hid_tx_done_event_submit(HID_TX_KIND_REPORT, false);
return false;
}
report_id = data[0];
payload = &data[1];
payload_len = data_len - 1U;
uint8_t rep_index;
if (report_id == REPORT_ID_KEYBOARD) {
rep_index = 0U;
} else if (report_id == REPORT_ID_CONSUMER) {
rep_index = 1U;
} else if (report_id == REPORT_ID_VENDOR) {
rep_index = 2U;
} else if (report_id == REPORT_ID_VENDOR_CMD) {
rep_index = 3U;
} else {
hid_tx_done_event_submit(HID_TX_KIND_REPORT, false);
return false;
}
if (payload_len > UINT8_MAX) {
LOG_WRN("Payload too large=%u", payload_len);
hid_tx_done_event_submit(HID_TX_KIND_REPORT, false);
return false;
}
int err = bt_hids_inp_rep_send(&hids_obj, ble_hid.link.conn, rep_index,
payload, (uint8_t)payload_len, NULL);
if (err) {
LOG_WRN("BLE HID send failed report=0x%02x err=%d", report_id, err);
}
hid_tx_done_event_submit(HID_TX_KIND_REPORT, (err == 0));
return false;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
const struct module_state_event *event = cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
static bool initialized;
__ASSERT_NO_MSG(!initialized);
initialized = true;
if (hids_service_init()) {
LOG_ERR("Cannot initialize HIDS service");
module_set_state(MODULE_STATE_ERROR);
} else {
module_set_state(MODULE_STATE_READY);
}
}
return false;
}
if (is_ble_peer_event(aeh)) {
handle_ble_peer_event(cast_ble_peer_event(aeh));
return false;
}
if (is_mode_event(aeh)) {
const struct mode_event *event = cast_mode_event(aeh);
ble_hid.policy.ble_mode_selected = (event->mode_type == MODE_TYPE_BLE);
return false;
}
if (is_hid_tx_event(aeh)) {
return handle_hid_tx_event(cast_hid_tx_event(aeh));
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, ble_peer_event);
APP_EVENT_SUBSCRIBE(MODULE, mode_event);
APP_EVENT_SUBSCRIBE(MODULE, hid_tx_event);

View File

@@ -0,0 +1,281 @@
#include <string.h>
#include <zephyr/kernel.h>
#include <app_event_manager.h>
#define MODULE ble_slot_ctrl
#include <caf/events/button_event.h>
#include <caf/events/module_state_event.h>
#include <caf/events/power_event.h>
#include <caf/key_id.h>
#include "config_event.h"
#include "mode_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define BLE_BOND_CFG_MODULE_ID 0x01
#define BLE_BOND_CFG_PEER_SELECT 0
#define BLE_BOND_CFG_PEER_ERASE 1
#define BLE_SLOT_CTRL_MUTE_HOLD_MS 3000
#define BLE_SLOT_CTRL_SLOT_KEY_NONE UINT16_MAX
#define BLE_SLOT_CTRL_KEY_MUTE KEY_ID(3, 0)
#define BLE_SLOT_CTRL_SLOT_COUNT 3
struct ble_slot_ctrl_ctx {
bool ble_mode_selected;
bool mute_pressed;
bool mute_hold_fired;
bool slot_combo_used;
bool initialized;
bool passthrough_mute_press;
bool passthrough_mute_release;
uint16_t slot_key_id;
struct k_work_delayable mute_hold_work;
};
static struct ble_slot_ctrl_ctx slot_ctrl = {
.slot_key_id = BLE_SLOT_CTRL_SLOT_KEY_NONE,
};
static uint8_t ble_slot_ctrl_make_event_id(uint8_t opt_id)
{
return (BLE_BOND_CFG_MODULE_ID << MOD_FIELD_POS) | (uint8_t)(opt_id + 1U);
}
static void ble_slot_ctrl_submit_config_request(uint8_t opt_id,
const uint8_t *payload,
size_t payload_len)
{
struct config_event *event = new_config_event(payload_len);
event->transport_id = 0U;
event->is_request = true;
event->event_id = ble_slot_ctrl_make_event_id(opt_id);
event->recipient = CFG_CHAN_RECIPIENT_LOCAL;
event->status = CONFIG_STATUS_SET;
if ((payload_len > 0U) && (payload != NULL)) {
memcpy(event->dyndata.data, payload, payload_len);
}
LOG_INF("Submit cfg request opt=%u len=%u", opt_id, payload_len);
APP_EVENT_SUBMIT(event);
}
static void ble_slot_ctrl_request_select(uint8_t slot_id)
{
ble_slot_ctrl_submit_config_request(BLE_BOND_CFG_PEER_SELECT, &slot_id, sizeof(slot_id));
}
static void ble_slot_ctrl_request_erase_current(void)
{
ble_slot_ctrl_submit_config_request(BLE_BOND_CFG_PEER_ERASE, NULL, 0U);
}
static void ble_slot_ctrl_reset(void)
{
LOG_DBG("Reset slot ctrl state mute=%d hold=%d combo=%d key=0x%04x",
slot_ctrl.mute_pressed,
slot_ctrl.mute_hold_fired,
slot_ctrl.slot_combo_used,
slot_ctrl.slot_key_id);
slot_ctrl.mute_pressed = false;
slot_ctrl.mute_hold_fired = false;
slot_ctrl.slot_combo_used = false;
slot_ctrl.slot_key_id = BLE_SLOT_CTRL_SLOT_KEY_NONE;
if (slot_ctrl.initialized) {
(void)k_work_cancel_delayable(&slot_ctrl.mute_hold_work);
}
}
static void ble_slot_ctrl_submit_button(uint16_t key_id, bool pressed)
{
struct button_event *event = new_button_event();
event->key_id = key_id;
event->pressed = pressed;
APP_EVENT_SUBMIT(event);
}
static void ble_slot_ctrl_forward_mute_tap(void)
{
LOG_INF("Forward mute tap to HID path");
slot_ctrl.passthrough_mute_press = true;
slot_ctrl.passthrough_mute_release = true;
ble_slot_ctrl_submit_button(BLE_SLOT_CTRL_KEY_MUTE, true);
ble_slot_ctrl_submit_button(BLE_SLOT_CTRL_KEY_MUTE, false);
}
static bool ble_slot_ctrl_try_get_slot_id(uint16_t key_id, uint8_t *slot_id)
{
switch (key_id) {
case KEY_ID(0, 4):
*slot_id = 0U;
return true;
case KEY_ID(1, 4):
*slot_id = 1U;
return true;
case KEY_ID(2, 4):
*slot_id = 2U;
return true;
default:
return false;
}
}
static bool ble_slot_ctrl_active(void)
{
return slot_ctrl.ble_mode_selected;
}
static void ble_slot_ctrl_mute_hold_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
if (!ble_slot_ctrl_active() ||
!slot_ctrl.mute_pressed ||
slot_ctrl.slot_combo_used) {
LOG_DBG("Ignore mute hold active=%d mute=%d combo=%d",
ble_slot_ctrl_active(),
slot_ctrl.mute_pressed,
slot_ctrl.slot_combo_used);
return;
}
ble_slot_ctrl_request_erase_current();
slot_ctrl.mute_hold_fired = true;
LOG_INF("Requested erase current BLE slot by mute hold");
}
static bool handle_button_event(const struct button_event *event)
{
if (!ble_slot_ctrl_active()) {
return false;
}
if (event->key_id == BLE_SLOT_CTRL_KEY_MUTE) {
if (event->pressed && slot_ctrl.passthrough_mute_press) {
LOG_DBG("Pass through synthetic mute press");
slot_ctrl.passthrough_mute_press = false;
return false;
}
if (!event->pressed && slot_ctrl.passthrough_mute_release) {
LOG_DBG("Pass through synthetic mute release");
slot_ctrl.passthrough_mute_release = false;
return false;
}
if (event->pressed) {
if (slot_ctrl.mute_pressed) {
LOG_DBG("Drop repeated mute press");
return true;
}
LOG_INF("Mute pressed: start hold timer %u ms", BLE_SLOT_CTRL_MUTE_HOLD_MS);
slot_ctrl.mute_pressed = true;
slot_ctrl.mute_hold_fired = false;
slot_ctrl.slot_combo_used = false;
slot_ctrl.slot_key_id = BLE_SLOT_CTRL_SLOT_KEY_NONE;
k_work_reschedule(&slot_ctrl.mute_hold_work,
K_MSEC(BLE_SLOT_CTRL_MUTE_HOLD_MS));
return true;
}
if (!slot_ctrl.mute_pressed) {
return false;
}
LOG_INF("Mute released hold=%d combo=%d",
slot_ctrl.mute_hold_fired,
slot_ctrl.slot_combo_used);
(void)k_work_cancel_delayable(&slot_ctrl.mute_hold_work);
if (!slot_ctrl.mute_hold_fired && !slot_ctrl.slot_combo_used) {
ble_slot_ctrl_forward_mute_tap();
}
ble_slot_ctrl_reset();
return true;
}
if (!slot_ctrl.mute_pressed) {
return false;
}
if (!event->pressed && (slot_ctrl.slot_key_id == event->key_id)) {
slot_ctrl.slot_key_id = BLE_SLOT_CTRL_SLOT_KEY_NONE;
return true;
}
if (!event->pressed) {
return false;
}
uint8_t slot_id;
if (!ble_slot_ctrl_try_get_slot_id(event->key_id, &slot_id)) {
LOG_DBG("Mute combo ignore key_id=0x%04x", event->key_id);
return false;
}
(void)k_work_cancel_delayable(&slot_ctrl.mute_hold_work);
slot_ctrl.slot_combo_used = true;
slot_ctrl.slot_key_id = event->key_id;
ble_slot_ctrl_request_select(slot_id);
LOG_INF("Requested BLE slot=%u from key_id=0x%04x", slot_id, event->key_id);
return true;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
const struct module_state_event *event = cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
if (!slot_ctrl.initialized) {
k_work_init_delayable(&slot_ctrl.mute_hold_work,
ble_slot_ctrl_mute_hold_work_fn);
slot_ctrl.initialized = true;
}
module_set_state(MODULE_STATE_READY);
}
return false;
}
if (is_mode_event(aeh)) {
const struct mode_event *event = cast_mode_event(aeh);
slot_ctrl.ble_mode_selected = mode_event_is_ble(event);
LOG_INF("BLE slot ctrl mode selected=%d", slot_ctrl.ble_mode_selected);
if (!slot_ctrl.ble_mode_selected) {
ble_slot_ctrl_reset();
}
return false;
}
if (is_power_down_event(aeh)) {
ble_slot_ctrl_reset();
return false;
}
if (is_button_event(aeh)) {
return handle_button_event(cast_button_event(aeh));
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE(MODULE, mode_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, button_event);

View File

@@ -0,0 +1,175 @@
#include <errno.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/sys/byteorder.h>
#include <app_event_manager.h>
#define MODULE ble_time_sync
#include <caf/events/module_state_event.h>
#include "time_manager.h"
#include "time_sync_event.h"
#include "time_sync_protocol.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define BT_UUID_TIME_SYNC_SERVICE_VAL \
BT_UUID_128_ENCODE(0x0b7f5000, 0x38d2, 0x4f62, 0x8f6f, 0x36c4fd73a110)
#define BT_UUID_TIME_SYNC_WRITE_CHAR_VAL \
BT_UUID_128_ENCODE(0x0b7f5001, 0x38d2, 0x4f62, 0x8f6f, 0x36c4fd73a110)
#define BT_UUID_TIME_SYNC_SERVICE \
BT_UUID_DECLARE_128(BT_UUID_TIME_SYNC_SERVICE_VAL)
#define BT_UUID_TIME_SYNC_WRITE_CHAR \
BT_UUID_DECLARE_128(BT_UUID_TIME_SYNC_WRITE_CHAR_VAL)
struct ble_time_sync_ctx {
bool ble_stack_ready;
bool time_manager_ready;
bool module_ready;
};
static struct ble_time_sync_ctx ble_time_sync;
/* 统一检查协议版本和长度,避免在回调里分散出现偏移判断。 */
static bool ble_time_sync_payload_is_valid(const uint8_t *buf, uint16_t len)
{
if (!buf || (len != TIME_SYNC_PROTOCOL_PAYLOAD_SIZE)) {
return false;
}
if (buf[TIME_SYNC_PROTOCOL_OFFSET_VERSION] != TIME_SYNC_PROTOCOL_VERSION) {
return false;
}
if ((buf[TIME_SYNC_PROTOCOL_OFFSET_FLAGS] &
TIME_SYNC_PROTOCOL_FLAG_TIMEZONE_VALID) == 0U) {
return false;
}
return true;
}
/*
* 把私有 GATT payload 解码为统一的 time_sync_update
* - BLE 只负责协议适配;
* - 传输无关的时间语义都转成公共结构体后再交给事件层。
*/
static void ble_time_sync_decode_payload(const uint8_t *buf,
struct time_sync_update *update)
{
update->utc_ms = sys_get_le64(&buf[TIME_SYNC_PROTOCOL_OFFSET_UTC_MS]);
update->timezone_min =
(int16_t)sys_get_le16(&buf[TIME_SYNC_PROTOCOL_OFFSET_TIMEZONE]);
update->accuracy_ms =
sys_get_le32(&buf[TIME_SYNC_PROTOCOL_OFFSET_ACCURACY_MS]);
update->source = TIME_SYNC_SOURCE_BLE;
}
/*
* GATT 写回调必须尽量短:
* - 不支持 offset/prepare write避免被拆成长写事务
* - 校验和解码完成后直接提交统一事件,不在这里做耗时存储。
*/
static ssize_t write_time_sync(struct bt_conn *conn,
const struct bt_gatt_attr *attr,
const void *buf,
uint16_t len,
uint16_t offset,
uint8_t flags)
{
struct time_sync_update update;
ARG_UNUSED(conn);
ARG_UNUSED(attr);
if (!ble_time_sync.module_ready || !time_manager_is_ready()) {
return BT_GATT_ERR(BT_ATT_ERR_UNLIKELY);
}
if (offset != 0U) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
if ((flags & BT_GATT_WRITE_FLAG_PREPARE) != 0U) {
return BT_GATT_ERR(BT_ATT_ERR_ATTRIBUTE_NOT_LONG);
}
if (!ble_time_sync_payload_is_valid(buf, len)) {
return BT_GATT_ERR(BT_ATT_ERR_VALUE_NOT_ALLOWED);
}
ble_time_sync_decode_payload(buf, &update);
time_sync_event_submit(&update);
LOG_INF("Accepted BLE time sync utc_ms=%llu tz=%d acc=%u",
(unsigned long long)update.utc_ms,
update.timezone_min,
update.accuracy_ms);
return len;
}
BT_GATT_SERVICE_DEFINE(ble_time_sync_svc,
BT_GATT_PRIMARY_SERVICE(BT_UUID_TIME_SYNC_SERVICE),
BT_GATT_CHARACTERISTIC(BT_UUID_TIME_SYNC_WRITE_CHAR,
BT_GATT_CHRC_WRITE |
BT_GATT_CHRC_WRITE_WITHOUT_RESP,
BT_GATT_PERM_WRITE_ENCRYPT,
NULL,
write_time_sync,
NULL),
);
/*
* 只有 BLE 栈和 time_manager 都 ready 后,才把模块状态标记为 READY
* - 虽然静态 GATT service 会跟随蓝牙栈注册;
* - 但真正是否接受写入,仍由 module_ready 再做一层保护。
*/
static void ble_time_sync_update_ready_state(void)
{
bool should_be_ready = ble_time_sync.ble_stack_ready &&
ble_time_sync.time_manager_ready;
if (should_be_ready == ble_time_sync.module_ready) {
return;
}
ble_time_sync.module_ready = should_be_ready;
module_set_state(should_be_ready ? MODULE_STATE_READY : MODULE_STATE_STANDBY);
LOG_INF("BLE time sync %s", should_be_ready ? "ready" : "standby");
}
/* 模块依赖只来自 ble_state 和 time_manager两者 READY 顺序不做假设。 */
static bool handle_module_state_event(const struct module_state_event *event)
{
if (check_state(event, MODULE_ID(ble_state), MODULE_STATE_READY)) {
ble_time_sync.ble_stack_ready = true;
ble_time_sync_update_ready_state();
return false;
}
if (check_state(event, MODULE_ID(time_manager), MODULE_STATE_READY)) {
ble_time_sync.time_manager_ready = true;
ble_time_sync_update_ready_state();
return false;
}
return false;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
return handle_module_state_event(cast_module_state_event(aeh));
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);

View File

@@ -0,0 +1,549 @@
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/display.h>
#include <zephyr/drivers/led.h>
#include <zephyr/kernel.h>
#include <zephyr/settings/settings.h>
#include <app_event_manager.h>
#include <lvgl.h>
#include <lvgl_zephyr.h>
#define MODULE display
#include <caf/events/button_event.h>
#include <caf/events/module_state_event.h>
#include <caf/events/power_event.h>
#include "battery_status_event.h"
#include "display_theme_event.h"
#include "display_ui.h"
#include "keyboard_led_event.h"
#include "mode_event.h"
#include "time_manager.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define DISPLAY_UPDATE_PERIOD_MS 1000
#define DISPLAY_IDLE_TIMEOUT_MIN 1
#define DISPLAY_BACKLIGHT_BRIGHTNESS 100
#define DISPLAY_THEME_SAVE_DELAY K_SECONDS(1)
#define DISPLAY_THEME_STORAGE_KEY "theme"
#define DISPLAY_DEMO_BASE_YEAR 2026
#define DISPLAY_DEMO_BASE_MONTH 3
#define DISPLAY_DEMO_BASE_DAY 27
#define DISPLAY_DEMO_BASE_HOUR 14
#define DISPLAY_DEMO_BASE_MIN 28
#define DISPLAY_DEMO_BASE_SEC 36
enum display_pm_state
{
DISPLAY_PM_STATE_ACTIVE = 0,
DISPLAY_PM_STATE_OFF,
};
struct display_theme_storage {
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t valid_marker;
};
struct display_ctx
{
const struct device *dev;
struct display_capabilities caps;
struct k_work_delayable update_work;
struct k_work_delayable idle_work;
struct k_work_delayable theme_save_work;
struct display_ui_model ui;
uint32_t tick_count;
enum display_pm_state pm_state;
bool initialized;
bool theme_storage_dirty;
bool theme_storage_loaded;
char date_text[16];
char time_text[16];
};
static struct display_ctx disp = {
.dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display)),
.ui.theme_color = LV_COLOR_MAKE(0x4C, 0xC9, 0xF0),
.ui.inactive_border_color = LV_COLOR_MAKE(0xA0, 0xA7, 0xB4),
.ui.battery_level = 15U,
.ui.battery_flags = 0U,
.ui.mode = MODE_TYPE_USB,
.ui.led_mask = 0U,
.pm_state = DISPLAY_PM_STATE_OFF,
};
static struct display_theme_storage display_theme_storage;
static const struct led_dt_spec display_backlight =
LED_DT_SPEC_GET(DT_NODELABEL(backlight));
static int display_theme_store(const struct display_theme_storage *storage)
{
char key[] = MODULE_NAME "/" DISPLAY_THEME_STORAGE_KEY;
int err = settings_save_one(key, storage, sizeof(*storage));
if (err) {
LOG_ERR("Failed to save display theme err=%d", err);
return err;
}
LOG_INF("Stored display theme rgb=(%u,%u,%u)",
storage->red, storage->green, storage->blue);
return 0;
}
static void display_theme_set_rgb(uint8_t red,
uint8_t green,
uint8_t blue,
bool persist)
{
disp.ui.theme_color = lv_color_make(red, green, blue);
if (persist) {
display_theme_storage.red = red;
display_theme_storage.green = green;
display_theme_storage.blue = blue;
display_theme_storage.valid_marker = 1U;
disp.theme_storage_loaded = true;
disp.theme_storage_dirty = true;
k_work_reschedule(&disp.theme_save_work, DISPLAY_THEME_SAVE_DELAY);
}
}
static void display_theme_apply_loaded_storage(void)
{
if (!disp.theme_storage_loaded ||
(display_theme_storage.valid_marker != 1U)) {
return;
}
display_theme_set_rgb(display_theme_storage.red,
display_theme_storage.green,
display_theme_storage.blue,
false);
}
static void display_theme_save_work_fn(struct k_work *work)
{
struct display_theme_storage storage;
ARG_UNUSED(work);
if (!disp.theme_storage_dirty || !disp.theme_storage_loaded) {
return;
}
disp.theme_storage_dirty = false;
storage = display_theme_storage;
(void)display_theme_store(&storage);
}
static int settings_set(const char *key, size_t len_rd,
settings_read_cb read_cb, void *cb_arg)
{
ssize_t rc;
if (strcmp(key, DISPLAY_THEME_STORAGE_KEY) != 0) {
return 0;
}
if (len_rd != sizeof(display_theme_storage)) {
disp.theme_storage_loaded = false;
return 0;
}
rc = read_cb(cb_arg, &display_theme_storage, sizeof(display_theme_storage));
disp.theme_storage_loaded = (rc == sizeof(display_theme_storage));
return 0;
}
SETTINGS_STATIC_HANDLER_DEFINE(display,
MODULE_NAME,
NULL,
settings_set,
NULL,
NULL);
static void display_schedule_update(k_timeout_t delay)
{
#ifdef CONFIG_LV_Z_RUN_LVGL_ON_WORKQUEUE
k_work_schedule_for_queue(lvgl_get_workqueue(), &disp.update_work, delay);
#else
k_work_reschedule(&disp.update_work, delay);
#endif
}
static void display_schedule_idle_timeout(k_timeout_t delay)
{
k_work_reschedule(&disp.idle_work, delay);
}
static int display_backlight_set(uint8_t brightness)
{
int err;
if (!led_is_ready_dt(&display_backlight))
{
LOG_WRN("Display backlight device not ready");
return 0;
}
err = led_set_brightness_dt(&display_backlight, brightness);
if (err)
{
LOG_ERR("Failed to set backlight brightness(%u): %d", brightness, err);
return err;
}
return 0;
}
static bool display_is_active(void)
{
return disp.pm_state == DISPLAY_PM_STATE_ACTIVE;
}
static void display_update_datetime_text(void)
{
struct time_manager_snapshot snapshot;
int err = time_manager_get_snapshot(&snapshot);
if (!err)
{
time_t local_seconds;
struct tm tm_buf;
struct tm *tm_info;
local_seconds = (time_t)(snapshot.utc_ms / 1000ULL) +
(time_t)((int32_t)snapshot.timezone_min * 60);
tm_info = gmtime_r(&local_seconds, &tm_buf);
if (tm_info)
{
unsigned int year = (unsigned int)(tm_info->tm_year + 1900);
unsigned int month = (unsigned int)(tm_info->tm_mon + 1);
unsigned int day = (unsigned int)tm_info->tm_mday;
unsigned int hour = (unsigned int)tm_info->tm_hour;
unsigned int minute = (unsigned int)tm_info->tm_min;
unsigned int second = (unsigned int)tm_info->tm_sec;
snprintk(disp.date_text, sizeof(disp.date_text), "%04u/%02u/%02u",
year, month, day);
snprintk(disp.time_text, sizeof(disp.time_text), "%02u:%02u:%02u",
hour, minute, second);
return;
}
}
{
uint32_t seconds = disp.tick_count;
uint32_t hour = (DISPLAY_DEMO_BASE_HOUR + (seconds / 3600U)) % 24U;
uint32_t minute = (DISPLAY_DEMO_BASE_MIN + ((seconds / 60U) % 60U)) % 60U;
uint32_t second = (DISPLAY_DEMO_BASE_SEC + (seconds % 60U)) % 60U;
snprintk(disp.date_text, sizeof(disp.date_text), "%04d/%02d/%02d",
DISPLAY_DEMO_BASE_YEAR,
DISPLAY_DEMO_BASE_MONTH,
DISPLAY_DEMO_BASE_DAY);
snprintk(disp.time_text, sizeof(disp.time_text), "%02u:%02u:%02u",
hour, minute, second);
}
}
static void display_refresh_all_locked(void)
{
display_update_datetime_text();
display_ui_refresh_all(&disp.ui, disp.date_text, disp.time_text);
}
static void display_kick_idle_timer(void)
{
if (!disp.initialized || !display_is_active())
return;
display_schedule_idle_timeout(K_MINUTES(DISPLAY_IDLE_TIMEOUT_MIN));
}
static void display_sleep(void)
{
int err;
if (!disp.initialized || !display_is_active())
return;
(void)k_work_cancel_delayable(&disp.update_work);
(void)k_work_cancel_delayable(&disp.idle_work);
(void)k_work_cancel_delayable(&disp.theme_save_work);
if (disp.theme_storage_dirty && disp.theme_storage_loaded) {
disp.theme_storage_dirty = false;
(void)display_theme_store(&display_theme_storage);
}
err = display_blanking_on(disp.dev);
if (err)
LOG_WRN("Display blanking on failed: %d", err);
(void)display_backlight_set(0U);
disp.pm_state = DISPLAY_PM_STATE_OFF;
module_set_state(MODULE_STATE_OFF);
}
static void display_wake(void)
{
int err;
if (!disp.initialized)
return;
if (display_is_active()) {
display_kick_idle_timer();
return;
}
err = display_blanking_off(disp.dev);
if (err)
LOG_WRN("Display blanking off failed: %d", err);
(void)display_backlight_set(DISPLAY_BACKLIGHT_BRIGHTNESS);
lvgl_lock();
display_refresh_all_locked();
lvgl_unlock();
disp.pm_state = DISPLAY_PM_STATE_ACTIVE;
display_schedule_update(K_NO_WAIT);
display_kick_idle_timer();
module_set_state(MODULE_STATE_READY);
}
static void display_idle_timeout_fn(struct k_work *work)
{
ARG_UNUSED(work);
display_sleep();
}
static void display_update_work_fn(struct k_work *work)
{
ARG_UNUSED(work);
if (!disp.initialized)
return;
if (!display_is_active())
return;
disp.tick_count++;
display_update_datetime_text();
lvgl_lock();
display_ui_refresh_datetime(disp.date_text, disp.time_text);
lvgl_unlock();
display_schedule_update(K_MSEC(DISPLAY_UPDATE_PERIOD_MS));
}
static int display_init(void)
{
int err;
if (!device_is_ready(disp.dev))
{
LOG_ERR("Display device not ready");
return -ENODEV;
}
display_get_capabilities(disp.dev, &disp.caps);
LOG_INF("Display caps: %ux%u fmt=%d",
disp.caps.x_resolution,
disp.caps.y_resolution,
disp.caps.current_pixel_format);
k_work_init_delayable(&disp.update_work, display_update_work_fn);
k_work_init_delayable(&disp.idle_work, display_idle_timeout_fn);
k_work_init_delayable(&disp.theme_save_work, display_theme_save_work_fn);
disp.tick_count = 0U;
display_theme_apply_loaded_storage();
display_update_datetime_text();
err = display_blanking_off(disp.dev);
if (err)
{
LOG_ERR("Display blanking off failed: %d", err);
return err;
}
err = display_backlight_set(DISPLAY_BACKLIGHT_BRIGHTNESS);
if (err)
return err;
lvgl_lock();
display_ui_init(&disp.ui, disp.date_text, disp.time_text);
lvgl_unlock();
disp.initialized = true;
disp.pm_state = DISPLAY_PM_STATE_ACTIVE;
display_schedule_update(K_NO_WAIT);
display_kick_idle_timer();
LOG_INF("Display UI initialized");
return 0;
}
static bool handle_battery_status_event(const struct battery_status_event *event)
{
disp.ui.battery_level = battery_status_event_get_soc(event);
disp.ui.battery_flags = battery_status_event_get_flags(event);
if (!disp.initialized || !display_is_active()) {
return false;
}
lvgl_lock();
display_ui_refresh_battery(&disp.ui);
lvgl_unlock();
return false;
}
static bool handle_mode_event(const struct mode_event *event)
{
disp.ui.mode = event->mode_type;
if (!disp.initialized || !display_is_active()) {
return false;
}
lvgl_lock();
display_ui_refresh_status_bar(&disp.ui);
lvgl_unlock();
return false;
}
static bool handle_keyboard_led_event(const struct keyboard_led_event *event)
{
disp.ui.led_mask = keyboard_led_event_get_mask(event);
if (!disp.initialized || !display_is_active()) {
return false;
}
lvgl_lock();
display_ui_refresh_status_bar(&disp.ui);
lvgl_unlock();
return false;
}
static bool handle_display_theme_event(const struct display_theme_event *event)
{
display_theme_set_rgb(event->red, event->green, event->blue, true);
if (!disp.initialized || !display_is_active()) {
return false;
}
lvgl_lock();
display_ui_refresh_status_bar(&disp.ui);
lvgl_unlock();
return false;
}
static bool handle_button_event(const struct button_event *event)
{
ARG_UNUSED(event);
display_wake();
return false;
}
static bool handle_power_down_event(void)
{
display_sleep();
return false;
}
static bool handle_wake_up_event(void)
{
display_wake();
return false;
}
static bool handle_module_state_event(const struct module_state_event *event)
{
if (check_state(event, MODULE_ID(settings_loader), MODULE_STATE_READY)) {
display_theme_apply_loaded_storage();
if (disp.initialized && display_is_active()) {
lvgl_lock();
display_ui_refresh_status_bar(&disp.ui);
lvgl_unlock();
}
return false;
}
if (!check_state(event, MODULE_ID(main), MODULE_STATE_READY))
return false;
if (!display_init())
{
module_set_state(MODULE_STATE_READY);
}
else
{
module_set_state(MODULE_STATE_ERROR);
}
return false;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh))
return handle_module_state_event(cast_module_state_event(aeh));
if (is_battery_status_event(aeh))
return handle_battery_status_event(cast_battery_status_event(aeh));
if (is_mode_event(aeh))
return handle_mode_event(cast_mode_event(aeh));
if (is_keyboard_led_event(aeh))
return handle_keyboard_led_event(cast_keyboard_led_event(aeh));
if (is_display_theme_event(aeh))
return handle_display_theme_event(cast_display_theme_event(aeh));
if (is_button_event(aeh))
return handle_button_event(cast_button_event(aeh));
if (is_power_down_event(aeh))
return handle_power_down_event();
if (is_wake_up_event(aeh))
return handle_wake_up_event();
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE(MODULE, battery_status_event);
APP_EVENT_SUBSCRIBE(MODULE, display_theme_event);
APP_EVENT_SUBSCRIBE(MODULE, mode_event);
APP_EVENT_SUBSCRIBE(MODULE, keyboard_led_event);
APP_EVENT_SUBSCRIBE(MODULE, button_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event);
APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);

View File

@@ -0,0 +1,121 @@
#include <zephyr/sys/byteorder.h>
#include <app_event_manager.h>
#define MODULE hid_host_command
#include <caf/events/module_state_event.h>
#include "display_theme_event.h"
#include "hid_host_ack_event.h"
#include "hid_host_command_error_event.h"
#include "hid_host_command_event.h"
#include "hid_host_command_protocol.h"
#include "time_manager.h"
#include "time_sync_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
static bool module_ready;
static bool handle_theme_color_command(const struct hid_host_command_event *event)
{
if (event->data_len < HID_HOST_CMD_THEME_PARAM_SIZE) {
hid_host_command_error_event_submit(
event->transport,
event->cmd,
HID_HOST_COMMAND_ERROR_INVALID_LENGTH);
return false;
}
display_theme_event_submit(event->data[0], event->data[1], event->data[2]);
hid_host_ack_event_submit(event->transport, event->cmd);
return false;
}
static bool handle_time_sync_command(const struct hid_host_command_event *event)
{
struct time_sync_update update = {
.timezone_min = 0,
.accuracy_ms = 0,
.source = TIME_SYNC_SOURCE_HID,
};
if (event->data_len != HID_HOST_CMD_TIME_SYNC_PARAM_SIZE) {
hid_host_command_error_event_submit(
event->transport,
event->cmd,
HID_HOST_COMMAND_ERROR_INVALID_LENGTH);
return false;
}
if (!time_manager_is_ready()) {
hid_host_command_error_event_submit(
event->transport,
event->cmd,
HID_HOST_COMMAND_ERROR_NOT_READY);
return false;
}
update.utc_ms = sys_get_le64(event->data);
if (update.utc_ms == 0U) {
hid_host_command_error_event_submit(
event->transport,
event->cmd,
HID_HOST_COMMAND_ERROR_INVALID_PARAM);
return false;
}
time_sync_event_submit(&update);
hid_host_ack_event_submit(event->transport, event->cmd);
return false;
}
static bool handle_hid_host_command_event(const struct hid_host_command_event *event)
{
if (!module_ready) {
hid_host_command_error_event_submit(
event->transport,
event->cmd,
HID_HOST_COMMAND_ERROR_NOT_READY);
return false;
}
switch (event->cmd) {
case HID_HOST_CMD_ID_THEME_COLOR:
return handle_theme_color_command(event);
case HID_HOST_CMD_ID_TIME_SYNC:
return handle_time_sync_command(event);
default:
hid_host_command_error_event_submit(
event->transport,
event->cmd,
HID_HOST_COMMAND_ERROR_UNKNOWN_CMD);
return false;
}
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
const struct module_state_event *event = cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
module_ready = true;
module_set_state(MODULE_STATE_READY);
}
return false;
}
if (is_hid_host_command_event(aeh)) {
return handle_hid_host_command_event(cast_hid_host_command_event(aeh));
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE(MODULE, hid_host_command_event);

View File

@@ -0,0 +1,313 @@
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/atomic.h>
#include <app_event_manager.h>
#define MODULE hid_tx_manager
#include <caf/events/module_state_event.h>
#include "hid_report_descriptor.h"
#include "hid_boot_event.h"
#include "hid_host_ack_event.h"
#include "hid_report_event.h"
#include "hid_tx_done_event.h"
#include "hid_tx_event.h"
#include "mode_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define HID_TX_QUEUE_SIZE 16
#define HID_TX_MAX_DATA 32
enum hid_tx_flag {
HID_TX_FLAG_INITIALIZED = 0,
HID_TX_FLAG_IN_FLIGHT,
HID_TX_FLAG_BOOT_VALID,
HID_TX_FLAG_BOOT_DIRTY,
HID_TX_FLAG_KEYBOARD_VALID,
HID_TX_FLAG_KEYBOARD_DIRTY,
HID_TX_FLAG_VENDOR_VALID,
HID_TX_FLAG_VENDOR_DIRTY,
};
struct hid_tx_item {
enum hid_tx_kind kind;
enum hid_tx_route route;
size_t len;
uint8_t data[HID_TX_MAX_DATA];
};
struct hid_tx_ctx {
atomic_t flags;
struct hid_tx_item boot_state;
struct hid_tx_item keyboard_state;
struct hid_tx_item vendor_state;
struct hid_tx_item inflight_item;
mode_type_t active_mode;
};
static struct hid_tx_ctx tx = {
.active_mode = MODE_TYPE_COUNT,
};
K_MSGQ_DEFINE(hid_tx_consumer_msgq, sizeof(struct hid_tx_item), HID_TX_QUEUE_SIZE, 4);
K_MSGQ_DEFINE(hid_tx_ack_msgq, sizeof(struct hid_tx_item), HID_TX_QUEUE_SIZE, 4);
K_MSGQ_DEFINE(hid_tx_misc_msgq, sizeof(struct hid_tx_item), HID_TX_QUEUE_SIZE, 4);
static bool hid_tx_item_store(struct hid_tx_item *item,
enum hid_tx_kind kind,
enum hid_tx_route route,
const uint8_t *data,
size_t len)
{
if (len > HID_TX_MAX_DATA) {
LOG_WRN("Drop HID tx kind=%u len=%u: too large", kind, len);
return false;
}
item->kind = kind;
item->route = route;
item->len = len;
if ((len > 0U) && (data != NULL)) {
memcpy(item->data, data, len);
}
return true;
}
static bool hid_tx_queue_push(struct k_msgq *queue,
enum hid_tx_kind kind,
enum hid_tx_route route,
const uint8_t *data,
size_t len)
{
struct hid_tx_item item;
if (!hid_tx_item_store(&item, kind, route, data, len)) {
return false;
}
if (k_msgq_put(queue, &item, K_NO_WAIT)) {
LOG_WRN("Drop HID tx kind=%u len=%u: queue full", kind, len);
return false;
}
return true;
}
static bool hid_tx_auto_route_available(void)
{
return (tx.active_mode == MODE_TYPE_USB) || (tx.active_mode == MODE_TYPE_BLE);
}
static bool hid_tx_dispatch_item(const struct hid_tx_item *item)
{
tx.inflight_item = *item;
atomic_set_bit(&tx.flags, HID_TX_FLAG_IN_FLIGHT);
hid_tx_event_submit_routed(item->kind, item->route, item->data, item->len);
return true;
}
static void dispatch_next_if_possible(void)
{
struct hid_tx_item item;
if (!atomic_test_bit(&tx.flags, HID_TX_FLAG_INITIALIZED) ||
atomic_test_bit(&tx.flags, HID_TX_FLAG_IN_FLIGHT)) {
return;
}
if (hid_tx_auto_route_available() &&
atomic_test_bit(&tx.flags, HID_TX_FLAG_KEYBOARD_DIRTY) &&
atomic_test_bit(&tx.flags, HID_TX_FLAG_KEYBOARD_VALID)) {
atomic_clear_bit(&tx.flags, HID_TX_FLAG_KEYBOARD_DIRTY);
(void)hid_tx_dispatch_item(&tx.keyboard_state);
return;
}
if (hid_tx_auto_route_available() &&
atomic_test_bit(&tx.flags, HID_TX_FLAG_BOOT_DIRTY) &&
atomic_test_bit(&tx.flags, HID_TX_FLAG_BOOT_VALID)) {
atomic_clear_bit(&tx.flags, HID_TX_FLAG_BOOT_DIRTY);
(void)hid_tx_dispatch_item(&tx.boot_state);
return;
}
if (hid_tx_auto_route_available() &&
!k_msgq_get(&hid_tx_consumer_msgq, &item, K_NO_WAIT)) {
(void)hid_tx_dispatch_item(&item);
return;
}
if (hid_tx_auto_route_available() &&
atomic_test_bit(&tx.flags, HID_TX_FLAG_VENDOR_DIRTY) &&
atomic_test_bit(&tx.flags, HID_TX_FLAG_VENDOR_VALID)) {
atomic_clear_bit(&tx.flags, HID_TX_FLAG_VENDOR_DIRTY);
(void)hid_tx_dispatch_item(&tx.vendor_state);
return;
}
if (!k_msgq_get(&hid_tx_ack_msgq, &item, K_NO_WAIT)) {
(void)hid_tx_dispatch_item(&item);
return;
}
if (hid_tx_auto_route_available() &&
!k_msgq_get(&hid_tx_misc_msgq, &item, K_NO_WAIT)) {
(void)hid_tx_dispatch_item(&item);
}
}
static bool handle_module_state_event(const struct module_state_event *event)
{
if (!check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
return false;
}
__ASSERT_NO_MSG(!atomic_test_bit(&tx.flags, HID_TX_FLAG_INITIALIZED));
atomic_set_bit(&tx.flags, HID_TX_FLAG_INITIALIZED);
module_set_state(MODULE_STATE_READY);
dispatch_next_if_possible();
return false;
}
static bool handle_mode_event(const struct mode_event *event)
{
tx.active_mode = event->mode_type;
dispatch_next_if_possible();
return false;
}
static enum hid_tx_route hid_tx_route_from_transport(enum hid_host_transport transport)
{
switch (transport) {
case HID_HOST_TRANSPORT_USB:
return HID_TX_ROUTE_USB;
case HID_HOST_TRANSPORT_BLE:
return HID_TX_ROUTE_BLE;
default:
return HID_TX_ROUTE_AUTO;
}
}
static bool handle_hid_boot_request_event(const struct hid_boot_event *event)
{
(void)hid_tx_item_store(&tx.boot_state,
HID_TX_KIND_BOOT,
HID_TX_ROUTE_AUTO,
hid_boot_event_get_data(event),
hid_boot_event_get_size(event));
atomic_set_bit(&tx.flags, HID_TX_FLAG_BOOT_VALID);
atomic_set_bit(&tx.flags, HID_TX_FLAG_BOOT_DIRTY);
dispatch_next_if_possible();
return true;
}
static bool handle_hid_report_request_event(const struct hid_report_event *event)
{
const uint8_t *data = hid_report_event_get_data(event);
size_t len = hid_report_event_get_size(event);
if ((len > 0U) && (data[0] == REPORT_ID_KEYBOARD)) {
(void)hid_tx_item_store(&tx.keyboard_state,
HID_TX_KIND_REPORT,
HID_TX_ROUTE_AUTO,
data, len);
atomic_set_bit(&tx.flags, HID_TX_FLAG_KEYBOARD_VALID);
atomic_set_bit(&tx.flags, HID_TX_FLAG_KEYBOARD_DIRTY);
} else if ((len > 0U) && (data[0] == REPORT_ID_CONSUMER)) {
(void)hid_tx_queue_push(&hid_tx_consumer_msgq,
HID_TX_KIND_REPORT,
HID_TX_ROUTE_AUTO,
data, len);
} else if ((len > 0U) && (data[0] == REPORT_ID_VENDOR)) {
(void)hid_tx_item_store(&tx.vendor_state,
HID_TX_KIND_REPORT,
HID_TX_ROUTE_AUTO,
data, len);
atomic_set_bit(&tx.flags, HID_TX_FLAG_VENDOR_VALID);
atomic_set_bit(&tx.flags, HID_TX_FLAG_VENDOR_DIRTY);
} else {
(void)hid_tx_queue_push(&hid_tx_misc_msgq,
HID_TX_KIND_REPORT,
HID_TX_ROUTE_AUTO,
data, len);
}
dispatch_next_if_possible();
return true;
}
static bool handle_hid_host_ack_event(const struct hid_host_ack_event *event)
{
uint8_t report[1U + HID_VENDOR_ACK_PAYLOAD_SIZE] = {
REPORT_ID_VENDOR_CMD,
event->cmd,
};
(void)hid_tx_queue_push(&hid_tx_ack_msgq,
HID_TX_KIND_REPORT,
hid_tx_route_from_transport(event->transport),
report,
sizeof(report));
dispatch_next_if_possible();
return true;
}
static bool handle_hid_tx_done_event(const struct hid_tx_done_event *event)
{
if (!atomic_test_bit(&tx.flags, HID_TX_FLAG_IN_FLIGHT)) {
return false;
}
if (event->kind != tx.inflight_item.kind) {
return false;
}
atomic_clear_bit(&tx.flags, HID_TX_FLAG_IN_FLIGHT);
dispatch_next_if_possible();
return false;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
return handle_module_state_event(cast_module_state_event(aeh));
}
if (is_mode_event(aeh)) {
return handle_mode_event(cast_mode_event(aeh));
}
if (is_hid_boot_event(aeh)) {
return handle_hid_boot_request_event(cast_hid_boot_event(aeh));
}
if (is_hid_report_event(aeh)) {
return handle_hid_report_request_event(cast_hid_report_event(aeh));
}
if (is_hid_tx_done_event(aeh)) {
return handle_hid_tx_done_event(cast_hid_tx_done_event(aeh));
}
if (is_hid_host_ack_event(aeh)) {
return handle_hid_host_ack_event(cast_hid_host_ack_event(aeh));
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE(MODULE, mode_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, hid_boot_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, hid_report_event);
APP_EVENT_SUBSCRIBE(MODULE, hid_host_ack_event);
APP_EVENT_SUBSCRIBE(MODULE, hid_tx_done_event);

View File

@@ -0,0 +1,440 @@
#include <string.h>
#include <stdlib.h>
#include <app_event_manager.h>
#define MODULE keyboard
#include <caf/events/module_state_event.h>
#include <caf/events/button_event.h>
#include <caf/key_id.h>
#include "hid_report_descriptor.h"
#include "hid_boot_event.h"
#include "hid_protocol_event.h"
#include "hid_report_event.h"
#include "hid_vendor_mask_event.h"
#include "qdec_step_event.h"
#include <zephyr/sys/util.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
/*
* 参考 nrf_desktop 的表驱动设计:
* - key_id -> (usage_id, report_id) 映射定义在外部 hid_keymap_def.h
* - keyboard_module 内部完成映射表校验与查询,不再依赖独立 hid_keymap 模块。
*/
struct hid_keymap {
uint16_t key_id;
uint16_t usage_id;
uint8_t report_id;
};
#include APP_HID_KEYMAP_DEF_PATH
static bool hid_keymap_initialized;
/* 比较函数:供 bsearch 按 key_id 升序查找映射项。 */
static int hid_keymap_compare(const void *a, const void *b)
{
const struct hid_keymap *pa = a;
const struct hid_keymap *pb = b;
return ((int)pa->key_id - (int)pb->key_id);
}
/*
* 初始化并校验 hid_keymap
* - 仅在 CONFIG_ASSERT 打开时执行校验,避免 release 构建引入额外开销;
* - 校验 key_id 严格升序,确保二分查找行为正确;
* - 校验 report_id 只落在当前模块支持的 Keyboard/Consumer 两类。
*/
static void hid_keymap_init_local(void)
{
if (!IS_ENABLED(CONFIG_ASSERT) || hid_keymap_initialized) {
return;
}
for (size_t i = 0; i < ARRAY_SIZE(hid_keymap); i++) {
if (i > 0U) {
__ASSERT(hid_keymap[i - 1].key_id < hid_keymap[i].key_id,
"hid_keymap must be sorted by key_id");
}
__ASSERT((hid_keymap[i].report_id == REPORT_ID_KEYBOARD) ||
(hid_keymap[i].report_id == REPORT_ID_CONSUMER),
"hid_keymap uses unsupported report_id");
}
hid_keymap_initialized = true;
}
/* 查询指定 key_id 的 HID 映射,查不到返回 NULL。 */
static const struct hid_keymap *hid_keymap_get_local(uint16_t key_id)
{
if (ARRAY_SIZE(hid_keymap) == 0U) {
return NULL;
}
struct hid_keymap key = {
.key_id = key_id,
.usage_id = 0U,
.report_id = 0U,
};
return bsearch(&key,
hid_keymap,
ARRAY_SIZE(hid_keymap),
sizeof(hid_keymap[0]),
hid_keymap_compare);
}
/* Report 协议键盘 payload: modifier(1) + usage bitset(0..0xE7 => 29B)。 */
#define KEYBOARD_USAGE_MAX HID_KBD_USAGE_MAX
#define KEYBOARD_BITMAP_SIZE HID_KBD_BITMAP_SIZE
struct keyboard_state {
uint8_t physical_modifier_bm;
uint8_t physical_usage_bm[KEYBOARD_BITMAP_SIZE];
uint8_t mask_modifier_bm;
uint8_t mask_bm[KEYBOARD_BITMAP_SIZE];
enum hid_protocol_type current_protocol;
uint16_t consumer_usage;
};
static struct keyboard_state ks = {
.current_protocol = HID_PROTO_REPORT,
.consumer_usage = 0,
};
/* 当前 HID 报告编码仅跟最近一次 set_protocol 结果相关。 */
static enum hid_protocol_type active_protocol_get(void)
{
return ks.current_protocol;
}
static void submit_hid_report(enum hid_protocol_type protocol,
uint8_t report_id,
const uint8_t *payload,
size_t payload_len);
static void keyboard_mask_init(void)
{
ks.mask_modifier_bm = 0xFF;
memset(ks.mask_bm, 0xFF, sizeof(ks.mask_bm));
}
/* 查询某 usage 位在当前键盘位图里是否处于按下状态。 */
static bool usage_pressed(uint16_t usage)
{
if (usage > KEYBOARD_USAGE_MAX) {
return false;
}
return (ks.physical_usage_bm[usage / 8] & BIT(usage % 8)) != 0U;
}
/*
* 更新键盘 usage 位图与 modifier 状态。
* 返回 true 表示状态有变化,需要向传输层同步新报告。
*/
static bool keyboard_usage_update(uint16_t usage_id, bool pressed)
{
if (usage_id > KEYBOARD_USAGE_MAX) {
LOG_WRN("Unsupported usage_id=0x%04x", usage_id);
return false;
}
uint8_t idx = usage_id / 8;
uint8_t mask = BIT(usage_id % 8);
bool changed = false;
if (pressed) {
if ((ks.physical_usage_bm[idx] & mask) == 0U) {
ks.physical_usage_bm[idx] |= mask;
changed = true;
}
} else {
if ((ks.physical_usage_bm[idx] & mask) != 0U) {
ks.physical_usage_bm[idx] &= (uint8_t)~mask;
changed = true;
}
}
/* modifier(E0~E7) 额外维护一份 bitmask便于 Boot/Report 复用。 */
if ((usage_id >= 0x00E0) && (usage_id <= 0x00E7)) {
uint8_t mod_mask = BIT(usage_id - 0x00E0);
if (pressed) {
ks.physical_modifier_bm |= mod_mask;
} else {
ks.physical_modifier_bm &= (uint8_t)~mod_mask;
}
}
return changed;
}
static uint8_t masked_modifier_get(void)
{
return ks.physical_modifier_bm & ks.mask_modifier_bm;
}
static void build_masked_keyboard_payload(uint8_t payload[HID_KBD_PAYLOAD_SIZE])
{
payload[0] = masked_modifier_get();
for (size_t i = 0; i < KEYBOARD_BITMAP_SIZE; i++) {
payload[1U + i] = ks.physical_usage_bm[i] & ks.mask_bm[i];
}
}
static void submit_vendor_report_payload(void)
{
uint8_t payload[HID_VENDOR_PAYLOAD_SIZE];
if (active_protocol_get() != HID_PROTO_REPORT) {
return;
}
payload[0] = ks.physical_modifier_bm;
memcpy(&payload[1], ks.physical_usage_bm, sizeof(ks.physical_usage_bm));
submit_hid_report(HID_PROTO_REPORT, REPORT_ID_VENDOR, payload, sizeof(payload));
}
/*
* 提交 HID 报告事件:
* - Report 协议编码为 [report_id | payload]
* - Boot 协议编码为 [payload](不含 report_id
*/
static void submit_hid_report(enum hid_protocol_type protocol,
uint8_t report_id,
const uint8_t *payload,
size_t payload_len)
{
uint8_t report_buf[HID_FULL_REPORT_SIZE(HID_KBD_PAYLOAD_SIZE)];
if (protocol == HID_PROTO_REPORT) {
size_t report_len = payload_len + 1U;
report_buf[0] = report_id;
memcpy(&report_buf[1], payload, payload_len);
hid_report_event_submit(report_buf, report_len);
} else {
hid_boot_event_submit(payload, payload_len);
}
}
/*
* 组包并提交键盘报告:
* - Report 协议发送 NKRO payload
* - Boot 协议降级为 6KRO 固定 8 字节格式。
*/
static void submit_keyboard_report_payload(enum hid_protocol_type protocol)
{
if (protocol == HID_PROTO_REPORT) {
uint8_t payload[HID_KBD_PAYLOAD_SIZE];
build_masked_keyboard_payload(payload);
submit_hid_report(HID_PROTO_REPORT, REPORT_ID_KEYBOARD,
payload, sizeof(payload));
return;
}
/*
* Boot 协议只支持 6KRO。
* 从 usage 位图中按升序提取最多 6 个普通键modifier 走独立字节。
*/
uint8_t payload[HID_BOOT_KBD_PAYLOAD_SIZE] = { 0 };
size_t key_pos = 2;
payload[0] = masked_modifier_get();
for (uint16_t usage = 0x04; usage <= 0x65; usage++) {
if (!usage_pressed(usage)) {
continue;
}
payload[key_pos++] = (uint8_t)usage;
if (key_pos >= ARRAY_SIZE(payload)) {
break;
}
}
submit_hid_report(HID_PROTO_BOOT, REPORT_ID_KEYBOARD, payload, sizeof(payload));
}
/* 组包并提交 consumer 报告16-bit usage。 */
static void submit_consumer_report_payload(void)
{
uint8_t payload[HID_CONSUMER_PAYLOAD_SIZE];
payload[0] = ks.consumer_usage & 0xFF;
payload[1] = (ks.consumer_usage >> 8) & 0xFF;
submit_hid_report(HID_PROTO_REPORT, REPORT_ID_CONSUMER, payload, sizeof(payload));
}
static void submit_consumer_click_usage(uint16_t usage_id)
{
uint8_t payload[HID_CONSUMER_PAYLOAD_SIZE];
if (active_protocol_get() == HID_PROTO_BOOT) {
return;
}
payload[0] = usage_id & 0xFF;
payload[1] = (usage_id >> 8) & 0xFF;
submit_hid_report(HID_PROTO_REPORT, REPORT_ID_CONSUMER, payload, sizeof(payload));
payload[0] = 0U;
payload[1] = 0U;
submit_hid_report(HID_PROTO_REPORT, REPORT_ID_CONSUMER, payload, sizeof(payload));
}
/*
* 处理键盘类 usage
* - 仅在按键状态实际变化时提交报告,避免无效重复上报。
*/
static bool handle_keyboard_usage_event(const struct hid_keymap *map, bool pressed)
{
if (!keyboard_usage_update(map->usage_id, pressed))
return false;
submit_keyboard_report_payload(active_protocol_get());
submit_vendor_report_payload();
return false;
}
/*
* 处理 consumer 类 usage
* - Boot 协议不发送 consumer 报告;
* - 按下时上报 usage抬起时上报 0 清状态。
*/
static bool handle_consumer_usage_event(const struct hid_keymap *map, bool pressed)
{
if (active_protocol_get() == HID_PROTO_BOOT)
return false;
if (pressed) {
if (ks.consumer_usage == map->usage_id)
return false;
ks.consumer_usage = map->usage_id;
submit_consumer_report_payload();
return false;
}
if (ks.consumer_usage != map->usage_id)
return false;
ks.consumer_usage = 0U;
submit_consumer_report_payload();
return false;
}
/*
* 处理 button_event
* - 先查 key_id 映射;
* - 再按 report_id 分派到键盘/consumer 分支。
*/
static bool handle_button_event(const struct button_event *event)
{
const struct hid_keymap *map = hid_keymap_get_local(event->key_id);
if (!map) {
return false;
}
if (map->report_id == REPORT_ID_KEYBOARD)
return handle_keyboard_usage_event(map, event->pressed);
if (map->report_id == REPORT_ID_CONSUMER)
return handle_consumer_usage_event(map, event->pressed);
LOG_WRN("Unsupported report_id=%u key_id=0x%04x", map->report_id, event->key_id);
return false;
}
/* 记录最近一次 set_protocol 结果。 */
static bool handle_hid_protocol_event(const struct hid_protocol_event *event)
{
ks.current_protocol = hid_protocol_event_get_protocol(event);
return false;
}
static bool handle_hid_vendor_mask_event(const struct hid_vendor_mask_event *event)
{
const uint8_t *data = hid_vendor_mask_event_get_data(event);
size_t size = hid_vendor_mask_event_get_size(event);
if (size != HID_VENDOR_PAYLOAD_SIZE) {
LOG_WRN("Ignore vendor mask len=%u expect=%u",
size, HID_VENDOR_PAYLOAD_SIZE);
return false;
}
ks.mask_modifier_bm = data[0];
memcpy(ks.mask_bm, &data[1], sizeof(ks.mask_bm));
submit_keyboard_report_payload(active_protocol_get());
submit_vendor_report_payload();
return false;
}
static bool handle_qdec_step_event(const struct qdec_step_event *event)
{
int8_t step = qdec_step_event_get_step(event);
uint16_t usage_id;
if (step == 0) {
return false;
}
usage_id = (step > 0) ? 0x00E9U : 0x00EAU;
submit_consumer_click_usage(usage_id);
return false;
}
/* 模块总事件分发入口。 */
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_button_event(aeh)) {
return handle_button_event(cast_button_event(aeh));
}
if (is_hid_protocol_event(aeh)) {
return handle_hid_protocol_event(cast_hid_protocol_event(aeh));
}
if (is_qdec_step_event(aeh)) {
return handle_qdec_step_event(cast_qdec_step_event(aeh));
}
if (is_hid_vendor_mask_event(aeh)) {
return handle_hid_vendor_mask_event(cast_hid_vendor_mask_event(aeh));
}
if (is_module_state_event(aeh)) {
const struct module_state_event *event = cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
/* 主模块 ready 后做一次 keymap 结构校验。 */
hid_keymap_init_local();
keyboard_mask_init();
module_set_state(MODULE_STATE_READY);
}
return false;
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, button_event);
APP_EVENT_SUBSCRIBE(MODULE, hid_protocol_event);
APP_EVENT_SUBSCRIBE(MODULE, hid_vendor_mask_event);
APP_EVENT_SUBSCRIBE(MODULE, qdec_step_event);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);

View File

@@ -0,0 +1,180 @@
#include <zephyr/kernel.h>
#include <app_event_manager.h>
#define MODULE led_state
#include <caf/events/module_state_event.h>
#include <caf/events/ble_common_event.h>
#include <caf/events/led_event.h>
#include "keyboard_led_event.h"
#include "led_state.h"
#include "mode_event.h"
#include "led_state_def.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
static uint8_t connected_peer_count;
static bool ble_mode_selected;
static bool peer_search_active;
static enum peer_operation peer_op = PEER_OPERATION_CANCEL;
static bool num_lock_on;
/* 根据当前聚合上下文决定 BLE 状态灯的逻辑状态。 */
static enum led_ble_state resolve_ble_led_state(void)
{
if (!ble_mode_selected)
return LED_BLE_STATE_OFF;
switch (peer_op) {
case PEER_OPERATION_SELECT:
case PEER_OPERATION_ERASE:
case PEER_OPERATION_ERASE_ADV:
return LED_BLE_STATE_PAIRING;
case PEER_OPERATION_SELECTED:
case PEER_OPERATION_ERASE_ADV_CANCEL:
case PEER_OPERATION_ERASED:
case PEER_OPERATION_CANCEL:
case PEER_OPERATION_SCAN_REQUEST:
if (peer_search_active)
return LED_BLE_STATE_PAIRING;
if (connected_peer_count > 0U)
return LED_BLE_STATE_CONNECTED;
return LED_BLE_STATE_WAIT_RECONNECT;
default:
__ASSERT_NO_MSG(false);
return LED_BLE_STATE_OFF;
}
}
/* 发布 Num Lock 灯效。 */
static void submit_num_lock_led(void)
{
if (led_map[LED_ID_NUM_LOCK] == LED_UNAVAILABLE)
return;
enum led_num_lock_state state =
num_lock_on ? LED_NUM_LOCK_STATE_ON : LED_NUM_LOCK_STATE_OFF;
struct led_event *event = new_led_event();
event->led_id = led_map[LED_ID_NUM_LOCK];
event->led_effect = &led_num_lock_state_effect[state];
APP_EVENT_SUBMIT(event);
}
/* 发布 BLE 状态灯效。 */
static void submit_ble_led(void)
{
if (led_map[LED_ID_BLE_STATE] == LED_UNAVAILABLE)
return;
enum led_ble_state state = resolve_ble_led_state();
struct led_event *event = new_led_event();
event->led_id = led_map[LED_ID_BLE_STATE];
event->led_effect = &led_ble_state_effect[state];
APP_EVENT_SUBMIT(event);
}
static bool handle_mode_event(const struct mode_event *event)
{
ble_mode_selected = (event->mode_type == MODE_TYPE_BLE);
submit_ble_led();
return false;
}
static bool handle_ble_peer_event(const struct ble_peer_event *event)
{
switch (event->state) {
case PEER_STATE_CONNECTED:
__ASSERT_NO_MSG(connected_peer_count < UINT8_MAX);
connected_peer_count++;
break;
case PEER_STATE_DISCONNECTED:
__ASSERT_NO_MSG(connected_peer_count > 0U);
connected_peer_count--;
break;
case PEER_STATE_SECURED:
case PEER_STATE_CONN_FAILED:
case PEER_STATE_DISCONNECTING:
break;
default:
__ASSERT_NO_MSG(false);
break;
}
submit_ble_led();
return false;
}
static bool handle_ble_peer_search_event(const struct ble_peer_search_event *event)
{
peer_search_active = event->active;
submit_ble_led();
return false;
}
static bool handle_ble_peer_operation_event(const struct ble_peer_operation_event *event)
{
peer_op = event->op;
submit_ble_led();
return false;
}
static bool handle_keyboard_led_event(const struct keyboard_led_event *event)
{
num_lock_on = keyboard_led_event_is_num_lock_on(event);
submit_num_lock_led();
return false;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
const struct module_state_event *event = cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
submit_num_lock_led();
submit_ble_led();
module_set_state(MODULE_STATE_READY);
}
return false;
}
if (is_mode_event(aeh))
return handle_mode_event(cast_mode_event(aeh));
if (is_keyboard_led_event(aeh))
return handle_keyboard_led_event(cast_keyboard_led_event(aeh));
if (IS_ENABLED(CONFIG_CAF_BLE_COMMON_EVENTS) && is_ble_peer_event(aeh))
return handle_ble_peer_event(cast_ble_peer_event(aeh));
if (IS_ENABLED(CONFIG_CAF_BLE_COMMON_EVENTS) && is_ble_peer_search_event(aeh))
return handle_ble_peer_search_event(cast_ble_peer_search_event(aeh));
if (IS_ENABLED(CONFIG_CAF_BLE_COMMON_EVENTS) && is_ble_peer_operation_event(aeh))
return handle_ble_peer_operation_event(cast_ble_peer_operation_event(aeh));
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE(MODULE, mode_event);
APP_EVENT_SUBSCRIBE(MODULE, keyboard_led_event);
#ifdef CONFIG_CAF_BLE_COMMON_EVENTS
APP_EVENT_SUBSCRIBE(MODULE, ble_peer_event);
APP_EVENT_SUBSCRIBE(MODULE, ble_peer_search_event);
APP_EVENT_SUBSCRIBE(MODULE, ble_peer_operation_event);
#endif

View File

@@ -0,0 +1,219 @@
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/sys/atomic.h>
#include <zephyr/sys/util.h>
#include <stdlib.h>
#include <app_event_manager.h>
#include <caf/events/power_event.h>
#include <caf/events/keep_alive_event.h>
#define MODULE mode_switch
#include <caf/events/module_state_event.h>
#include "mode_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE);
#define MODE_SENSE_NODE DT_NODELABEL(mode_sense)
#define MODE_SAMPLE_INTERVAL_MS 50
static const struct device *const mode_sensor_dev = DEVICE_DT_GET(MODE_SENSE_NODE);
static struct k_work_delayable mode_sample_work;
static atomic_t active;
static mode_type_t current_mode;
static uint8_t mode_stable_status;
static int init_mode_sensor(void)
{
if (!device_is_ready(mode_sensor_dev))
{
LOG_ERR("Mode sense device not ready");
return -ENODEV;
}
return 0;
}
static mode_type_t classify_mode_from_mv(int32_t mv)
{
/*
* 使用“距离最近中心点”的分类方式,避免阈值边界附近抖动时出现模式跳变。
* 三个中心点直接对应硬件设计目标电压USB=0mV、2.4G=1650mV、BLE=3300mV。
*/
static const int32_t centers[MODE_TYPE_COUNT] = {
[MODE_TYPE_USB] = 0,
[MODE_TYPE_BLE] = 3300,
[MODE_TYPE_2G4] = 1650,
};
int32_t best_diff = INT32_MAX;
mode_type_t best_mode = MODE_TYPE_USB;
for (size_t i = 0; i < MODE_TYPE_COUNT; i++)
{
int32_t diff = abs(mv - centers[i]);
if (diff < best_diff)
{
best_diff = diff;
best_mode = (mode_type_t)i;
}
}
return best_mode;
}
static int read_mode(mode_type_t *mode)
{
struct sensor_value value;
int err = sensor_sample_fetch(mode_sensor_dev);
if (err)
{
LOG_WRN("sensor_sample_fetch(mode) failed (err=%d)", err);
return err;
}
err = sensor_channel_get(mode_sensor_dev, SENSOR_CHAN_VOLTAGE, &value);
if (err)
{
LOG_WRN("sensor_channel_get(mode) failed (err=%d)", err);
return err;
}
int32_t v = (int32_t)sensor_value_to_milli(&value);
*mode = classify_mode_from_mv(v);
return 0;
}
static void publish_mode_event(mode_type_t mode)
{
if (current_mode == mode)
return;
current_mode = mode;
mode_event_submit(mode);
/*
* 模式切换是明确的人机交互动作。这里同步上报 keep_alive_event
* 让 power manager 重置休眠倒计时,避免用户刚切换模式就进入省电流程。
*/
keep_alive();
}
static void mode_sample_fn(struct k_work *work)
{
ARG_UNUSED(work);
if (!atomic_get(&active))
{
return;
}
mode_type_t sampled_mode;
int err = read_mode(&sampled_mode);
if (err)
{
LOG_ERR("ADC read failed (err=%d)", err);
module_set_state(MODULE_STATE_ERROR);
return;
}
mode_stable_status = mode_stable_status << 2;
mode_stable_status |= (sampled_mode & 0x3);
switch (mode_stable_status)
{
case 0b00000000:
publish_mode_event(MODE_TYPE_USB);
break;
case 0b01010101:
publish_mode_event(MODE_TYPE_BLE);
break;
case 0b10101010:
publish_mode_event(MODE_TYPE_2G4);
break;
default:
break;
}
k_work_reschedule(&mode_sample_work, K_MSEC(MODE_SAMPLE_INTERVAL_MS));
}
static void mode_switch_suspend(void)
{
if (!atomic_get(&active))
return;
atomic_set(&active, false);
(void)k_work_cancel_delayable(&mode_sample_work);
module_set_state(MODULE_STATE_STANDBY);
}
static void mode_switch_resume(void)
{
if (atomic_get(&active))
return;
atomic_set(&active, true);
k_work_reschedule(&mode_sample_work, K_NO_WAIT);
module_set_state(MODULE_STATE_READY);
}
static void init_mode_switch(void)
{
if (atomic_get(&active))
return;
if (init_mode_sensor())
{
module_set_state(MODULE_STATE_ERROR);
return;
}
mode_stable_status = 0xFF;
current_mode = MODE_TYPE_COUNT;
atomic_set(&active, false);
k_work_init_delayable(&mode_sample_work, mode_sample_fn);
mode_switch_resume();
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh))
{
const struct module_state_event *event = cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY))
{
init_mode_switch();
}
return false;
}
if (is_power_down_event(aeh))
{
mode_switch_suspend();
return false;
}
if (is_wake_up_event(aeh))
{
mode_switch_resume();
return false;
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event);
APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);

243
src/modules/qdec_module.c Normal file
View File

@@ -0,0 +1,243 @@
#include <errno.h>
#include <zephyr/device.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/kernel.h>
#include <zephyr/pm/device.h>
#include <zephyr/sys/atomic.h>
#include <app_event_manager.h>
#include <caf/events/power_event.h>
#define MODULE qdec
#include <caf/events/module_state_event.h>
#include "qdec_step_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define QDEC_DEG_PER_STEP_EVENT 36
#define QDEC_EVENT_INTERVAL_MS 20
BUILD_ASSERT(QDEC_DEG_PER_STEP_EVENT > 0, "QDEC_DEG_PER_STEP_EVENT must be positive");
BUILD_ASSERT(QDEC_EVENT_INTERVAL_MS > 0, "QDEC_EVENT_INTERVAL_MS must be positive");
struct qdec_ctx {
const struct device *dev;
struct sensor_trigger trigger;
struct k_work_delayable emit_work;
atomic_t active;
int32_t acc_deg;
bool emit_scheduled;
};
static struct qdec_ctx qdec = {
.dev = DEVICE_DT_GET(DT_NODELABEL(qdec)),
.trigger = {
.type = SENSOR_TRIG_DATA_READY,
.chan = SENSOR_CHAN_ROTATION,
},
};
static void qdec_reset_state(void)
{
qdec.acc_deg = 0;
qdec.emit_scheduled = false;
}
static int qdec_device_set_enabled(bool enable)
{
int err = pm_device_action_run(qdec.dev,
enable ? PM_DEVICE_ACTION_RESUME :
PM_DEVICE_ACTION_SUSPEND);
if ((err == 0) || (err == -EALREADY)) {
return 0;
}
return err;
}
static void schedule_emit_work(void)
{
if (!atomic_get(&qdec.active)) {
return;
}
if (qdec.emit_scheduled) {
return;
}
qdec.emit_scheduled = true;
(void)k_work_reschedule(&qdec.emit_work, K_MSEC(QDEC_EVENT_INTERVAL_MS));
}
static void qdec_emit_work_handler(struct k_work *work)
{
int8_t step_delta;
ARG_UNUSED(work);
if (!atomic_get(&qdec.active)) {
qdec.emit_scheduled = false;
return;
}
qdec.emit_scheduled = false;
if ((qdec.acc_deg < QDEC_DEG_PER_STEP_EVENT) &&
(qdec.acc_deg > -QDEC_DEG_PER_STEP_EVENT)) {
return;
}
if (qdec.acc_deg > 0) {
step_delta = 1;
qdec.acc_deg -= QDEC_DEG_PER_STEP_EVENT;
} else {
step_delta = -1;
qdec.acc_deg += QDEC_DEG_PER_STEP_EVENT;
}
qdec_step_event_submit(step_delta);
if ((qdec.acc_deg >= QDEC_DEG_PER_STEP_EVENT) ||
(qdec.acc_deg <= -QDEC_DEG_PER_STEP_EVENT)) {
schedule_emit_work();
}
}
static void qdec_data_handler(const struct device *dev,
const struct sensor_trigger *trigger)
{
struct sensor_value rotation = {0};
int err;
ARG_UNUSED(trigger);
if (!atomic_get(&qdec.active)) {
return;
}
err = sensor_sample_fetch_chan(dev, SENSOR_CHAN_ROTATION);
if (err) {
LOG_ERR("QDEC sample fetch failed: %d", err);
return;
}
err = sensor_channel_get(dev, SENSOR_CHAN_ROTATION, &rotation);
if (err) {
LOG_ERR("QDEC channel get failed: %d", err);
return;
}
qdec.acc_deg += rotation.val1;
LOG_DBG("QDEC rotation=%d acc_deg=%d", rotation.val1, qdec.acc_deg);
if ((qdec.acc_deg >= QDEC_DEG_PER_STEP_EVENT) ||
(qdec.acc_deg <= -QDEC_DEG_PER_STEP_EVENT)) {
schedule_emit_work();
}
}
static int qdec_init(void)
{
int err;
if (!device_is_ready(qdec.dev)) {
LOG_ERR("QDEC device not ready");
return -ENODEV;
}
qdec_reset_state();
atomic_set(&qdec.active, false);
k_work_init_delayable(&qdec.emit_work, qdec_emit_work_handler);
err = sensor_trigger_set(qdec.dev, &qdec.trigger, qdec_data_handler);
if (err) {
LOG_ERR("QDEC trigger set failed: %d", err);
return err;
}
LOG_INF("QDEC initialized: %d deg/step, <=1 step per %d ms",
QDEC_DEG_PER_STEP_EVENT, QDEC_EVENT_INTERVAL_MS);
return 0;
}
static void qdec_module_suspend(void)
{
int err;
if (!atomic_get(&qdec.active)) {
return;
}
atomic_set(&qdec.active, false);
(void)k_work_cancel_delayable(&qdec.emit_work);
qdec_reset_state();
err = qdec_device_set_enabled(false);
if (err) {
LOG_WRN("QDEC suspend failed: %d", err);
}
module_set_state(MODULE_STATE_STANDBY);
}
static void qdec_module_resume(void)
{
int err;
if (atomic_get(&qdec.active)) {
return;
}
err = qdec_device_set_enabled(true);
if (err) {
LOG_ERR("QDEC resume failed: %d", err);
module_set_state(MODULE_STATE_ERROR);
return;
}
qdec_reset_state();
atomic_set(&qdec.active, true);
module_set_state(MODULE_STATE_READY);
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
const struct module_state_event *event = cast_module_state_event(aeh);
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
int err = qdec_init();
if (err) {
module_set_state(MODULE_STATE_ERROR);
} else {
qdec_module_resume();
}
}
return false;
}
if (is_power_down_event(aeh)) {
qdec_module_suspend();
return false;
}
if (is_wake_up_event(aeh)) {
qdec_module_resume();
return false;
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event);
APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);

View File

@@ -0,0 +1,374 @@
#include <errno.h>
#include <string.h>
#include <zephyr/kernel.h>
#include <zephyr/settings/settings.h>
#include <zephyr/spinlock.h>
#include <app_event_manager.h>
#define MODULE time_manager
#include <caf/events/module_state_event.h>
#include <caf/events/power_event.h>
#include "time_manager.h"
#include "time_sync_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define TIME_MANAGER_SAVE_DELAY K_HOURS(24)
#define TIME_MANAGER_STORAGE_KEY "state"
/*
* 持久化数据只记录“最近一次成功校时”的元数据:
* - 它可以帮助我们知道设备曾经被校时过;
* - 但由于当前没有独立 RTC重启后不能把这份数据当作“当前仍准确”的时间。
*/
struct time_manager_storage_data {
uint64_t utc_ms;
int16_t timezone_min;
uint32_t accuracy_ms;
uint8_t source;
uint8_t valid_marker;
};
struct time_manager_ctx {
struct k_spinlock lock;
struct k_work_delayable save_work;
struct time_manager_storage_data persisted;
uint64_t base_utc_ms;
int64_t base_uptime_ms;
int16_t timezone_min;
uint32_t accuracy_ms;
enum time_sync_source source;
bool ready;
bool synchronized;
bool has_persisted_time;
bool storage_dirty;
bool storage_loaded;
};
static struct time_manager_ctx time_ctx;
/* 统一判断来源是否合法,避免把损坏配置或“未设置来源”写进时间状态。 */
static bool time_manager_source_is_valid(enum time_sync_source source)
{
switch (source) {
case TIME_SYNC_SOURCE_BLE:
case TIME_SYNC_SOURCE_USB:
case TIME_SYNC_SOURCE_MANUAL:
case TIME_SYNC_SOURCE_HID:
return true;
default:
return false;
}
}
/* 对时区做保守校验,避免异常包把显示层带到离谱偏移。 */
static bool time_manager_timezone_is_valid(int16_t timezone_min)
{
return (timezone_min >= -(24 * 60)) && (timezone_min <= (24 * 60));
}
/*
* 保存工作在系统工作队列里执行:
* - 这样同步入口只做内存更新;
* - flash 写入被节流到 24 小时窗口,降低频繁校时带来的磨损。
*/
static int time_manager_store_state(const struct time_manager_storage_data *storage)
{
char key[] = MODULE_NAME "/" TIME_MANAGER_STORAGE_KEY;
int err = settings_save_one(key, storage, sizeof(*storage));
if (err) {
LOG_ERR("Failed to save time state err=%d", err);
return err;
}
LOG_INF("Stored time state src=%u utc_ms=%llu",
storage->source,
(unsigned long long)storage->utc_ms);
return 0;
}
/*
* 从运行态复制一份稳定快照,再在锁外执行 flash 写入:
* - 锁内只做小块 memcpy避免长时间持锁
* - 即使保存失败,也不回滚内存时间状态,避免影响当前功能。
*/
static void time_manager_save_work_fn(struct k_work *work)
{
struct time_manager_storage_data storage;
bool should_store;
k_spinlock_key_t key;
ARG_UNUSED(work);
key = k_spin_lock(&time_ctx.lock);
should_store = time_ctx.storage_dirty && time_ctx.has_persisted_time;
storage = time_ctx.persisted;
time_ctx.storage_dirty = false;
k_spin_unlock(&time_ctx.lock, key);
if (!should_store) {
return;
}
(void)time_manager_store_state(&storage);
}
/*
* 把一次同步结果写入运行态:
* - base_utc_ms + base_uptime_ms 组成当前时间基准;
* - persisted 只保存“最近一次同步结果”,后续异步落盘。
*/
static void time_manager_apply_update(const struct time_sync_update *update)
{
k_spinlock_key_t key = k_spin_lock(&time_ctx.lock);
time_ctx.base_utc_ms = update->utc_ms;
time_ctx.base_uptime_ms = k_uptime_get();
time_ctx.timezone_min = update->timezone_min;
time_ctx.accuracy_ms = update->accuracy_ms;
time_ctx.source = update->source;
time_ctx.synchronized = true;
time_ctx.has_persisted_time = true;
time_ctx.persisted.utc_ms = update->utc_ms;
time_ctx.persisted.timezone_min = update->timezone_min;
time_ctx.persisted.accuracy_ms = update->accuracy_ms;
time_ctx.persisted.source = (uint8_t)update->source;
time_ctx.persisted.valid_marker = 1U;
time_ctx.storage_dirty = true;
k_spin_unlock(&time_ctx.lock, key);
/*
* 时间同步允许立即生效,但 flash 落盘不要求同步完成后立刻发生。
* 这里将写入节流到 24 小时窗口:只要当前还没有待执行的保存工作,
* 就挂一个延迟保存;后续新的校时只更新内存快照,不反复重置定时器。
*/
if (!k_work_delayable_is_pending(&time_ctx.save_work)) {
k_work_schedule(&time_ctx.save_work, TIME_MANAGER_SAVE_DELAY);
}
LOG_INF("Time synchronized src=%u tz=%d utc_ms=%llu acc=%u",
update->source,
update->timezone_min,
(unsigned long long)update->utc_ms,
update->accuracy_ms);
}
/*
* settings 子系统回调只负责恢复原始持久化结构:
* - 不在这里决定“当前时间是否有效”;
* - 运行态初始化放到 settings_loader ready 之后统一完成。
*/
static int settings_set(const char *key,
size_t len_rd,
settings_read_cb read_cb,
void *cb_arg)
{
ssize_t rc;
if (strcmp(key, TIME_MANAGER_STORAGE_KEY) != 0) {
return 0;
}
if (len_rd != sizeof(time_ctx.persisted)) {
LOG_WRN("Time state size mismatch got=%u expect=%u",
(uint32_t)len_rd,
sizeof(time_ctx.persisted));
time_ctx.storage_loaded = false;
return 0;
}
rc = read_cb(cb_arg, &time_ctx.persisted, sizeof(time_ctx.persisted));
time_ctx.storage_loaded = (rc == sizeof(time_ctx.persisted));
if (!time_ctx.storage_loaded) {
LOG_WRN("Time state read failed rc=%d", (int)rc);
}
return 0;
}
SETTINGS_STATIC_HANDLER_DEFINE(time_manager,
MODULE_NAME,
NULL,
settings_set,
NULL,
NULL);
/*
* settings 恢复完成后初始化运行态:
* - 有持久化历史仅表示“以前同步过”,不能直接把时间标记成有效;
* - 当前 boot 仍然需要新的同步事件来建立可信时间基准。
*/
static void time_manager_init_after_settings_loaded(void)
{
k_spinlock_key_t key = k_spin_lock(&time_ctx.lock);
time_ctx.ready = true;
time_ctx.synchronized = false;
time_ctx.source = TIME_SYNC_SOURCE_NONE;
time_ctx.base_utc_ms = 0U;
time_ctx.base_uptime_ms = 0;
time_ctx.timezone_min = 0;
time_ctx.accuracy_ms = 0U;
time_ctx.has_persisted_time = time_ctx.storage_loaded &&
(time_ctx.persisted.valid_marker == 1U) &&
time_manager_source_is_valid(
(enum time_sync_source)time_ctx.persisted.source) &&
time_manager_timezone_is_valid(
time_ctx.persisted.timezone_min);
time_ctx.storage_dirty = false;
k_spin_unlock(&time_ctx.lock, key);
if (time_ctx.has_persisted_time) {
LOG_INF("Loaded persisted time metadata tz=%d src=%u utc_ms=%llu",
time_ctx.persisted.timezone_min,
time_ctx.persisted.source,
(unsigned long long)time_ctx.persisted.utc_ms);
} else {
LOG_INF("No valid persisted time metadata");
}
module_set_state(MODULE_STATE_READY);
}
/*
* 校时事件入口只做快速校验和内存更新:
* - 数据格式错误直接丢弃;
* - flash 持久化交给延迟工作处理。
*/
static bool handle_time_sync_event(const struct time_sync_event *event)
{
const struct time_sync_update *update = time_sync_event_get_update(event);
if (!time_ctx.ready) {
LOG_WRN("Drop time sync before manager ready");
return false;
}
if (!time_manager_source_is_valid(update->source)) {
LOG_WRN("Drop time sync invalid source=%u", update->source);
return false;
}
if (!time_manager_timezone_is_valid(update->timezone_min)) {
LOG_WRN("Drop time sync invalid timezone=%d", update->timezone_min);
return false;
}
if (update->utc_ms == 0U) {
LOG_WRN("Drop time sync utc_ms=0");
return false;
}
time_manager_apply_update(update);
return false;
}
/* 仅在 settings_loader 完成后宣布 READY保证外部读取到的是稳定状态。 */
static bool handle_module_state_event(const struct module_state_event *event)
{
if (!check_state(event, MODULE_ID(settings_loader), MODULE_STATE_READY)) {
return false;
}
if (time_ctx.ready) {
return false;
}
k_work_init_delayable(&time_ctx.save_work, time_manager_save_work_fn);
time_manager_init_after_settings_loaded();
return false;
}
bool time_manager_is_ready(void)
{
k_spinlock_key_t key = k_spin_lock(&time_ctx.lock);
bool ready = time_ctx.ready;
k_spin_unlock(&time_ctx.lock, key);
return ready;
}
/*
* 获取快照时只复制一次基准,再在锁外计算当前 UTC
* - 这样不会让调用者在锁里做时间换算;
* - 也避免 64 位字段在 32 位 MCU 上被撕裂读取。
*/
int time_manager_get_snapshot(struct time_manager_snapshot *snapshot)
{
uint64_t base_utc_ms;
int64_t base_uptime_ms;
int16_t timezone_min;
uint32_t accuracy_ms;
enum time_sync_source source;
bool ready;
bool synchronized;
bool has_persisted_time;
k_spinlock_key_t key;
int64_t elapsed_ms;
if (!snapshot) {
return -EINVAL;
}
key = k_spin_lock(&time_ctx.lock);
ready = time_ctx.ready;
synchronized = time_ctx.synchronized;
has_persisted_time = time_ctx.has_persisted_time;
base_utc_ms = time_ctx.base_utc_ms;
base_uptime_ms = time_ctx.base_uptime_ms;
timezone_min = time_ctx.timezone_min;
accuracy_ms = time_ctx.accuracy_ms;
source = time_ctx.source;
k_spin_unlock(&time_ctx.lock, key);
snapshot->ready = ready;
snapshot->synchronized = synchronized;
snapshot->has_persisted_time = has_persisted_time;
snapshot->timezone_min = timezone_min;
snapshot->accuracy_ms = accuracy_ms;
snapshot->source = source;
snapshot->utc_ms = 0U;
if (!ready) {
return -EAGAIN;
}
if (!synchronized) {
return -ENODATA;
}
elapsed_ms = k_uptime_get() - base_uptime_ms;
if (elapsed_ms < 0) {
elapsed_ms = 0;
}
snapshot->utc_ms = base_utc_ms + (uint64_t)elapsed_ms;
return 0;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh)) {
return handle_module_state_event(cast_module_state_event(aeh));
}
if (is_time_sync_event(aeh)) {
return handle_time_sync_event(cast_time_sync_event(aeh));
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE(MODULE, time_sync_event);

View File

@@ -0,0 +1,925 @@
#include <errno.h>
#include <string.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/usb/class/usbd_hid.h>
#include <zephyr/usb/usbd.h>
#include <app_event_manager.h>
#include <caf/events/power_event.h>
#define MODULE usb_hid
#include <caf/events/module_state_event.h>
#include "hid_report_descriptor.h"
#include "hid_boot_event.h"
#include "hid_host_command_event.h"
#include "hid_protocol_event.h"
#include "hid_tx_done_event.h"
#include "hid_tx_event.h"
#include "hid_vendor_mask_event.h"
#include "keyboard_led_event.h"
#include "mode_event.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
#define APP_USB_VID 0x1209
#define APP_USB_PID 0x0001
/*
* 模块目标:
* 1) 模块内聚控制 USB HID 栈生命周期(初始化/启用/禁用)。
* 2) 对外发布 HID 协议事件和 NumLock 指示事件。
* 3) 仅响应 mode_eventUSB/BLE/2.4G)和 power_event休眠/唤醒)。
*
* 约束:
* - 启动时只做 USB 设备初始化,不自动 enable USB 栈;
* - 只有当 mode 切到 USB 且系统非休眠时才 enable
* - BLE 逻辑保持不变,不在本模块中触碰。
*/
struct usb_hid_iface
{
const struct device *dev;
bool iface_ready;
bool in_flight;
};
enum usb_hid_stack_state
{
USB_HID_STACK_STATE_OFF,
USB_HID_STACK_STATE_READY,
USB_HID_STACK_STATE_ACTIVE,
USB_HID_STACK_STATE_ERROR,
};
struct usb_hid_policy
{
bool usb_mode_selected;
bool pm_suspended;
};
struct usb_hid_ctx
{
struct usb_hid_iface boot;
struct usb_hid_iface nkro;
struct usb_hid_iface raw;
enum usb_hid_stack_state stack_state;
struct usb_hid_policy policy;
enum hid_protocol_type current_protocol;
};
static struct usb_hid_ctx g_usb_hid = {
.current_protocol = HID_PROTO_REPORT,
};
static void submit_usb_tx_done(enum hid_tx_kind kind, bool success)
{
hid_tx_done_event_submit(kind, success);
}
USBD_DEVICE_DEFINE(new_kbd_usbd,
DEVICE_DT_GET(DT_NODELABEL(usbd)),
APP_USB_VID, APP_USB_PID);
USBD_DESC_LANG_DEFINE(new_kbd_lang);
USBD_DESC_MANUFACTURER_DEFINE(new_kbd_mfr, "new_kbd");
USBD_DESC_PRODUCT_DEFINE(new_kbd_product, "new_kbd composite HID");
USBD_DESC_CONFIG_DEFINE(new_kbd_fs_cfg_desc, "FS Configuration");
USBD_CONFIGURATION_DEFINE(new_kbd_fs_config, 0, 100, &new_kbd_fs_cfg_desc);
static const uint8_t boot_report_desc[] = HID_KEYBOARD_REPORT_DESC();
static const uint8_t nkro_report_desc[] = HID_DESC_KEYBOARD_NKRO_CONSUMER();
static const uint8_t raw_report_desc[] = HID_DESC_RAW_64();
static bool usb_hid_stack_is_active(void)
{
return g_usb_hid.stack_state == USB_HID_STACK_STATE_ACTIVE;
}
static bool usb_hid_stack_is_error(void)
{
return g_usb_hid.stack_state == USB_HID_STACK_STATE_ERROR;
}
static bool usb_hid_should_be_active(void)
{
return g_usb_hid.policy.usb_mode_selected && !g_usb_hid.policy.pm_suspended;
}
static bool usb_hid_should_handle_tx_event(const struct hid_tx_event *event)
{
switch (hid_tx_event_get_route(event)) {
case HID_TX_ROUTE_AUTO:
return g_usb_hid.policy.usb_mode_selected;
case HID_TX_ROUTE_USB:
return true;
case HID_TX_ROUTE_BLE:
default:
return false;
}
}
static struct usb_hid_iface *usb_hid_iface_from_dev(const struct device *dev)
{
if (dev == g_usb_hid.boot.dev)
{
return &g_usb_hid.boot;
}
if (dev == g_usb_hid.nkro.dev)
{
return &g_usb_hid.nkro;
}
if (dev == g_usb_hid.raw.dev)
{
return &g_usb_hid.raw;
}
return NULL;
}
static void usb_hid_clear_runtime_iface_state(void)
{
g_usb_hid.boot.iface_ready = false;
g_usb_hid.nkro.iface_ready = false;
g_usb_hid.raw.iface_ready = false;
g_usb_hid.boot.in_flight = false;
g_usb_hid.nkro.in_flight = false;
g_usb_hid.raw.in_flight = false;
}
static bool should_handle_led_input_from_dev(const struct device *dev)
{
if (g_usb_hid.current_protocol == HID_PROTO_BOOT)
return (dev == g_usb_hid.boot.dev);
return (dev == g_usb_hid.nkro.dev);
}
static bool try_extract_led_mask(const struct device *dev,
uint16_t len,
const uint8_t *buf,
uint8_t *led_mask)
{
if ((buf == NULL) || (len == 0U))
return false;
if (dev == g_usb_hid.boot.dev)
{
*led_mask = buf[0];
return true;
}
if (dev != g_usb_hid.nkro.dev)
return false;
if (len >= 2U)
{
if (buf[0] != REPORT_ID_KEYBOARD)
return false;
*led_mask = buf[1];
return true;
}
*led_mask = buf[0];
return true;
}
static bool try_extract_vendor_mask(const struct device *dev,
uint16_t len,
const uint8_t *buf,
const uint8_t **mask_data,
size_t *mask_len)
{
if ((buf == NULL) || (len < 1U))
{
return false;
}
if (dev != g_usb_hid.nkro.dev)
{
return false;
}
if (buf[0] != REPORT_ID_VENDOR)
{
return false;
}
if ((len - 1U) != HID_VENDOR_PAYLOAD_SIZE)
{
return false;
}
*mask_data = &buf[1];
*mask_len = len - 1U;
return true;
}
static bool try_extract_host_command(const struct device *dev,
uint16_t len,
const uint8_t *buf,
uint8_t *cmd,
const uint8_t **data,
size_t *data_len)
{
if ((buf == NULL) || (len < 1U)) {
return false;
}
if (dev != g_usb_hid.nkro.dev) {
return false;
}
if (buf[0] != REPORT_ID_VENDOR_CMD) {
return false;
}
if ((len - 1U) != HID_HOST_CMD_OUTPUT_PAYLOAD_SIZE) {
return false;
}
*cmd = buf[1];
*data = &buf[2];
*data_len = len - 2U;
return true;
}
static int hid_stub_get_report(const struct device *dev,
uint8_t type, uint8_t id,
uint16_t len, uint8_t *buf)
{
ARG_UNUSED(dev);
ARG_UNUSED(type);
ARG_UNUSED(id);
ARG_UNUSED(len);
ARG_UNUSED(buf);
return -ENOTSUP;
}
static int hid_stub_set_report(const struct device *dev,
uint8_t type, uint8_t id,
uint16_t len, const uint8_t *buf)
{
ARG_UNUSED(type);
ARG_UNUSED(id);
const uint8_t *mask_data;
size_t mask_len;
uint8_t cmd;
const uint8_t *cmd_data;
size_t cmd_len;
if (try_extract_vendor_mask(dev, len, buf, &mask_data, &mask_len))
{
LOG_INF("hid_stub_set_report vendor mask len=%u", mask_len);
hid_vendor_mask_event_submit(mask_data, mask_len);
return 0;
}
if (try_extract_host_command(dev, len, buf, &cmd, &cmd_data, &cmd_len))
{
LOG_INF("hid_stub_set_report vendor cmd=0x%02x len=%u", cmd, cmd_len);
hid_host_command_event_submit(HID_HOST_TRANSPORT_USB, cmd, cmd_data, cmd_len);
return 0;
}
if (!should_handle_led_input_from_dev(dev))
{
return 0;
}
uint8_t led_mask;
if (!try_extract_led_mask(dev, len, buf, &led_mask))
{
return 0;
}
LOG_INF("hid_stub_set_report led_mask=0x%02x", led_mask);
keyboard_led_event_submit(led_mask);
return 0;
}
static void hid_stub_set_idle(const struct device *dev, uint8_t id, uint32_t duration)
{
ARG_UNUSED(dev);
ARG_UNUSED(id);
ARG_UNUSED(duration);
}
static uint32_t hid_stub_get_idle(const struct device *dev, uint8_t id)
{
ARG_UNUSED(dev);
ARG_UNUSED(id);
return 0;
}
static void hid_stub_set_protocol(const struct device *dev, uint8_t proto)
{
ARG_UNUSED(dev);
enum hid_protocol_type new_protocol =
(proto == HID_PROTOCOL_BOOT) ? HID_PROTO_BOOT : HID_PROTO_REPORT;
if (g_usb_hid.current_protocol == new_protocol)
{
return;
}
g_usb_hid.current_protocol = new_protocol;
/*
* 按需求USB HID 在连接后收到 set_protocol 时上报 hid_protocol_event。
* 这里额外检查接口 ready避免在未枚举完成阶段上报无意义协议切换。
*/
if (g_usb_hid.boot.iface_ready || g_usb_hid.nkro.iface_ready)
{
hid_protocol_event_submit(new_protocol);
}
}
static void hid_stub_input_done(const struct device *dev, const uint8_t *report)
{
ARG_UNUSED(report);
/*
* 发送完成回调:
* - 仅在这里清除“在途发送”标志,确保“上一包未完成则丢弃新包”的策略可闭环;
* - 若收到未知 dev 的回调,仅记录告警,避免静默状态错乱。
*/
struct usb_hid_iface *iface = usb_hid_iface_from_dev(dev);
if (iface)
{
iface->in_flight = false;
submit_usb_tx_done((dev == g_usb_hid.boot.dev) ? HID_TX_KIND_BOOT : HID_TX_KIND_REPORT,
true);
return;
}
LOG_WRN("input_done from unknown HID dev: %p", (void *)dev);
}
static void hid_stub_output_report(const struct device *dev, uint16_t len, const uint8_t *buf)
{
const uint8_t *mask_data;
size_t mask_len;
uint8_t cmd;
const uint8_t *cmd_data;
size_t cmd_len;
if (try_extract_vendor_mask(dev, len, buf, &mask_data, &mask_len))
{
LOG_INF("hid_stub_output_report vendor mask len=%u", mask_len);
hid_vendor_mask_event_submit(mask_data, mask_len);
return;
}
if (try_extract_host_command(dev, len, buf, &cmd, &cmd_data, &cmd_len))
{
LOG_INF("hid_stub_output_report vendor cmd=0x%02x len=%u", cmd, cmd_len);
hid_host_command_event_submit(HID_HOST_TRANSPORT_USB,
cmd, cmd_data, cmd_len);
return;
}
if (!should_handle_led_input_from_dev(dev))
{
return;
}
uint8_t led_mask;
if (!try_extract_led_mask(dev, len, buf, &led_mask))
{
return;
}
LOG_INF("hid_stub_output_report led_mask=0x%02x", led_mask);
keyboard_led_event_submit(led_mask);
}
static void hid_iface_ready_cb(const struct device *dev, bool ready)
{
struct usb_hid_iface *iface = usb_hid_iface_from_dev(dev);
if (!iface)
{
return;
}
iface->iface_ready = ready;
if (!ready)
{
iface->in_flight = false;
}
}
static const struct hid_device_ops boot_hid_ops = {
.iface_ready = hid_iface_ready_cb,
.get_report = hid_stub_get_report,
.set_report = hid_stub_set_report,
.set_idle = hid_stub_set_idle,
.get_idle = hid_stub_get_idle,
.set_protocol = hid_stub_set_protocol,
.input_report_done = hid_stub_input_done,
.output_report = hid_stub_output_report,
};
static const struct hid_device_ops report_hid_ops = {
.iface_ready = hid_iface_ready_cb,
.get_report = hid_stub_get_report,
.set_report = hid_stub_set_report,
.set_idle = hid_stub_set_idle,
.get_idle = hid_stub_get_idle,
.set_protocol = hid_stub_set_protocol,
.input_report_done = hid_stub_input_done,
.output_report = hid_stub_output_report,
};
static const struct hid_device_ops raw_hid_ops = {
.iface_ready = hid_iface_ready_cb,
.get_report = hid_stub_get_report,
.set_report = hid_stub_set_report,
.set_idle = hid_stub_set_idle,
.get_idle = hid_stub_get_idle,
.input_report_done = hid_stub_input_done,
.output_report = hid_stub_output_report,
};
static void usbd_msg_cb(struct usbd_context *const usbd_ctx,
const struct usbd_msg *const msg)
{
switch (msg->type)
{
case USBD_MSG_VBUS_READY:
if (g_usb_hid.policy.pm_suspended)
{
LOG_INF("VBUS ready: submit wake_up_event");
APP_EVENT_SUBMIT(new_wake_up_event());
}
/*
* 只有在 USB 模式下才允许拉起 USB 栈。
* 这样即使插着线,只要用户切到 BLE/2.4G,也不会强制进入 USB HID。
*/
if (usbd_can_detect_vbus(usbd_ctx) && usb_hid_stack_is_active())
{
(void)usbd_enable(usbd_ctx);
}
break;
case USBD_MSG_VBUS_REMOVED:
break;
case USBD_MSG_SUSPEND:
case USBD_MSG_RESUME:
case USBD_MSG_CONFIGURATION:
break;
case USBD_MSG_UDC_ERROR:
case USBD_MSG_STACK_ERROR:
LOG_ERR("USBD stack error message: %d", msg->type);
g_usb_hid.stack_state = USB_HID_STACK_STATE_ERROR;
break;
default:
break;
}
}
static bool usb_hid_devices_ready(void)
{
if (!device_is_ready(g_usb_hid.boot.dev))
{
LOG_ERR("HID boot device is not ready");
return false;
}
if (!device_is_ready(g_usb_hid.nkro.dev))
{
LOG_ERR("HID nkro device is not ready");
return false;
}
if (!device_is_ready(g_usb_hid.raw.dev))
{
LOG_ERR("HID raw device is not ready");
return false;
}
if (!device_is_ready(DEVICE_DT_GET(DT_NODELABEL(usbd))))
{
LOG_ERR("USBD device is not ready");
return false;
}
return true;
}
static int usb_hid_register_hid_devices(void)
{
int err = hid_device_register(g_usb_hid.boot.dev,
boot_report_desc, sizeof(boot_report_desc),
&boot_hid_ops);
if (err)
{
LOG_ERR("hid_device_register(boot) failed: %d", err);
return err;
}
err = hid_device_register(g_usb_hid.nkro.dev,
nkro_report_desc, sizeof(nkro_report_desc),
&report_hid_ops);
if (err)
{
LOG_ERR("hid_device_register(nkro) failed: %d", err);
return err;
}
err = hid_device_register(g_usb_hid.raw.dev,
raw_report_desc, sizeof(raw_report_desc),
&raw_hid_ops);
if (err)
{
LOG_ERR("hid_device_register(raw) failed: %d", err);
return err;
}
return 0;
}
static int usb_hid_configure_usbd(void)
{
int err = usbd_add_descriptor(&new_kbd_usbd, &new_kbd_lang);
if (err)
{
LOG_ERR("usbd_add_descriptor(lang) failed: %d", err);
return err;
}
err = usbd_add_descriptor(&new_kbd_usbd, &new_kbd_mfr);
if (err)
{
LOG_ERR("usbd_add_descriptor(mfr) failed: %d", err);
return err;
}
err = usbd_add_descriptor(&new_kbd_usbd, &new_kbd_product);
if (err)
{
LOG_ERR("usbd_add_descriptor(product) failed: %d", err);
return err;
}
err = usbd_add_configuration(&new_kbd_usbd, USBD_SPEED_FS, &new_kbd_fs_config);
if (err)
{
LOG_ERR("usbd_add_configuration failed: %d", err);
return err;
}
err = usbd_register_all_classes(&new_kbd_usbd, USBD_SPEED_FS, 1, NULL);
if (err)
{
LOG_ERR("usbd_register_all_classes failed: %d", err);
return err;
}
return 0;
}
static int usb_hid_init_usbd_stack(void)
{
int err;
usbd_device_set_code_triple(&new_kbd_usbd, USBD_SPEED_FS, 0, 0, 0);
err = usbd_msg_register_cb(&new_kbd_usbd, usbd_msg_cb);
if (err)
{
LOG_ERR("usbd_msg_register_cb failed: %d", err);
return err;
}
err = usbd_init(&new_kbd_usbd);
if (err && (err != -EALREADY))
{
LOG_ERR("usbd_init failed: %d", err);
return err;
}
return 0;
}
static int usb_hid_stack_init(void)
{
if (g_usb_hid.stack_state != USB_HID_STACK_STATE_OFF)
{
return 0;
}
g_usb_hid.boot.dev = DEVICE_DT_GET(DT_NODELABEL(hid_dev_0));
g_usb_hid.nkro.dev = DEVICE_DT_GET(DT_NODELABEL(hid_dev_1));
g_usb_hid.raw.dev = DEVICE_DT_GET(DT_NODELABEL(raw_hid));
if (!usb_hid_devices_ready())
{
return -ENODEV;
}
int err = usb_hid_register_hid_devices();
if (err)
{
return err;
}
err = usb_hid_configure_usbd();
if (err)
{
return err;
}
err = usb_hid_init_usbd_stack();
if (err)
{
return err;
}
g_usb_hid.stack_state = USB_HID_STACK_STATE_READY;
return 0;
}
static int usb_hid_set_enabled(bool enable)
{
int err;
if (usb_hid_stack_is_error())
{
return -EIO;
}
if (g_usb_hid.stack_state == USB_HID_STACK_STATE_OFF)
{
err = usb_hid_stack_init();
if (err)
{
return err;
}
}
if (enable && usb_hid_stack_is_active())
{
return 0;
}
if (!enable && (g_usb_hid.stack_state == USB_HID_STACK_STATE_READY))
{
return 0;
}
if (enable)
{
err = usbd_enable(&new_kbd_usbd);
}
else
{
err = usbd_disable(&new_kbd_usbd);
usb_hid_clear_runtime_iface_state();
}
if (err && (err != -EALREADY))
{
LOG_ERR("usbd_%s failed: %d", enable ? "enable" : "disable", err);
g_usb_hid.stack_state = USB_HID_STACK_STATE_ERROR;
return err;
}
g_usb_hid.stack_state = enable ? USB_HID_STACK_STATE_ACTIVE : USB_HID_STACK_STATE_READY;
return 0;
}
static void refresh_usb_state_by_policy(void)
{
/*
* 控制策略:
* - USB 模式 + 非休眠:启用 USB HID。
* - 其他情况:关闭 USB HID不销毁初始化结果后续可快速恢复
*/
bool should_enable = usb_hid_should_be_active();
int err = usb_hid_set_enabled(should_enable);
if (err)
{
LOG_ERR("usb_hid_set_enabled(%d) failed: %d", should_enable, err);
}
}
static bool handle_module_state_event(const struct module_state_event *event)
{
if (!check_state(event, MODULE_ID(main), MODULE_STATE_READY))
{
return false;
}
int err = usb_hid_stack_init();
if (err)
{
LOG_ERR("USB HID stack init failed: %d", err);
g_usb_hid.stack_state = USB_HID_STACK_STATE_ERROR;
module_set_state(MODULE_STATE_ERROR);
return false;
}
module_set_state(MODULE_STATE_READY);
return false;
}
static bool handle_mode_event(const struct mode_event *event)
{
g_usb_hid.policy.usb_mode_selected = (event->mode_type == MODE_TYPE_USB);
refresh_usb_state_by_policy();
return false;
}
static bool handle_power_down_event(void)
{
if (g_usb_hid.policy.pm_suspended)
{
/* 避免重复上报 STANDBY 导致 power_manager 在 SUSPENDING 期间反复迭代。 */
return false;
}
g_usb_hid.policy.pm_suspended = true;
refresh_usb_state_by_policy();
module_set_state(MODULE_STATE_STANDBY);
return false;
}
static bool handle_wake_up_event(void)
{
if (!g_usb_hid.policy.pm_suspended)
{
return false;
}
g_usb_hid.policy.pm_suspended = false;
refresh_usb_state_by_policy();
module_set_state(MODULE_STATE_READY);
return false;
}
static bool handle_hid_tx_event(const struct hid_tx_event *event)
{
if (!usb_hid_should_handle_tx_event(event))
{
return false;
}
if (!usb_hid_stack_is_active())
{
submit_usb_tx_done(event->kind, false);
return false;
}
if (event->kind == HID_TX_KIND_BOOT)
{
const uint8_t *payload = hid_tx_event_get_data(event);
size_t payload_len = hid_tx_event_get_size(event);
int err;
if (g_usb_hid.current_protocol != HID_PROTO_BOOT)
{
submit_usb_tx_done(HID_TX_KIND_BOOT, false);
return false;
}
if (!g_usb_hid.boot.iface_ready || !g_usb_hid.boot.dev)
{
submit_usb_tx_done(HID_TX_KIND_BOOT, false);
return false;
}
if (g_usb_hid.boot.in_flight)
{
LOG_WRN("Drop boot tx: previous report not sent");
submit_usb_tx_done(HID_TX_KIND_BOOT, false);
return false;
}
err = hid_device_submit_report(g_usb_hid.boot.dev,
payload_len,
payload);
if (err)
{
LOG_WRN("USB boot report send failed err=%d", err);
submit_usb_tx_done(HID_TX_KIND_BOOT, false);
}
else
{
g_usb_hid.boot.in_flight = true;
}
return false;
}
/*
* USB 侧仅在 active 条件满足时发送:
* - 当前 mode 为 USB
* - USB HID 栈已启用且对应接口 ready。
*/
if (g_usb_hid.current_protocol != HID_PROTO_REPORT)
{
submit_usb_tx_done(HID_TX_KIND_REPORT, false);
return false;
}
const uint8_t *data = hid_tx_event_get_data(event);
size_t data_len = hid_tx_event_get_size(event);
uint8_t report_id;
if (data_len < 1U)
{
submit_usb_tx_done(HID_TX_KIND_REPORT, false);
return false;
}
report_id = data[0];
if (!g_usb_hid.nkro.iface_ready || !g_usb_hid.nkro.dev)
{
submit_usb_tx_done(HID_TX_KIND_REPORT, false);
return false;
}
if ((report_id != REPORT_ID_KEYBOARD) &&
(report_id != REPORT_ID_CONSUMER) &&
(report_id != REPORT_ID_VENDOR) &&
(report_id != REPORT_ID_VENDOR_CMD))
{
submit_usb_tx_done(HID_TX_KIND_REPORT, false);
return false;
}
if (g_usb_hid.nkro.in_flight)
{
LOG_WRN("Drop tx report id=0x%02x: previous report not sent", report_id);
submit_usb_tx_done(HID_TX_KIND_REPORT, false);
return false;
}
/* Report 协议下 dyndata 是 [report_id|payload],可直接透传。 */
int err = hid_device_submit_report(g_usb_hid.nkro.dev, data_len, data);
if (err)
{
LOG_WRN("USB report send failed id=0x%02x err=%d", report_id, err);
submit_usb_tx_done(HID_TX_KIND_REPORT, false);
}
else
{
g_usb_hid.nkro.in_flight = true;
}
return false;
}
static bool app_event_handler(const struct app_event_header *aeh)
{
if (is_module_state_event(aeh))
{
return handle_module_state_event(cast_module_state_event(aeh));
}
if (is_mode_event(aeh))
{
return handle_mode_event(cast_mode_event(aeh));
}
if (is_power_down_event(aeh))
{
return handle_power_down_event();
}
if (is_wake_up_event(aeh))
{
return handle_wake_up_event();
}
if (is_hid_tx_event(aeh))
{
return handle_hid_tx_event(cast_hid_tx_event(aeh));
}
__ASSERT_NO_MSG(false);
return false;
}
APP_EVENT_LISTENER(MODULE, app_event_handler);
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
APP_EVENT_SUBSCRIBE(MODULE, mode_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event);
APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);
APP_EVENT_SUBSCRIBE_EARLY(MODULE, hid_tx_event);

306
src/ui/display_ui.c Normal file
View File

@@ -0,0 +1,306 @@
#include <string.h>
#include <lvgl.h>
#include <zephyr/sys/printk.h>
#include "battery_status_event.h"
#include "display_ui.h"
#include "keyboard_led_event.h"
#define DISPLAY_SYMBOL_PLUG "\xEF\x87\xA6"
LV_FONT_DECLARE(ui_font_keyboard_small_18);
LV_FONT_DECLARE(ui_font_keyboard_time_48);
enum display_status_id
{
DISPLAY_STATUS_USB = 0,
DISPLAY_STATUS_BLE,
DISPLAY_STATUS_NUMLOCK,
DISPLAY_STATUS_CAPSLOCK,
DISPLAY_STATUS_COUNT,
};
struct display_ui_ctx
{
lv_obj_t *status_badges[DISPLAY_STATUS_COUNT];
lv_obj_t *status_labels[DISPLAY_STATUS_COUNT];
lv_obj_t *battery_icon;
lv_obj_t *battery_label;
lv_obj_t *battery_state_label;
lv_obj_t *date_label;
lv_obj_t *time_label;
};
static struct display_ui_ctx g_display_ui;
static const char *const g_status_texts[DISPLAY_STATUS_COUNT] = {
LV_SYMBOL_USB,
LV_SYMBOL_BLUETOOTH,
"1",
"A",
};
static lv_color_t display_ui_get_battery_color(uint8_t battery_level)
{
if (battery_level > 70U) {
return lv_color_hex(0x8BD450);
}
if (battery_level >= 20U) {
return lv_color_hex(0xF4D35E);
}
return lv_color_hex(0xE63946);
}
static const char *display_ui_get_battery_symbol(uint8_t battery_level)
{
if (battery_level > 85U) {
return LV_SYMBOL_BATTERY_FULL;
}
if (battery_level > 60U) {
return LV_SYMBOL_BATTERY_3;
}
if (battery_level > 35U) {
return LV_SYMBOL_BATTERY_2;
}
if (battery_level >= 20U) {
return LV_SYMBOL_BATTERY_1;
}
return LV_SYMBOL_BATTERY_EMPTY;
}
static bool display_ui_status_is_active(enum display_status_id id,
const struct display_ui_model *model)
{
switch (id) {
case DISPLAY_STATUS_USB:
return model->mode == MODE_TYPE_USB;
case DISPLAY_STATUS_BLE:
return model->mode == MODE_TYPE_BLE;
case DISPLAY_STATUS_NUMLOCK:
return (model->led_mask & KEYBOARD_LED_MASK_NUM_LOCK) != 0U;
case DISPLAY_STATUS_CAPSLOCK:
return (model->led_mask & KEYBOARD_LED_MASK_CAPS_LOCK) != 0U;
default:
return false;
}
}
static void display_ui_create_status_chip(lv_obj_t *parent, enum display_status_id id)
{
lv_obj_t *badge = lv_obj_create(parent);
lv_obj_t *label;
lv_obj_remove_style_all(badge);
lv_obj_set_size(badge, 50, 32);
lv_obj_set_style_radius(badge, 10, 0);
lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0);
lv_obj_set_style_pad_all(badge, 0, 0);
label = lv_label_create(badge);
lv_label_set_text(label, g_status_texts[id]);
lv_obj_set_width(label, LV_PCT(100));
lv_obj_set_style_text_font(label, &ui_font_keyboard_small_18, 0);
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_center(label);
g_display_ui.status_badges[id] = badge;
g_display_ui.status_labels[id] = label;
}
void display_ui_refresh_status_bar(const struct display_ui_model *model)
{
for (uint32_t i = 0; i < DISPLAY_STATUS_COUNT; i++) {
lv_obj_t *badge = g_display_ui.status_badges[i];
lv_obj_t *label = g_display_ui.status_labels[i];
bool active = display_ui_status_is_active((enum display_status_id)i, model);
if (!badge || !label) {
continue;
}
lv_obj_set_style_border_width(badge, 4, 0);
lv_obj_set_style_border_color(badge,
active ? model->theme_color :
model->inactive_border_color,
0);
lv_obj_set_style_bg_color(badge,
active ? lv_color_hex(0x1D2735) :
lv_color_hex(0x161A20),
0);
lv_obj_set_style_text_color(label,
active ? lv_color_white() :
lv_color_hex(0x7C8798),
0);
}
}
void display_ui_refresh_battery(const struct display_ui_model *model)
{
char battery_text[8];
lv_color_t battery_color;
const char *state_symbol = "";
lv_color_t state_color = lv_color_white();
if (!g_display_ui.battery_icon ||
!g_display_ui.battery_label ||
!g_display_ui.battery_state_label) {
return;
}
battery_color = display_ui_get_battery_color(model->battery_level);
snprintk(battery_text, sizeof(battery_text), "%u%%", model->battery_level);
if ((model->battery_flags & BATTERY_STATUS_FLAG_FULL) != 0U) {
state_symbol = DISPLAY_SYMBOL_PLUG;
state_color = lv_color_hex(0x4C9EF5);
} else if ((model->battery_flags & BATTERY_STATUS_FLAG_CHARGING) != 0U) {
state_symbol = LV_SYMBOL_CHARGE;
state_color = lv_color_hex(0xF4D35E);
}
lv_label_set_text(g_display_ui.battery_icon,
display_ui_get_battery_symbol(model->battery_level));
lv_obj_set_style_text_color(g_display_ui.battery_icon, battery_color, 0);
lv_label_set_text(g_display_ui.battery_label, battery_text);
lv_label_set_text(g_display_ui.battery_state_label, state_symbol);
lv_obj_set_style_text_color(g_display_ui.battery_state_label, state_color, 0);
}
void display_ui_refresh_datetime(const char *date_text, const char *time_text)
{
if (!g_display_ui.date_label || !g_display_ui.time_label) {
return;
}
lv_label_set_text(g_display_ui.date_label, date_text);
lv_label_set_text(g_display_ui.time_label, time_text);
}
void display_ui_refresh_all(const struct display_ui_model *model,
const char *date_text,
const char *time_text)
{
display_ui_refresh_status_bar(model);
display_ui_refresh_battery(model);
display_ui_refresh_datetime(date_text, time_text);
}
void display_ui_init(const struct display_ui_model *model,
const char *date_text,
const char *time_text)
{
lv_obj_t *screen = lv_screen_active();
lv_obj_t *content;
lv_obj_t *top_row;
lv_obj_t *battery_wrap;
lv_obj_t *middle_row;
lv_obj_t *bottom_row;
memset(&g_display_ui, 0, sizeof(g_display_ui));
lv_obj_clean(screen);
lv_obj_set_style_bg_color(screen, lv_color_hex(0x0F1115), 0);
lv_obj_set_style_bg_grad_color(screen, lv_color_hex(0x1A1F29), 0);
lv_obj_set_style_bg_grad_dir(screen, LV_GRAD_DIR_VER, 0);
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
lv_obj_set_style_text_color(screen, lv_color_white(), 0);
lv_obj_set_style_pad_all(screen, 0, 0);
lv_obj_set_scrollbar_mode(screen, LV_SCROLLBAR_MODE_OFF);
content = lv_obj_create(screen);
lv_obj_remove_style_all(content);
lv_obj_set_size(content, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(content, lv_color_hex(0x0F1115), 0);
lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, 0);
lv_obj_set_style_pad_left(content, 14, 0);
lv_obj_set_style_pad_right(content, 14, 0);
lv_obj_set_style_pad_top(content, 8, 0);
lv_obj_set_style_pad_bottom(content, 8, 0);
lv_obj_set_layout(content, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(content,
LV_FLEX_ALIGN_START,
LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
top_row = lv_obj_create(content);
lv_obj_remove_style_all(top_row);
lv_obj_set_width(top_row, LV_PCT(100));
lv_obj_set_flex_grow(top_row, 1);
lv_obj_set_style_bg_color(top_row, lv_color_hex(0x0F1115), 0);
lv_obj_set_style_bg_opa(top_row, LV_OPA_TRANSP, 0);
lv_obj_set_layout(top_row, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(top_row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(top_row,
LV_FLEX_ALIGN_SPACE_BETWEEN,
LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
g_display_ui.date_label = lv_label_create(top_row);
lv_obj_set_style_text_font(g_display_ui.date_label, &ui_font_keyboard_small_18, 0);
lv_obj_set_style_text_color(g_display_ui.date_label, lv_color_hex(0xD8DEE9), 0);
battery_wrap = lv_obj_create(top_row);
lv_obj_remove_style_all(battery_wrap);
lv_obj_set_width(battery_wrap, LV_SIZE_CONTENT);
lv_obj_set_layout(battery_wrap, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(battery_wrap, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(battery_wrap,
LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(battery_wrap, 4, 0);
g_display_ui.battery_icon = lv_label_create(battery_wrap);
lv_obj_set_style_text_font(g_display_ui.battery_icon, &ui_font_keyboard_small_18, 0);
g_display_ui.battery_label = lv_label_create(battery_wrap);
lv_obj_set_style_text_font(g_display_ui.battery_label, &ui_font_keyboard_small_18, 0);
lv_obj_set_style_text_color(g_display_ui.battery_label, lv_color_hex(0xD8DEE9), 0);
g_display_ui.battery_state_label = lv_label_create(battery_wrap);
lv_obj_set_style_text_font(g_display_ui.battery_state_label, &ui_font_keyboard_small_18, 0);
middle_row = lv_obj_create(content);
lv_obj_remove_style_all(middle_row);
lv_obj_set_width(middle_row, LV_PCT(100));
lv_obj_set_flex_grow(middle_row, 2);
lv_obj_set_style_bg_color(middle_row, lv_color_hex(0x0F1115), 0);
lv_obj_set_style_bg_opa(middle_row, LV_OPA_TRANSP, 0);
g_display_ui.time_label = lv_label_create(middle_row);
lv_obj_set_style_text_font(g_display_ui.time_label, &ui_font_keyboard_time_48, 0);
lv_obj_set_style_text_color(g_display_ui.time_label, lv_color_white(), 0);
lv_obj_center(g_display_ui.time_label);
bottom_row = lv_obj_create(content);
lv_obj_remove_style_all(bottom_row);
lv_obj_set_width(bottom_row, LV_PCT(100));
lv_obj_set_flex_grow(bottom_row, 1);
lv_obj_set_style_bg_color(bottom_row, lv_color_hex(0x0F1115), 0);
lv_obj_set_style_bg_opa(bottom_row, LV_OPA_TRANSP, 0);
lv_obj_set_layout(bottom_row, LV_LAYOUT_FLEX);
lv_obj_set_flex_flow(bottom_row, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(bottom_row,
LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER,
LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_column(bottom_row, 6, 0);
for (uint32_t i = 0; i < DISPLAY_STATUS_COUNT; i++) {
display_ui_create_status_chip(bottom_row, (enum display_status_id)i);
}
display_ui_refresh_all(model, date_text, time_text);
}

34
src/ui/display_ui.h Normal file
View File

@@ -0,0 +1,34 @@
#ifndef DISPLAY_UI_H
#define DISPLAY_UI_H
#include <stdint.h>
#include <lvgl.h>
#include "mode_event.h"
struct display_ui_model
{
lv_color_t theme_color;
lv_color_t inactive_border_color;
uint8_t battery_level;
mode_type_t mode;
uint8_t led_mask;
uint8_t battery_flags;
};
void display_ui_init(const struct display_ui_model *model,
const char *date_text,
const char *time_text);
void display_ui_refresh_all(const struct display_ui_model *model,
const char *date_text,
const char *time_text);
void display_ui_refresh_status_bar(const struct display_ui_model *model);
void display_ui_refresh_battery(const struct display_ui_model *model);
void display_ui_refresh_datetime(const char *date_text, const char *time_text);
#endif /* DISPLAY_UI_H */

Some files were not shown because too many files have changed in this diff Show More