diff --git a/CMakeLists.txt b/CMakeLists.txt index c47f646..ce5f22a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,7 +27,11 @@ 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 @@ -41,9 +45,9 @@ target_sources(app PRIVATE src/modules/ble_adv_ctrl_module.c src/modules/ble_battery_module.c src/modules/ble_bond_module.c - src/modules/ble_time_sync_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 diff --git a/docs/ble_time_sync_pc_host.md b/docs/ble_time_sync_pc_host.md new file mode 100644 index 0000000..947d694 --- /dev/null +++ b/docs/ble_time_sync_pc_host.md @@ -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(" 设备 单向校时 + +当前不支持: + +- BLE 读回当前设备时间 +- 校时结果通知 +- 历史同步记录查询 +- DST 单独字段 +- 通过 payload 指定同步来源 + +同步来源在设备端固定记为: + +- `TIME_SYNC_SOURCE_BLE` + +## 11. 对接建议 + +对 PC 上位机实现建议如下: + +- 首选带响应写入,不要默认用 write without response +- 时间戳统一使用 UTC 毫秒 +- 时区单独使用分钟偏移 +- 每次建立加密连接后可主动同步一次时间 +- 若 PC 有系统授时状态,可把估计精度填入 `accuracy_ms` + diff --git a/inc/hid_host_command_protocol.h b/inc/hid_host_command_protocol.h new file mode 100644 index 0000000..882c7d6 --- /dev/null +++ b/inc/hid_host_command_protocol.h @@ -0,0 +1,16 @@ +#ifndef HID_HOST_COMMAND_PROTOCOL_H__ +#define HID_HOST_COMMAND_PROTOCOL_H__ + +#include + +#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__ */ diff --git a/inc/hid_host_transport.h b/inc/hid_host_transport.h new file mode 100644 index 0000000..2dca093 --- /dev/null +++ b/inc/hid_host_transport.h @@ -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__ */ diff --git a/inc/hid_report_descriptor.h b/inc/hid_report_descriptor.h index 3ffe8e1..4d9b109 100644 --- a/inc/hid_report_descriptor.h +++ b/inc/hid_report_descriptor.h @@ -3,11 +3,14 @@ #include +#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 @@ -18,6 +21,7 @@ enum { #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)) @@ -33,9 +37,10 @@ enum { * 键盘(NKRO) + Consumer + Vendor 的复合 Report 描述符: * - USB Report 接口和 BLE HIDS Report Map 统一使用这份定义, * 避免两边手写常量后长期演进出现不一致。 - * - Vendor Report 复用与 NKRO 键盘状态相同的 payload 结构: - * [modifier(1B) | usage_bitmap(29B)]。 - * 这样主机可以下发“屏蔽遮罩”,设备也可以上报“真实键盘状态”。 + * - 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() \ { \ @@ -105,6 +110,22 @@ enum { 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, \ } diff --git a/inc/time_manager.h b/inc/time_manager.h index c4d181c..0be0877 100644 --- a/inc/time_manager.h +++ b/inc/time_manager.h @@ -18,6 +18,7 @@ enum time_sync_source { TIME_SYNC_SOURCE_BLE, TIME_SYNC_SOURCE_USB, TIME_SYNC_SOURCE_MANUAL, + TIME_SYNC_SOURCE_HID, }; /* diff --git a/prj.conf b/prj.conf index f0ce0ec..dccd133 100644 --- a/prj.conf +++ b/prj.conf @@ -52,8 +52,8 @@ 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=3 -CONFIG_BT_HIDS_OUTPUT_REP_MAX=2 +CONFIG_BT_HIDS_INPUT_REP_MAX=4 +CONFIG_BT_HIDS_OUTPUT_REP_MAX=3 CONFIG_BT_HIDS_FEATURE_REP_MAX=0 CONFIG_USB_DEVICE_STACK_NEXT=y diff --git a/src/events/display_theme_event.c b/src/events/display_theme_event.c new file mode 100644 index 0000000..6c735ff --- /dev/null +++ b/src/events/display_theme_event.c @@ -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)); diff --git a/src/events/display_theme_event.h b/src/events/display_theme_event.h new file mode 100644 index 0000000..98c251d --- /dev/null +++ b/src/events/display_theme_event.h @@ -0,0 +1,30 @@ +#ifndef DISPLAY_THEME_EVENT_H__ +#define DISPLAY_THEME_EVENT_H__ + +#include + +#include +#include + +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__ */ diff --git a/src/events/hid_host_ack_event.c b/src/events/hid_host_ack_event.c new file mode 100644 index 0000000..8447c00 --- /dev/null +++ b/src/events/hid_host_ack_event.c @@ -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)); diff --git a/src/events/hid_host_ack_event.h b/src/events/hid_host_ack_event.h new file mode 100644 index 0000000..de0166d --- /dev/null +++ b/src/events/hid_host_ack_event.h @@ -0,0 +1,29 @@ +#ifndef HID_HOST_ACK_EVENT_H__ +#define HID_HOST_ACK_EVENT_H__ + +#include + +#include +#include + +#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__ */ diff --git a/src/events/hid_host_command_error_event.c b/src/events/hid_host_command_error_event.c new file mode 100644 index 0000000..64d2350 --- /dev/null +++ b/src/events/hid_host_command_error_event.c @@ -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)); diff --git a/src/events/hid_host_command_error_event.h b/src/events/hid_host_command_error_event.h new file mode 100644 index 0000000..6fb7b23 --- /dev/null +++ b/src/events/hid_host_command_error_event.h @@ -0,0 +1,41 @@ +#ifndef HID_HOST_COMMAND_ERROR_EVENT_H__ +#define HID_HOST_COMMAND_ERROR_EVENT_H__ + +#include + +#include +#include + +#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__ */ diff --git a/src/events/hid_host_command_event.c b/src/events/hid_host_command_event.c new file mode 100644 index 0000000..95e9ee3 --- /dev/null +++ b/src/events/hid_host_command_event.c @@ -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)); diff --git a/src/events/hid_host_command_event.h b/src/events/hid_host_command_event.h new file mode 100644 index 0000000..6d7f20a --- /dev/null +++ b/src/events/hid_host_command_event.h @@ -0,0 +1,44 @@ +#ifndef HID_HOST_COMMAND_EVENT_H__ +#define HID_HOST_COMMAND_EVENT_H__ + +#include +#include + +#include +#include +#include + +#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__ */ diff --git a/src/events/hid_tx_event.c b/src/events/hid_tx_event.c index 36f81e7..917b60b 100644 --- a/src/events/hid_tx_event.c +++ b/src/events/hid_tx_event.c @@ -11,8 +11,12 @@ static void log_hid_tx_event(const struct app_event_header *aeh) payload_len = event->dyndata.size - 1U; } - APP_EVENT_MANAGER_LOG(aeh, "kind=%u report_id=0x%02x payload_len=%u", - event->kind, report_id, payload_len); + 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, @@ -26,15 +30,17 @@ static void profile_hid_tx_event(struct log_event_buf *buf, } 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", "report_id", "len"), + ENCODE("kind", "route", "report_id", "len"), profile_hid_tx_event); APP_EVENT_TYPE_DEFINE(hid_tx_event, diff --git a/src/events/hid_tx_event.h b/src/events/hid_tx_event.h index 51e18ce..5ec1fdb 100644 --- a/src/events/hid_tx_event.h +++ b/src/events/hid_tx_event.h @@ -13,21 +13,30 @@ enum hid_tx_kind { 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(enum hid_tx_kind kind, - const uint8_t *data, - size_t size) +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); } @@ -35,6 +44,13 @@ static inline void hid_tx_event_submit(enum hid_tx_kind kind, 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; @@ -45,4 +61,9 @@ 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__ */ diff --git a/src/events/time_sync_event.c b/src/events/time_sync_event.c index a1af66b..6477e21 100644 --- a/src/events/time_sync_event.c +++ b/src/events/time_sync_event.c @@ -12,6 +12,8 @@ static const char *time_sync_source_name(enum time_sync_source source) return "usb"; case TIME_SYNC_SOURCE_MANUAL: return "manual"; + case TIME_SYNC_SOURCE_HID: + return "hid"; default: return "unknown"; } diff --git a/src/modules/battery_module.c b/src/modules/battery_module.c index a689e7f..a615fa3 100644 --- a/src/modules/battery_module.c +++ b/src/modules/battery_module.c @@ -230,7 +230,8 @@ static void battery_sampling_set_enabled(bool enable) if (enable) { - k_work_reschedule(&battery.sample_work, K_NO_WAIT); + // 延迟2s开始采样等待电池电压稳定 + k_work_reschedule(&battery.sample_work, K_MSEC(2000)); } else { diff --git a/src/modules/ble_hid_module.c b/src/modules/ble_hid_module.c index ac6b284..9281470 100644 --- a/src/modules/ble_hid_module.c +++ b/src/modules/ble_hid_module.c @@ -8,6 +8,7 @@ #include #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" @@ -18,8 +19,8 @@ #include LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); -#define INPUT_REPORT_COUNT 3 -#define OUTPUT_REPORT_COUNT 2 +#define INPUT_REPORT_COUNT 4 +#define OUTPUT_REPORT_COUNT 3 BT_HIDS_DEF(hids_obj, INPUT_REPORT_COUNT, OUTPUT_REPORT_COUNT, 0); @@ -52,6 +53,19 @@ 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; @@ -156,6 +170,25 @@ static void vendor_output_report_handler(struct bt_hids_rep *rep, 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(); @@ -182,6 +215,10 @@ static int hids_service_init(void) 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; @@ -190,6 +227,10 @@ static int hids_service_init(void) 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; @@ -227,7 +268,7 @@ static void handle_ble_peer_event(const struct ble_peer_event *event) static bool handle_hid_tx_event(const struct hid_tx_event *event) { - if (!ble_hid.policy.ble_mode_selected) { + if (!ble_hid_should_handle_tx_event(event)) { return false; } @@ -291,6 +332,8 @@ static bool handle_hid_tx_event(const struct hid_tx_event *event) 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; diff --git a/src/modules/display_module.c b/src/modules/display_module.c index 99d3325..4005ec3 100644 --- a/src/modules/display_module.c +++ b/src/modules/display_module.c @@ -1,4 +1,5 @@ #include +#include #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include @@ -18,6 +20,7 @@ #include #include "battery_status_event.h" +#include "display_theme_event.h" #include "keyboard_led_event.h" #include "mode_event.h" #include "time_manager.h" @@ -28,6 +31,8 @@ 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 @@ -78,10 +83,20 @@ struct display_ctx 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_state ui; uint32_t tick_count; enum display_pm_state pm_state; bool initialized; + bool theme_storage_dirty; + bool theme_storage_loaded; +}; + +struct display_theme_storage { + uint8_t red; + uint8_t green; + uint8_t blue; + uint8_t valid_marker; }; static struct display_ctx disp = { @@ -95,6 +110,8 @@ static struct display_ctx disp = { .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)); @@ -107,6 +124,94 @@ static const char *const g_status_texts[DISPLAY_STATUS_COUNT] = { static void display_refresh_all_locked(void); +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 @@ -166,6 +271,12 @@ static void display_sleep(void) (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) @@ -553,7 +664,9 @@ static int display_init(void) 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(); err = display_blanking_off(disp.dev); if (err) @@ -631,6 +744,20 @@ static bool handle_keyboard_led_event(const struct keyboard_led_event *event) 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_refresh_status_bar_locked(); + lvgl_unlock(); + return false; +} + /* 任意按钮事件都可点亮屏幕并重置 1 分钟空闲计时。 */ static bool handle_button_event(const struct button_event *event) { @@ -654,6 +781,18 @@ static bool handle_wake_up_event(void) 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_refresh_status_bar_locked(); + lvgl_unlock(); + } + + return false; + } + if (!check_state(event, MODULE_ID(main), MODULE_STATE_READY)) return false; @@ -683,6 +822,9 @@ static bool app_event_handler(const struct app_event_header *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)); @@ -699,6 +841,7 @@ static bool app_event_handler(const struct app_event_header *aeh) 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); diff --git a/src/modules/hid_host_command_module.c b/src/modules/hid_host_command_module.c new file mode 100644 index 0000000..0b447cd --- /dev/null +++ b/src/modules/hid_host_command_module.c @@ -0,0 +1,121 @@ +#include + +#include + +#define MODULE hid_host_command +#include + +#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 +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); diff --git a/src/modules/hid_tx_manager_module.c b/src/modules/hid_tx_manager_module.c index 61e1071..d1f5ed1 100644 --- a/src/modules/hid_tx_manager_module.c +++ b/src/modules/hid_tx_manager_module.c @@ -10,6 +10,7 @@ #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" @@ -26,14 +27,15 @@ enum hid_tx_flag { HID_TX_FLAG_IN_FLIGHT, HID_TX_FLAG_BOOT_VALID, HID_TX_FLAG_BOOT_DIRTY, - HID_TX_FLAG_NKRO_VALID, - HID_TX_FLAG_NKRO_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]; }; @@ -41,7 +43,7 @@ struct hid_tx_item { struct hid_tx_ctx { atomic_t flags; struct hid_tx_item boot_state; - struct hid_tx_item nkro_state; + struct hid_tx_item keyboard_state; struct hid_tx_item vendor_state; struct hid_tx_item inflight_item; mode_type_t active_mode; @@ -51,10 +53,13 @@ static struct hid_tx_ctx tx = { .active_mode = MODE_TYPE_COUNT, }; -K_MSGQ_DEFINE(hid_tx_queue_msgq, sizeof(struct hid_tx_item), HID_TX_QUEUE_SIZE, 4); +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) { @@ -64,6 +69,7 @@ static bool hid_tx_item_store(struct hid_tx_item *item, } item->kind = kind; + item->route = route; item->len = len; if ((len > 0U) && (data != NULL)) { memcpy(item->data, data, len); @@ -72,15 +78,19 @@ static bool hid_tx_item_store(struct hid_tx_item *item, return true; } -static bool hid_tx_queue_push(enum hid_tx_kind kind, const uint8_t *data, size_t len) +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, data, len)) { + if (!hid_tx_item_store(&item, kind, route, data, len)) { return false; } - if (k_msgq_put(&hid_tx_queue_msgq, &item, K_NO_WAIT)) { + if (k_msgq_put(queue, &item, K_NO_WAIT)) { LOG_WRN("Drop HID tx kind=%u len=%u: queue full", kind, len); return false; } @@ -88,11 +98,16 @@ static bool hid_tx_queue_push(enum hid_tx_kind kind, const uint8_t *data, size_t 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(item->kind, item->data, item->len); + hid_tx_event_submit_routed(item->kind, item->route, item->data, item->len); return true; } @@ -105,33 +120,44 @@ static void dispatch_next_if_possible(void) return; } - if ((tx.active_mode != MODE_TYPE_USB) && (tx.active_mode != MODE_TYPE_BLE)) { + 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 (atomic_test_bit(&tx.flags, HID_TX_FLAG_NKRO_DIRTY) && - atomic_test_bit(&tx.flags, HID_TX_FLAG_NKRO_VALID)) { - atomic_clear_bit(&tx.flags, HID_TX_FLAG_NKRO_DIRTY); - (void)hid_tx_dispatch_item(&tx.nkro_state); - return; - } - - if (atomic_test_bit(&tx.flags, HID_TX_FLAG_BOOT_DIRTY) && + 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 (!k_msgq_get(&hid_tx_queue_msgq, &item, K_NO_WAIT)) { + 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 (atomic_test_bit(&tx.flags, HID_TX_FLAG_VENDOR_DIRTY) && + 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); } } @@ -156,10 +182,23 @@ static bool handle_mode_event(const struct mode_event *event) 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); @@ -174,21 +213,51 @@ static bool handle_hid_report_request_event(const struct hid_report_event *event size_t len = hid_report_event_get_size(event); if ((len > 0U) && (data[0] == REPORT_ID_KEYBOARD)) { - (void)hid_tx_item_store(&tx.nkro_state, HID_TX_KIND_REPORT, data, len); - atomic_set_bit(&tx.flags, HID_TX_FLAG_NKRO_VALID); - atomic_set_bit(&tx.flags, HID_TX_FLAG_NKRO_DIRTY); + (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, data, len); + (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_KIND_REPORT, data, len); + (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)) { @@ -227,6 +296,10 @@ static bool app_event_handler(const struct app_event_header *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; } @@ -236,4 +309,5 @@ 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); diff --git a/src/modules/time_manager_module.c b/src/modules/time_manager_module.c index 94e79b6..ea05d0d 100644 --- a/src/modules/time_manager_module.c +++ b/src/modules/time_manager_module.c @@ -17,7 +17,7 @@ #include LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); -#define TIME_MANAGER_SAVE_DELAY_MS 1000 +#define TIME_MANAGER_SAVE_DELAY K_HOURS(24) #define TIME_MANAGER_STORAGE_KEY "state" /* @@ -58,6 +58,7 @@ static bool time_manager_source_is_valid(enum time_sync_source 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; @@ -72,8 +73,8 @@ static bool time_manager_timezone_is_valid(int16_t timezone_min) /* * 保存工作在系统工作队列里执行: - * - 这样 GATT 写回调和事件派发路径都只做内存更新; - * - 真正可能触发 flash 擦写的 settings_save_one 被挪到异步上下文。 + * - 这样同步入口只做内存更新; + * - flash 写入被节流到 24 小时窗口,降低频繁校时带来的磨损。 */ static int time_manager_store_state(const struct time_manager_storage_data *storage) { @@ -144,11 +145,13 @@ static void time_manager_apply_update(const struct time_sync_update *update) k_spin_unlock(&time_ctx.lock, key); /* - * 保存延迟 1 秒: - * - 避免未来 BLE/USB 连续校时时反复触发 flash 写入; - * - 也避免在同步入口上下文里直接阻塞等待存储完成。 + * 时间同步允许立即生效,但 flash 落盘不要求同步完成后立刻发生。 + * 这里将写入节流到 24 小时窗口:只要当前还没有待执行的保存工作, + * 就挂一个延迟保存;后续新的校时只更新内存快照,不反复重置定时器。 */ - k_work_reschedule(&time_ctx.save_work, K_MSEC(TIME_MANAGER_SAVE_DELAY_MS)); + 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, @@ -269,37 +272,6 @@ static bool handle_time_sync_event(const struct time_sync_event *event) return false; } -/* - * 掉电前尽量把最后一次同步结果落盘: - * - 如果没有脏数据,立即返回; - * - 如果有待保存数据,则同步执行一次保存,降低突然掉电时的丢失概率。 - */ -static bool handle_power_down_event(void) -{ - struct time_manager_storage_data storage; - bool should_store; - k_spinlock_key_t key; - - if (!time_manager_is_ready()) { - return false; - } - - (void)k_work_cancel_delayable(&time_ctx.save_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 false; - } - - (void)time_manager_store_state(&storage); - return false; -} - /* 仅在 settings_loader 完成后宣布 READY,保证外部读取到的是稳定状态。 */ static bool handle_module_state_event(const struct module_state_event *event) { @@ -393,10 +365,6 @@ static bool app_event_handler(const struct app_event_header *aeh) return handle_time_sync_event(cast_time_sync_event(aeh)); } - if (is_power_down_event(aeh)) { - return handle_power_down_event(); - } - __ASSERT_NO_MSG(false); return false; } @@ -404,4 +372,3 @@ static bool app_event_handler(const struct app_event_header *aeh) APP_EVENT_LISTENER(MODULE, app_event_handler); APP_EVENT_SUBSCRIBE(MODULE, module_state_event); APP_EVENT_SUBSCRIBE(MODULE, time_sync_event); -APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event); diff --git a/src/modules/usb_hid_module.c b/src/modules/usb_hid_module.c index d5386d3..62663bc 100644 --- a/src/modules/usb_hid_module.c +++ b/src/modules/usb_hid_module.c @@ -14,6 +14,7 @@ #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" @@ -106,6 +107,19 @@ 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) @@ -205,6 +219,35 @@ static bool try_extract_vendor_mask(const struct device *dev, 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) @@ -226,6 +269,9 @@ static int hid_stub_set_report(const struct device *dev, 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)) { @@ -234,6 +280,13 @@ static int hid_stub_set_report(const struct device *dev, 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; @@ -314,17 +367,29 @@ static void hid_stub_input_done(const struct device *dev, const uint8_t *report) 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)) { - const uint8_t *mask_data; - size_t mask_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; } @@ -711,7 +776,7 @@ static bool handle_wake_up_event(void) static bool handle_hid_tx_event(const struct hid_tx_event *event) { - if (!g_usb_hid.policy.usb_mode_selected) + if (!usb_hid_should_handle_tx_event(event)) { return false; } @@ -793,7 +858,8 @@ static bool handle_hid_tx_event(const struct hid_tx_event *event) if ((report_id != REPORT_ID_KEYBOARD) && (report_id != REPORT_ID_CONSUMER) && - (report_id != REPORT_ID_VENDOR)) + (report_id != REPORT_ID_VENDOR) && + (report_id != REPORT_ID_VENDOR_CMD)) { submit_usb_tx_done(HID_TX_KIND_REPORT, false); return false;