From 42a36164be01811c48a48cb66b03f8f1e9da8484 Mon Sep 17 00:00:00 2001 From: stli Date: Fri, 10 Apr 2026 16:53:41 +0800 Subject: [PATCH] Initial import of firmware and host projects --- .gitignore | 16 + CMakeLists.txt | 72 ++ Kconfig | 17 + KeyBorad/KeyBorad.slnx | 6 + KeyBorad/KeyBorad/COM/Com_Cdc.h | 121 +++ KeyBorad/KeyBorad/COM/Com_CdcDecode.cpp | 198 ++++ KeyBorad/KeyBorad/COM/Com_CdcDecode.h | 8 + KeyBorad/KeyBorad/COM/Com_CdcEncode.cpp | 159 +++ KeyBorad/KeyBorad/COM/Com_CdcEncode.h | 19 + KeyBorad/KeyBorad/DRI/Dri_Cdc.cpp | 227 +++++ KeyBorad/KeyBorad/DRI/Dri_Cdc.h | 40 + KeyBorad/KeyBorad/KeyBorad.qrc | 4 + KeyBorad/KeyBorad/KeyBorad.ui | 26 + KeyBorad/KeyBorad/KeyBorad.vcxproj | 112 +++ KeyBorad/KeyBorad/KeyBorad.vcxproj.filters | 51 + KeyBorad/KeyBorad/main.cpp | 19 + KeyBorad/PROTOBUF_ALIGN_STEPS.md | 305 ++++++ KeyBorad/proto/keyboard.options | 1 + KeyBorad/proto/keyboard.proto | 83 ++ app.overlay | 47 + .../Kconfig.atguigu_keyboard_dongle | 2 + .../atguigu_keyboard_dongle-pinctrl.dtsi | 2 + .../atguigu_keyboard_dongle.dts | 42 + .../atguigu_keyboard_dongle.yaml | 10 + .../atguigu_keyboard_dongle_defconfig | 2 + .../atguigu_keyboard_dongle/board.cmake | 9 + .../atguigu/atguigu_keyboard_dongle/board.yml | 5 + .../pre_dt_board.cmake | 2 + .../Kconfig.atguigu_mini_keyboard | 2 + .../atguigu_mini_keyboard-pinctrl.dtsi | 73 ++ .../atguigu_mini_keyboard.dts | 240 +++++ .../atguigu_mini_keyboard.yaml | 10 + .../atguigu_mini_keyboard_defconfig | 2 + .../atguigu/atguigu_mini_keyboard/board.cmake | 9 + .../atguigu/atguigu_mini_keyboard/board.yml | 5 + .../atguigu_mini_keyboard/pre_dt_board.cmake | 2 + docs/ble_bond_design_notes.md | 411 ++++++++ docs/ble_time_sync_pc_host.md | 281 ++++++ docs/bluetooth_sig_ble_profiles_summary.md | 314 ++++++ docs/lvgl_zephyr_porting_notes.md | 791 +++++++++++++++ docs/ncs_v3_2_3_gatt_services_summary.md | 926 ++++++++++++++++++ docs/nordic_ncs_官方知识索引.md | 118 +++ docs/zephyr_官方知识索引.md | 96 ++ inc/buttons_def.h | 34 + inc/hid_host_command_protocol.h | 16 + inc/hid_host_transport.h | 9 + inc/hid_keymap_def.h | 43 + inc/hid_report_descriptor.h | 161 +++ inc/led_state.h | 29 + inc/led_state_def.h | 39 + inc/settings_loader_def.h | 17 + inc/time_manager.h | 69 ++ inc/time_sync_protocol.h | 33 + modules/ip5306/CMakeLists.txt | 5 + modules/ip5306/Kconfig | 5 + modules/ip5306/drivers/power/Kconfig | 17 + modules/ip5306/drivers/power/ip5306.c | 152 +++ .../drivers/power/ip5306_keepalive_nrf.c | 198 ++++ .../drivers/power/ip5306_keepalive_sw.c | 54 + modules/ip5306/drivers/power/ip5306_priv.h | 58 ++ .../dts/bindings/power/injoinic,ip5306.yaml | 33 + .../ip5306/dts/bindings/vendor-prefixes.txt | 1 + .../include/zephyr/drivers/power/ip5306.h | 48 + modules/ip5306/sysbuild/CMakeLists.txt | 24 + modules/ip5306/zephyr/module.yml | 6 + pm_static.yml | 33 + prj.conf | 119 +++ src/events/battery_status_event.c | 33 + src/events/battery_status_event.h | 61 ++ src/events/config_event.c | 18 + src/events/config_event.h | 54 + src/events/display_theme_event.c | 31 + src/events/display_theme_event.h | 30 + src/events/hid_boot_event.c | 26 + src/events/hid_boot_event.h | 45 + src/events/hid_host_ack_event.c | 29 + src/events/hid_host_ack_event.h | 29 + src/events/hid_host_command_error_event.c | 36 + src/events/hid_host_command_error_event.h | 41 + src/events/hid_host_command_event.c | 47 + src/events/hid_host_command_event.h | 44 + src/events/hid_protocol_event.c | 34 + src/events/hid_protocol_event.h | 49 + src/events/hid_report_event.c | 42 + src/events/hid_report_event.h | 45 + src/events/hid_tx_done_event.c | 29 + src/events/hid_tx_done_event.h | 28 + src/events/hid_tx_event.c | 49 + src/events/hid_tx_event.h | 69 ++ src/events/hid_vendor_mask_event.c | 26 + src/events/hid_vendor_mask_event.h | 39 + src/events/keyboard_led_event.c | 28 + src/events/keyboard_led_event.h | 73 ++ src/events/mode_event.c | 34 + src/events/mode_event.h | 53 + src/events/qdec_step_event.c | 26 + src/events/qdec_step_event.h | 36 + src/events/time_sync_event.c | 39 + src/events/time_sync_event.h | 35 + src/main.c | 18 + src/modules/battery_module.c | 392 ++++++++ src/modules/ble_adv_ctrl_module.c | 85 ++ src/modules/ble_battery_module.c | 51 + src/modules/ble_bond_module.c | 583 +++++++++++ src/modules/ble_hid_module.c | 404 ++++++++ src/modules/ble_slot_ctrl_module.c | 281 ++++++ src/modules/ble_time_sync_module.c | 175 ++++ src/modules/display_module.c | 549 +++++++++++ src/modules/hid_host_command_module.c | 121 +++ src/modules/hid_tx_manager_module.c | 313 ++++++ src/modules/keyboard_module.c | 440 +++++++++ src/modules/led_state_module.c | 180 ++++ src/modules/mode_switch_module.c | 219 +++++ src/modules/qdec_module.c | 243 +++++ src/modules/time_manager_module.c | 374 +++++++ src/modules/usb_hid_module.c | 925 +++++++++++++++++ src/ui/display_ui.c | 306 ++++++ src/ui/display_ui.h | 34 + src/ui/fonts/ui_font_keyboard_small_18.c | 379 +++++++ src/ui/fonts/ui_font_keyboard_time_48.c | 455 +++++++++ sysbuild.conf | 2 + sysbuild/CMakeLists.txt | 20 + sysbuild/mcuboot.conf | 25 + sysbuild/mcuboot.overlay | 23 + 124 files changed, 13943 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 Kconfig create mode 100644 KeyBorad/KeyBorad.slnx create mode 100644 KeyBorad/KeyBorad/COM/Com_Cdc.h create mode 100644 KeyBorad/KeyBorad/COM/Com_CdcDecode.cpp create mode 100644 KeyBorad/KeyBorad/COM/Com_CdcDecode.h create mode 100644 KeyBorad/KeyBorad/COM/Com_CdcEncode.cpp create mode 100644 KeyBorad/KeyBorad/COM/Com_CdcEncode.h create mode 100644 KeyBorad/KeyBorad/DRI/Dri_Cdc.cpp create mode 100644 KeyBorad/KeyBorad/DRI/Dri_Cdc.h create mode 100644 KeyBorad/KeyBorad/KeyBorad.qrc create mode 100644 KeyBorad/KeyBorad/KeyBorad.ui create mode 100644 KeyBorad/KeyBorad/KeyBorad.vcxproj create mode 100644 KeyBorad/KeyBorad/KeyBorad.vcxproj.filters create mode 100644 KeyBorad/KeyBorad/main.cpp create mode 100644 KeyBorad/PROTOBUF_ALIGN_STEPS.md create mode 100644 KeyBorad/proto/keyboard.options create mode 100644 KeyBorad/proto/keyboard.proto create mode 100644 app.overlay create mode 100644 boards/atguigu/atguigu_keyboard_dongle/Kconfig.atguigu_keyboard_dongle create mode 100644 boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle-pinctrl.dtsi create mode 100644 boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle.dts create mode 100644 boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle.yaml create mode 100644 boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle_defconfig create mode 100644 boards/atguigu/atguigu_keyboard_dongle/board.cmake create mode 100644 boards/atguigu/atguigu_keyboard_dongle/board.yml create mode 100644 boards/atguigu/atguigu_keyboard_dongle/pre_dt_board.cmake create mode 100644 boards/atguigu/atguigu_mini_keyboard/Kconfig.atguigu_mini_keyboard create mode 100644 boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard-pinctrl.dtsi create mode 100644 boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.dts create mode 100644 boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.yaml create mode 100644 boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard_defconfig create mode 100644 boards/atguigu/atguigu_mini_keyboard/board.cmake create mode 100644 boards/atguigu/atguigu_mini_keyboard/board.yml create mode 100644 boards/atguigu/atguigu_mini_keyboard/pre_dt_board.cmake create mode 100644 docs/ble_bond_design_notes.md create mode 100644 docs/ble_time_sync_pc_host.md create mode 100644 docs/bluetooth_sig_ble_profiles_summary.md create mode 100644 docs/lvgl_zephyr_porting_notes.md create mode 100644 docs/ncs_v3_2_3_gatt_services_summary.md create mode 100644 docs/nordic_ncs_官方知识索引.md create mode 100644 docs/zephyr_官方知识索引.md create mode 100644 inc/buttons_def.h create mode 100644 inc/hid_host_command_protocol.h create mode 100644 inc/hid_host_transport.h create mode 100644 inc/hid_keymap_def.h create mode 100644 inc/hid_report_descriptor.h create mode 100644 inc/led_state.h create mode 100644 inc/led_state_def.h create mode 100644 inc/settings_loader_def.h create mode 100644 inc/time_manager.h create mode 100644 inc/time_sync_protocol.h create mode 100644 modules/ip5306/CMakeLists.txt create mode 100644 modules/ip5306/Kconfig create mode 100644 modules/ip5306/drivers/power/Kconfig create mode 100644 modules/ip5306/drivers/power/ip5306.c create mode 100644 modules/ip5306/drivers/power/ip5306_keepalive_nrf.c create mode 100644 modules/ip5306/drivers/power/ip5306_keepalive_sw.c create mode 100644 modules/ip5306/drivers/power/ip5306_priv.h create mode 100644 modules/ip5306/dts/bindings/power/injoinic,ip5306.yaml create mode 100644 modules/ip5306/dts/bindings/vendor-prefixes.txt create mode 100644 modules/ip5306/include/zephyr/drivers/power/ip5306.h create mode 100644 modules/ip5306/sysbuild/CMakeLists.txt create mode 100644 modules/ip5306/zephyr/module.yml create mode 100644 pm_static.yml create mode 100644 prj.conf create mode 100644 src/events/battery_status_event.c create mode 100644 src/events/battery_status_event.h create mode 100644 src/events/config_event.c create mode 100644 src/events/config_event.h create mode 100644 src/events/display_theme_event.c create mode 100644 src/events/display_theme_event.h create mode 100644 src/events/hid_boot_event.c create mode 100644 src/events/hid_boot_event.h create mode 100644 src/events/hid_host_ack_event.c create mode 100644 src/events/hid_host_ack_event.h create mode 100644 src/events/hid_host_command_error_event.c create mode 100644 src/events/hid_host_command_error_event.h create mode 100644 src/events/hid_host_command_event.c create mode 100644 src/events/hid_host_command_event.h create mode 100644 src/events/hid_protocol_event.c create mode 100644 src/events/hid_protocol_event.h create mode 100644 src/events/hid_report_event.c create mode 100644 src/events/hid_report_event.h create mode 100644 src/events/hid_tx_done_event.c create mode 100644 src/events/hid_tx_done_event.h create mode 100644 src/events/hid_tx_event.c create mode 100644 src/events/hid_tx_event.h create mode 100644 src/events/hid_vendor_mask_event.c create mode 100644 src/events/hid_vendor_mask_event.h create mode 100644 src/events/keyboard_led_event.c create mode 100644 src/events/keyboard_led_event.h create mode 100644 src/events/mode_event.c create mode 100644 src/events/mode_event.h create mode 100644 src/events/qdec_step_event.c create mode 100644 src/events/qdec_step_event.h create mode 100644 src/events/time_sync_event.c create mode 100644 src/events/time_sync_event.h create mode 100644 src/main.c create mode 100644 src/modules/battery_module.c create mode 100644 src/modules/ble_adv_ctrl_module.c create mode 100644 src/modules/ble_battery_module.c create mode 100644 src/modules/ble_bond_module.c create mode 100644 src/modules/ble_hid_module.c create mode 100644 src/modules/ble_slot_ctrl_module.c create mode 100644 src/modules/ble_time_sync_module.c create mode 100644 src/modules/display_module.c create mode 100644 src/modules/hid_host_command_module.c create mode 100644 src/modules/hid_tx_manager_module.c create mode 100644 src/modules/keyboard_module.c create mode 100644 src/modules/led_state_module.c create mode 100644 src/modules/mode_switch_module.c create mode 100644 src/modules/qdec_module.c create mode 100644 src/modules/time_manager_module.c create mode 100644 src/modules/usb_hid_module.c create mode 100644 src/ui/display_ui.c create mode 100644 src/ui/display_ui.h create mode 100644 src/ui/fonts/ui_font_keyboard_small_18.c create mode 100644 src/ui/fonts/ui_font_keyboard_time_48.c create mode 100644 sysbuild.conf create mode 100644 sysbuild/CMakeLists.txt create mode 100644 sysbuild/mcuboot.conf create mode 100644 sysbuild/mcuboot.overlay diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd3d18c --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +build*/ +external*/ +.vs/ +**/.vs/ +x64/ +**/x64/ +*.user +*.suo +*.pdb +*.idb +*.ilk +*.recipe +*.tlog +*.lastbuildstate +*.vcxproj.FileListAbsolute.txt +qt_work.log diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5f9e57e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,72 @@ +cmake_minimum_required(VERSION 3.20.0) + +set(IP5306_LOCAL_MODULE "${CMAKE_CURRENT_SOURCE_DIR}/modules/ip5306") +set(IP5306_EXTERNAL_MODULE "E:/extra/modules/ip5306") +set(extra_zephyr_modules "$CACHE{ZEPHYR_EXTRA_MODULES}") + +if(EXISTS "${IP5306_LOCAL_MODULE}/zephyr/module.yml") + list(APPEND extra_zephyr_modules "${IP5306_LOCAL_MODULE}") +elseif(EXISTS "${IP5306_EXTERNAL_MODULE}/zephyr/module.yml") + list(APPEND extra_zephyr_modules "${IP5306_EXTERNAL_MODULE}") +endif() + +list(REMOVE_DUPLICATES extra_zephyr_modules) +set(ZEPHYR_EXTRA_MODULES "${extra_zephyr_modules}" CACHE STRING "Extra Zephyr modules" FORCE) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +project(new_kbd) + +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 +) diff --git a/Kconfig b/Kconfig new file mode 100644 index 0000000..f67b023 --- /dev/null +++ b/Kconfig @@ -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" diff --git a/KeyBorad/KeyBorad.slnx b/KeyBorad/KeyBorad.slnx new file mode 100644 index 0000000..0d81634 --- /dev/null +++ b/KeyBorad/KeyBorad.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/KeyBorad/KeyBorad/COM/Com_Cdc.h b/KeyBorad/KeyBorad/COM/Com_Cdc.h new file mode 100644 index 0000000..0876631 --- /dev/null +++ b/KeyBorad/KeyBorad/COM/Com_Cdc.h @@ -0,0 +1,121 @@ +#pragma once + +#include +#include + +struct Packet_len +{ + static const quint8 Com_Len_UnKnow = 0; + static const quint8 Com_Len_HelloReq = 1; + static const quint8 Com_Len_HelloRsp = 9; + static const quint8 Com_Len_Bitmap = 29; + static const quint8 Com_Len_FunctionKeyEvent = 3; + static const quint8 Com_Len_LedState = 1; + static const quint8 Com_Len_TimeSync = 16; + static const quint8 Com_Len_ThemeRgb = 3; + static const quint8 Com_Len_Ack = 1; + static const quint8 Com_Len_Error = 2; +}; + +enum Packet_Type : quint8 +{ + Com_Type_UnKnow = 0x00, + + Com_Type_HelloReq = 0x01, + Com_Type_HelloRsp = 0x02, + + Com_Type_Bitmap = 0x10, + + Com_Type_FunctionKeyEvent = 0x20, + Com_Type_LedState = 0x21, + + Com_Type_TimeSync = 0x30, + Com_Type_ThemeRgb = 0x31, + + Com_Type_Ack = 0x7E, + Com_Type_Error = 0x7F +}; + +enum Key_Action : quint8 +{ + Key_Action_Release = 0x00, + Key_Action_Press = 0x01 +}; + +enum Error_Code : quint8 +{ + Error_Code_None = 0x00, + Error_Code_UnknownType = 0x01, + Error_Code_InvalidLength = 0x02, + Error_Code_InvalidParam = 0x03, + Error_Code_NotReady = 0x04 +}; + +struct Packet +{ + quint8 Com_Packet_Head1 = 0xAA; + quint8 Com_Packet_Head2 = 0x55; + + quint8 len = Packet_len::Com_Len_UnKnow; + Packet_Type type = Com_Type_UnKnow; + + QByteArray data; +}; + +struct Packet_HelloReq +{ + quint8 ProtocolVersion = 0x01; +}; + +struct Packet_HelloRsp +{ + quint8 ProtocolVersion = 0x01; + quint16 VendorId = 0; + quint16 ProductId = 0; + quint8 FirmwareMajor = 0; + quint8 FirmwareMinor = 0; + quint16 CapabilityFlags = 0; +}; + +struct Packet_Bitmap +{ + QByteArray UsageBitmap = QByteArray(Packet_len::Com_Len_Bitmap, 0); +}; + +struct Packet_FunctionKeyEvent +{ + quint16 Usage = 0; + quint8 Action = Key_Action_Release; +}; + +struct Packet_LedState +{ + quint8 LedMask = 0; +}; + +struct Packet_TimeSync +{ + quint8 Version = 1; + quint8 Flags = 0; + quint16 TimezoneMin = 0; + quint64 UtcMs = 0; + quint32 AccuracyMs = 0; +}; + +struct Packet_ThemeRgb +{ + quint8 Red = 0; + quint8 Green = 0; + quint8 Blue = 0; +}; + +struct Packet_Ack +{ + quint8 AckedType = 0; +}; + +struct Packet_Error +{ + quint8 ErrorType = 0; + quint8 ErrorCode = Error_Code_None; +}; diff --git a/KeyBorad/KeyBorad/COM/Com_CdcDecode.cpp b/KeyBorad/KeyBorad/COM/Com_CdcDecode.cpp new file mode 100644 index 0000000..8470afe --- /dev/null +++ b/KeyBorad/KeyBorad/COM/Com_CdcDecode.cpp @@ -0,0 +1,198 @@ +#include "COM/Com_CdcDecode.h" +#include "COM/Com_CdcEncode.h" + +namespace +{ + +constexpr quint8 COM_CDC_CONST_PACKET_HEAD1 = 0xAA; +constexpr quint8 COM_CDC_CONST_PACKET_HEAD2 = 0x55; +constexpr int COM_CDC_CONST_FRAME_OVERHEAD = 5; +constexpr int COM_CDC_CONST_MAX_PAYLOAD_LENGTH = 64; + +bool Com_Cdc_Func_IsKnownLengthValid(Packet_Type Type, quint8 DataLength) +{ + switch (Type) + { + case Com_Type_UnKnow: + return DataLength == Packet_len::Com_Len_UnKnow; + + case Com_Type_HelloReq: + return DataLength == Packet_len::Com_Len_HelloReq; + + case Com_Type_HelloRsp: + return DataLength == Packet_len::Com_Len_HelloRsp; + + case Com_Type_Bitmap: + return DataLength == Packet_len::Com_Len_Bitmap; + + case Com_Type_FunctionKeyEvent: + return DataLength == Packet_len::Com_Len_FunctionKeyEvent; + + case Com_Type_LedState: + return DataLength == Packet_len::Com_Len_LedState; + + case Com_Type_TimeSync: + return DataLength == Packet_len::Com_Len_TimeSync; + + case Com_Type_ThemeRgb: + return DataLength == Packet_len::Com_Len_ThemeRgb; + + case Com_Type_Ack: + return DataLength == Packet_len::Com_Len_Ack; + + case Com_Type_Error: + return DataLength == Packet_len::Com_Len_Error; + + default: + return true; + } +} + +bool Com_Cdc_Func_ParseFrameAtStart(const QByteArray& ByteArray, Packet* p_Packet) +{ + if ((p_Packet == nullptr) || (ByteArray.size() < COM_CDC_CONST_FRAME_OVERHEAD)) + { + return false; + } + + if (static_cast(ByteArray.at(0)) != COM_CDC_CONST_PACKET_HEAD1) + { + return false; + } + + if (static_cast(ByteArray.at(1)) != COM_CDC_CONST_PACKET_HEAD2) + { + return false; + } + + const quint8 DataLength = static_cast(ByteArray.at(2)); + if (DataLength > COM_CDC_CONST_MAX_PAYLOAD_LENGTH) + { + return false; + } + + const Packet_Type Type = + static_cast(static_cast(ByteArray.at(3))); + if (!Com_Cdc_Func_IsKnownLengthValid(Type, DataLength)) + { + return false; + } + + const int FrameLength = COM_CDC_CONST_FRAME_OVERHEAD + static_cast(DataLength); + if (ByteArray.size() != FrameLength) + { + return false; + } + + const QByteArray Body = ByteArray.left(FrameLength - 1); + const quint8 Checksum = Com_Cdc_Func_CalcChecksum(Body); + const quint8 ChecksumRx = static_cast(ByteArray.at(FrameLength - 1)); + if (Checksum != ChecksumRx) + { + return false; + } + + p_Packet->Com_Packet_Head1 = COM_CDC_CONST_PACKET_HEAD1; + p_Packet->Com_Packet_Head2 = COM_CDC_CONST_PACKET_HEAD2; + p_Packet->len = DataLength; + p_Packet->type = Type; + p_Packet->data = ByteArray.mid(4, DataLength); + return true; +} + +int Com_Cdc_Func_FindHead(const QByteArray& ByteArray) +{ + for (int Index = 0; Index + 1 < ByteArray.size(); ++Index) + { + if ((static_cast(ByteArray.at(Index)) == COM_CDC_CONST_PACKET_HEAD1) && + (static_cast(ByteArray.at(Index + 1)) == COM_CDC_CONST_PACKET_HEAD2)) + { + return Index; + } + } + + return -1; +} + +void Com_Cdc_Func_KeepTailForNextScan(QByteArray* p_Buffer) +{ + if (!p_Buffer->isEmpty() && + (static_cast(p_Buffer->at(p_Buffer->size() - 1)) == COM_CDC_CONST_PACKET_HEAD1)) + { + *p_Buffer = QByteArray(1, static_cast(COM_CDC_CONST_PACKET_HEAD1)); + return; + } + + p_Buffer->clear(); +} + +} // namespace + +bool Com_Cdc_Func_ParseFrame(const QByteArray& ByteArray, Packet* p_Packet) +{ + return Com_Cdc_Func_ParseFrameAtStart(ByteArray, p_Packet); +} + +bool Com_Cdc_Func_TryTakeFrame(QByteArray* p_Buffer, Packet* p_Packet) +{ + if ((p_Buffer == nullptr) || (p_Packet == nullptr)) + { + return false; + } + + while (true) + { + if (p_Buffer->size() < 2) + { + return false; + } + + const int HeadIndex = Com_Cdc_Func_FindHead(*p_Buffer); + if (HeadIndex < 0) + { + Com_Cdc_Func_KeepTailForNextScan(p_Buffer); + return false; + } + + if (HeadIndex > 0) + { + p_Buffer->remove(0, HeadIndex); + } + + if (p_Buffer->size() < 4) + { + return false; + } + + const quint8 DataLength = static_cast(p_Buffer->at(2)); + if (DataLength > COM_CDC_CONST_MAX_PAYLOAD_LENGTH) + { + p_Buffer->remove(0, 1); + continue; + } + + const Packet_Type Type = + static_cast(static_cast(p_Buffer->at(3))); + if (!Com_Cdc_Func_IsKnownLengthValid(Type, DataLength)) + { + p_Buffer->remove(0, 1); + continue; + } + + const int FrameLength = COM_CDC_CONST_FRAME_OVERHEAD + static_cast(DataLength); + if (p_Buffer->size() < FrameLength) + { + return false; + } + + const QByteArray FrameBytes = p_Buffer->left(FrameLength); + if (!Com_Cdc_Func_ParseFrameAtStart(FrameBytes, p_Packet)) + { + p_Buffer->remove(0, 1); + continue; + } + + p_Buffer->remove(0, FrameLength); + return true; + } +} diff --git a/KeyBorad/KeyBorad/COM/Com_CdcDecode.h b/KeyBorad/KeyBorad/COM/Com_CdcDecode.h new file mode 100644 index 0000000..0b6f72e --- /dev/null +++ b/KeyBorad/KeyBorad/COM/Com_CdcDecode.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#include "COM/Com_Cdc.h" + +bool Com_Cdc_Func_ParseFrame(const QByteArray& ByteArray, Packet* p_Packet); +bool Com_Cdc_Func_TryTakeFrame(QByteArray* p_Buffer, Packet* p_Packet); diff --git a/KeyBorad/KeyBorad/COM/Com_CdcEncode.cpp b/KeyBorad/KeyBorad/COM/Com_CdcEncode.cpp new file mode 100644 index 0000000..e9a973e --- /dev/null +++ b/KeyBorad/KeyBorad/COM/Com_CdcEncode.cpp @@ -0,0 +1,159 @@ +#include "COM/Com_CdcEncode.h" + +namespace +{ + +Packet Com_Cdc_Func_MakePacket(Packet_Type Type, const QByteArray& Data) +{ + Packet PacketData; + PacketData.type = Type; + PacketData.len = static_cast(Data.size()); + PacketData.data = Data; + return PacketData; +} + +void Com_Cdc_Func_WriteLe16(QByteArray* p_Data, int Offset, quint16 Value) +{ + (*p_Data)[Offset] = static_cast(Value & 0x00FFU); + (*p_Data)[Offset + 1] = static_cast((Value >> 8) & 0x00FFU); +} + +void Com_Cdc_Func_WriteLe32(QByteArray* p_Data, int Offset, quint32 Value) +{ + for (int Index = 0; Index < 4; ++Index) + { + (*p_Data)[Offset + Index] = static_cast((Value >> (Index * 8)) & 0xFFU); + } +} + +void Com_Cdc_Func_WriteLe64(QByteArray* p_Data, int Offset, quint64 Value) +{ + for (int Index = 0; Index < 8; ++Index) + { + (*p_Data)[Offset + Index] = static_cast((Value >> (Index * 8)) & 0xFFULL); + } +} + +QByteArray Com_Cdc_Func_FitPayload(const QByteArray& Data, int ExpectedLength) +{ + QByteArray Payload = Data.left(ExpectedLength); + if (Payload.size() < ExpectedLength) + { + Payload.append(ExpectedLength - Payload.size(), 0); + } + return Payload; +} + +} // namespace + +quint8 Com_Cdc_Func_CalcChecksum(const QByteArray& ByteArray) +{ + quint8 Checksum = 0; + + for (int Index = 0; Index < ByteArray.size(); ++Index) + { + const quint8 ByteValue = static_cast(ByteArray.at(Index)); + Checksum = static_cast(Checksum ^ ByteValue); + } + + return Checksum; +} + +QByteArray Com_Cdc_Func_BuildFrame(const Packet& PacketData) +{ + if (PacketData.data.size() > 0xFF) + { + return QByteArray(); + } + + QByteArray ByteArray; + const quint8 DataLength = static_cast(PacketData.data.size()); + + ByteArray.append(static_cast(PacketData.Com_Packet_Head1)); + ByteArray.append(static_cast(PacketData.Com_Packet_Head2)); + ByteArray.append(static_cast(DataLength)); + ByteArray.append(static_cast(PacketData.type)); + ByteArray.append(PacketData.data); + + const quint8 Checksum = Com_Cdc_Func_CalcChecksum(ByteArray); + ByteArray.append(static_cast(Checksum)); + return ByteArray; +} + +QByteArray Com_Cdc_Func_BuildHelloReq(const Packet_HelloReq& PacketData) +{ + QByteArray Payload(Packet_len::Com_Len_HelloReq, 0); + Payload[0] = static_cast(PacketData.ProtocolVersion); + return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_HelloReq, Payload)); +} + +QByteArray Com_Cdc_Func_BuildHelloRsp(const Packet_HelloRsp& PacketData) +{ + QByteArray Payload(Packet_len::Com_Len_HelloRsp, 0); + Payload[0] = static_cast(PacketData.ProtocolVersion); + Com_Cdc_Func_WriteLe16(&Payload, 1, PacketData.VendorId); + Com_Cdc_Func_WriteLe16(&Payload, 3, PacketData.ProductId); + Payload[5] = static_cast(PacketData.FirmwareMajor); + Payload[6] = static_cast(PacketData.FirmwareMinor); + Com_Cdc_Func_WriteLe16(&Payload, 7, PacketData.CapabilityFlags); + return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_HelloRsp, Payload)); +} + +QByteArray Com_Cdc_Func_BuildBitmap(const Packet_Bitmap& PacketData) +{ + return Com_Cdc_Func_BuildFrame( + Com_Cdc_Func_MakePacket( + Com_Type_Bitmap, + Com_Cdc_Func_FitPayload(PacketData.UsageBitmap, Packet_len::Com_Len_Bitmap))); +} + +QByteArray Com_Cdc_Func_BuildFunctionKeyEvent(const Packet_FunctionKeyEvent& PacketData) +{ + QByteArray Payload(Packet_len::Com_Len_FunctionKeyEvent, 0); + Com_Cdc_Func_WriteLe16(&Payload, 0, PacketData.Usage); + Payload[2] = static_cast(PacketData.Action); + return Com_Cdc_Func_BuildFrame( + Com_Cdc_Func_MakePacket(Com_Type_FunctionKeyEvent, Payload)); +} + +QByteArray Com_Cdc_Func_BuildLedState(const Packet_LedState& PacketData) +{ + QByteArray Payload(Packet_len::Com_Len_LedState, 0); + Payload[0] = static_cast(PacketData.LedMask); + return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_LedState, Payload)); +} + +QByteArray Com_Cdc_Func_BuildTimeSync(const Packet_TimeSync& PacketData) +{ + QByteArray Payload(Packet_len::Com_Len_TimeSync, 0); + Payload[0] = static_cast(PacketData.Version); + Payload[1] = static_cast(PacketData.Flags); + Com_Cdc_Func_WriteLe16(&Payload, 2, PacketData.TimezoneMin); + Com_Cdc_Func_WriteLe64(&Payload, 4, PacketData.UtcMs); + Com_Cdc_Func_WriteLe32(&Payload, 12, PacketData.AccuracyMs); + return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_TimeSync, Payload)); +} + +QByteArray Com_Cdc_Func_BuildThemeRgb(const Packet_ThemeRgb& PacketData) +{ + QByteArray Payload(Packet_len::Com_Len_ThemeRgb, 0); + Payload[0] = static_cast(PacketData.Red); + Payload[1] = static_cast(PacketData.Green); + Payload[2] = static_cast(PacketData.Blue); + return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_ThemeRgb, Payload)); +} + +QByteArray Com_Cdc_Func_BuildAck(const Packet_Ack& PacketData) +{ + QByteArray Payload(Packet_len::Com_Len_Ack, 0); + Payload[0] = static_cast(PacketData.AckedType); + return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_Ack, Payload)); +} + +QByteArray Com_Cdc_Func_BuildError(const Packet_Error& PacketData) +{ + QByteArray Payload(Packet_len::Com_Len_Error, 0); + Payload[0] = static_cast(PacketData.ErrorType); + Payload[1] = static_cast(PacketData.ErrorCode); + return Com_Cdc_Func_BuildFrame(Com_Cdc_Func_MakePacket(Com_Type_Error, Payload)); +} diff --git a/KeyBorad/KeyBorad/COM/Com_CdcEncode.h b/KeyBorad/KeyBorad/COM/Com_CdcEncode.h new file mode 100644 index 0000000..ffcd5d3 --- /dev/null +++ b/KeyBorad/KeyBorad/COM/Com_CdcEncode.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include "COM/Com_Cdc.h" + +quint8 Com_Cdc_Func_CalcChecksum(const QByteArray& ByteArray); + +QByteArray Com_Cdc_Func_BuildFrame(const Packet& PacketData); + +QByteArray Com_Cdc_Func_BuildHelloReq(const Packet_HelloReq& PacketData); +QByteArray Com_Cdc_Func_BuildHelloRsp(const Packet_HelloRsp& PacketData); +QByteArray Com_Cdc_Func_BuildBitmap(const Packet_Bitmap& PacketData); +QByteArray Com_Cdc_Func_BuildFunctionKeyEvent(const Packet_FunctionKeyEvent& PacketData); +QByteArray Com_Cdc_Func_BuildLedState(const Packet_LedState& PacketData); +QByteArray Com_Cdc_Func_BuildTimeSync(const Packet_TimeSync& PacketData); +QByteArray Com_Cdc_Func_BuildThemeRgb(const Packet_ThemeRgb& PacketData); +QByteArray Com_Cdc_Func_BuildAck(const Packet_Ack& PacketData); +QByteArray Com_Cdc_Func_BuildError(const Packet_Error& PacketData); diff --git a/KeyBorad/KeyBorad/DRI/Dri_Cdc.cpp b/KeyBorad/KeyBorad/DRI/Dri_Cdc.cpp new file mode 100644 index 0000000..72b1cbb --- /dev/null +++ b/KeyBorad/KeyBorad/DRI/Dri_Cdc.cpp @@ -0,0 +1,227 @@ +#include "DRI/Dri_Cdc.h" + +#include "COM/Com_Cdc.h" +#include "COM/Com_CdcDecode.h" +#include "COM/Com_CdcEncode.h" + +#include +#include +#include +#include + +namespace +{ + +bool Dri_Cdc_Func_TryReadFrame( + QSerialPort* p_Serial, + QByteArray* p_ReadBuffer, + int TimeoutMs, + Packet* p_Packet) +{ + if ((p_Serial == nullptr) || (p_ReadBuffer == nullptr) || (p_Packet == nullptr)) + { + return false; + } + + if (Com_Cdc_Func_TryTakeFrame(p_ReadBuffer, p_Packet)) + { + return true; + } + + QElapsedTimer Timer; + Timer.start(); + while (Timer.elapsed() < TimeoutMs) + { + const int RemainingMs = TimeoutMs - static_cast(Timer.elapsed()); + const int WaitMs = qMin(20, RemainingMs); + if (WaitMs <= 0) + { + break; + } + + if (!p_Serial->waitForReadyRead(WaitMs)) + { + continue; + } + + const QByteArray ReadBytes = p_Serial->readAll(); + if (!ReadBytes.isEmpty()) + { + p_ReadBuffer->append(ReadBytes); + } + + if (Com_Cdc_Func_TryTakeFrame(p_ReadBuffer, p_Packet)) + { + return true; + } + } + + return false; +} + +} // namespace + +QVector Dri_Cdc_Enum() +{ + QVector PortList; + + const QList InfoList = QSerialPortInfo::availablePorts(); + for (int Index = 0; Index < InfoList.size(); ++Index) + { + const QSerialPortInfo& Info = InfoList.at(Index); + + Dri_Cdc_Struct_PortInfo PortInfo; + PortInfo.PortName = Info.portName(); + PortInfo.Description = Info.description(); + PortInfo.Manufacturer = Info.manufacturer(); + PortList.append(PortInfo); + } + + return PortList; +} + +void Dri_Cdc_Deinit(Dri_Cdc_Struct_Port* p_Port) +{ + if (p_Port->p_Port != nullptr) + { + if (p_Port->p_Port->isOpen()) + { + p_Port->p_Port->close(); + } + delete p_Port->p_Port; + } + + *p_Port = Dri_Cdc_Struct_Port(); +} + +bool Dri_Cdc_Init( + Dri_Cdc_Struct_Port* p_Port, + const Dri_Cdc_Struct_InitConfig& Config) +{ + Dri_Cdc_Deinit(p_Port); + + const QVector PortList = Dri_Cdc_Enum(); + if (PortList.isEmpty()) + { + return false; + } + + Packet_HelloReq HelloReq; + const QByteArray HelloReqFrame = Com_Cdc_Func_BuildHelloReq(HelloReq); + + for (int Index = 0; Index < PortList.size(); ++Index) + { + const Dri_Cdc_Struct_PortInfo& PortInfo = PortList.at(Index); + + QSerialPort* p_Serial = new QSerialPort(); + p_Serial->setPortName(PortInfo.PortName); + p_Serial->setBaudRate(Config.BaudRate); + p_Serial->setDataBits(QSerialPort::Data8); + p_Serial->setParity(QSerialPort::NoParity); + p_Serial->setStopBits(QSerialPort::OneStop); + p_Serial->setFlowControl(QSerialPort::NoFlowControl); + + if (!p_Serial->open(QIODevice::ReadWrite)) + { + delete p_Serial; + continue; + } + + p_Serial->readAll(); + + if (p_Serial->write(HelloReqFrame) != HelloReqFrame.size()) + { + p_Serial->close(); + delete p_Serial; + continue; + } + + if (!p_Serial->waitForBytesWritten(Config.HandshakeTimeoutMs)) + { + p_Serial->close(); + delete p_Serial; + continue; + } + + QByteArray HandshakeBuffer; + QElapsedTimer Timer; + Timer.start(); + while (Timer.elapsed() < Config.HandshakeTimeoutMs) + { + Packet PacketData; + const int RemainingMs = + Config.HandshakeTimeoutMs - static_cast(Timer.elapsed()); + if (!Dri_Cdc_Func_TryReadFrame( + p_Serial, + &HandshakeBuffer, + qMin(20, RemainingMs), + &PacketData)) + { + continue; + } + + if (PacketData.type == Com_Type_HelloRsp) + { + p_Port->p_Port = p_Serial; + p_Port->IsOpened = true; + p_Port->PortName = PortInfo.PortName; + p_Port->ReadBuffer = HandshakeBuffer; + return true; + } + } + + p_Serial->close(); + delete p_Serial; + } + + return false; +} + +bool Dri_Cdc_Read( + Dri_Cdc_Struct_Port* p_Port, + QByteArray& ByteArray) +{ + ByteArray.clear(); + + if (!p_Port->IsOpened || (p_Port->p_Port == nullptr)) + { + return false; + } + + Packet PacketData; + if (Com_Cdc_Func_TryTakeFrame(&p_Port->ReadBuffer, &PacketData) || + Dri_Cdc_Func_TryReadFrame(p_Port->p_Port, &p_Port->ReadBuffer, 50, &PacketData)) + { + ByteArray = Com_Cdc_Func_BuildFrame(PacketData); + return !ByteArray.isEmpty(); + } + + return false; +} + +bool Dri_Cdc_Write( + Dri_Cdc_Struct_Port* p_Port, + const QByteArray& ByteArray) +{ + if (!p_Port->IsOpened || (p_Port->p_Port == nullptr)) + { + return false; + } + + if (ByteArray.isEmpty()) + { + return false; + } + + if (p_Port->p_Port->write(ByteArray) != ByteArray.size()) + { + return false; + } + + if (!p_Port->p_Port->waitForBytesWritten(200)) + { + return false; + } + + return true; +} diff --git a/KeyBorad/KeyBorad/DRI/Dri_Cdc.h b/KeyBorad/KeyBorad/DRI/Dri_Cdc.h new file mode 100644 index 0000000..c3e8dc5 --- /dev/null +++ b/KeyBorad/KeyBorad/DRI/Dri_Cdc.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +class QSerialPort; + +struct Dri_Cdc_Struct_PortInfo +{ + QString PortName; + QString Description; + QString Manufacturer; +}; + +struct Dri_Cdc_Struct_InitConfig +{ + qint32 BaudRate = 115200; + int HandshakeTimeoutMs = 200; +}; + +struct Dri_Cdc_Struct_Port +{ + QSerialPort* p_Port = nullptr; + bool IsOpened = false; + QString PortName; + QByteArray ReadBuffer; +}; + +QVector Dri_Cdc_Enum(); +void Dri_Cdc_Deinit(Dri_Cdc_Struct_Port* p_Port); +bool Dri_Cdc_Init( + Dri_Cdc_Struct_Port* p_Port, + const Dri_Cdc_Struct_InitConfig& Config); +bool Dri_Cdc_Read( + Dri_Cdc_Struct_Port* p_Port, + QByteArray& ByteArray); +bool Dri_Cdc_Write( + Dri_Cdc_Struct_Port* p_Port, + const QByteArray& ByteArray); diff --git a/KeyBorad/KeyBorad/KeyBorad.qrc b/KeyBorad/KeyBorad/KeyBorad.qrc new file mode 100644 index 0000000..bba499f --- /dev/null +++ b/KeyBorad/KeyBorad/KeyBorad.qrc @@ -0,0 +1,4 @@ + + + + diff --git a/KeyBorad/KeyBorad/KeyBorad.ui b/KeyBorad/KeyBorad/KeyBorad.ui new file mode 100644 index 0000000..91ed30c --- /dev/null +++ b/KeyBorad/KeyBorad/KeyBorad.ui @@ -0,0 +1,26 @@ + + + KeyBoradClass + + + KeyBoradClass + + + + 0 + 0 + 600 + 400 + + + + KeyBorad + $centralwidget$ + + + + + + + + diff --git a/KeyBorad/KeyBorad/KeyBorad.vcxproj b/KeyBorad/KeyBorad/KeyBorad.vcxproj new file mode 100644 index 0000000..10249cd --- /dev/null +++ b/KeyBorad/KeyBorad/KeyBorad.vcxproj @@ -0,0 +1,112 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {175BED2A-ED45-4DDC-A67B-7C4042190634} + QtVS_v304 + 10.0 + 10.0 + $(MSBuildProjectDirectory)\QtMsBuild + + + + Application + v145 + true + Unicode + + + Application + v145 + false + true + Unicode + + + + + + + D:\App\Qt\5.13.1\msvc2015_64 + core;serialport + debug + + + D:\App\Qt\5.13.1\msvc2015_64 + core;serialport + release + + + + + + + + + + + + + + + + + + + + + + /utf-8 %(AdditionalOptions) + true + Level3 + true + true + + + Console + true + + + + + /utf-8 %(AdditionalOptions) + true + Level3 + true + true + true + true + + + Console + false + true + true + + + + + + + + + + + + + + + + + + + diff --git a/KeyBorad/KeyBorad/KeyBorad.vcxproj.filters b/KeyBorad/KeyBorad/KeyBorad.vcxproj.filters new file mode 100644 index 0000000..d6f320c --- /dev/null +++ b/KeyBorad/KeyBorad/KeyBorad.vcxproj.filters @@ -0,0 +1,51 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + qml;cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + qrc;rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {99349809-55BA-4b9d-BF79-8FDBB0286EB3} + ui + + + {639EADAA-A684-42e4-A9AD-28FC9BCB8F7C} + ts + + + + + Header Files + + + Source Files + + + Header Files + + + Source Files + + + Header Files + + + Source Files + + + Source Files + + + Header Files + + + diff --git a/KeyBorad/KeyBorad/main.cpp b/KeyBorad/KeyBorad/main.cpp new file mode 100644 index 0000000..20b7836 --- /dev/null +++ b/KeyBorad/KeyBorad/main.cpp @@ -0,0 +1,19 @@ +#include +#include + +#include "DRI/Dri_Cdc.h" + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + + Dri_Cdc_Struct_Port Port; + Dri_Cdc_Struct_InitConfig Config; + Config.BaudRate = 115200; + + const bool IsInitOk = Dri_Cdc_Init(&Port, Config); + qDebug() << "Dri_Cdc_Init =" << IsInitOk; + + Dri_Cdc_Deinit(&Port); + return 0; +} diff --git a/KeyBorad/PROTOBUF_ALIGN_STEPS.md b/KeyBorad/PROTOBUF_ALIGN_STEPS.md new file mode 100644 index 0000000..425262b --- /dev/null +++ b/KeyBorad/PROTOBUF_ALIGN_STEPS.md @@ -0,0 +1,305 @@ +# KeyBorad Protobuf 对齐步骤 + +这份步骤是按当前工程目录写的,目标是: + +1. 保留当前 CDC 传输帧: `AA 55 + len + type + data + checksum` +2. 把 `data` 改成 protobuf 二进制载荷 +3. 上位机使用 protobuf C++ +4. 下位机使用 nanopb + +## 1. 先确定边界 + +当前工程里三层职责不要打乱: + +- `KeyBorad\KeyBorad\DRI\Dri_Cdc.*` + 作用: 串口枚举、握手、收发字节流 +- `KeyBorad\KeyBorad\COM\Com_CdcEncode.*` + 作用: 把一帧 CDC 包组出来 +- `KeyBorad\KeyBorad\COM\Com_CdcDecode.*` + 作用: 从 CDC 字节流缓存里拆出一帧 + +引入 protobuf 以后,建议职责变成: + +- `DRI` + 只管串口 +- `COM` + 只管外层帧 +- `proto` + 只管 `data` 的结构定义 + +也就是说: + +- `type` 继续保留在外层帧里 +- `data` 改为 protobuf 序列化结果 +- `len` 改为 protobuf 载荷实际长度 + +## 2. 当前已经准备好的文件 + +本次已经加了两份协议源文件: + +- `KeyBorad\proto\keyboard.proto` +- `KeyBorad\proto\keyboard.options` + +其中: + +- `keyboard.proto` + 定义了 `HelloReqPayload / HelloRspPayload / BitmapPayload / FunctionKeyEventPayload / TimeSyncPayload` 等消息 +- `keyboard.options` + 给 `nanopb` 的 `bytes usage_bitmap` 指定了 `max_size:29` + +## 3. 下载工具 + +### 上位机 protobuf + +按 protobuf 官方文档,`protoc` 用 `--cpp_out` 生成 C++ 代码。 + +你需要准备: + +- `protoc.exe` +- protobuf C++ runtime 头文件和库 + +官方参考: + +- protobuf C++ generated code guide +- protobuf releases / protoc binary + +### 下位机 nanopb + +你需要准备: + +- `nanopb` runtime + `pb.h`, `pb_common.c/h`, `pb_encode.c/h`, `pb_decode.c/h` +- `nanopb_generator.py` + +官方参考: + +- nanopb overview +- nanopb generator reference + +## 4. 生成代码 + +### 4.1 生成上位机 C++ 代码 + +在工程根目录 `C:\Users\lst\Desktop\动态链接库版本\20260320_new_keyboard` 下执行: + +```powershell +New-Item -ItemType Directory -Force -Path KeyBorad\generated\cpp | Out-Null +protoc --proto_path=KeyBorad\proto --cpp_out=KeyBorad\generated\cpp KeyBorad\proto\keyboard.proto +``` + +预期生成: + +- `KeyBorad\generated\cpp\keyboard.pb.h` +- `KeyBorad\generated\cpp\keyboard.pb.cc` + +### 4.2 生成下位机 nanopb 代码 + +如果你是 nanopb 源码目录: + +```powershell +New-Item -ItemType Directory -Force -Path KeyBorad\generated\nanopb | Out-Null +python third_party\nanopb\generator\nanopb_generator.py --output-dir=KeyBorad\generated\nanopb --strip-path KeyBorad\proto\keyboard.proto +``` + +如果你用的是 nanopb 二进制包,自带 generator,也可以直接换成对应可执行文件。 + +预期生成: + +- `KeyBorad\generated\nanopb\keyboard.pb.h` +- `KeyBorad\generated\nanopb\keyboard.pb.c` + +## 5. 先做一处关键改造 + +### 必改: 不再把 payload 长度当成固定值 + +一旦 `data` 改成 protobuf,`HelloRsp / TimeSync / Ack` 的 `data` 长度就不一定再等于今天的固定字节数。 + +所以迁移时要先把 `COM` 层规则改成: + +- `len` 表示 protobuf 载荷实际字节数 +- `Com_CdcDecode` 不再用 `type -> 固定 len` 做强校验 +- 改成: + - 校验帧头 + - 校验 `len` + - 校验 checksum + - 可选校验某个 `type` 的最大长度,不再校验固定长度 + +建议做法: + +- 先保留 `Packet_Type` +- 删除或弱化 `Packet_len` 在解码期的“硬匹配” +- `Packet_len` 只保留给旧协议兼容或做上限参考 + +## 6. 上位机接入方式 + +### 6.1 在 VCXPROJ 里加入生成文件 + +把下面文件加入 `KeyBorad\KeyBorad\KeyBorad.vcxproj`: + +- `..\generated\cpp\keyboard.pb.cc` +- `..\generated\cpp\keyboard.pb.h` + +并加 include 目录: + +- `KeyBorad\generated\cpp` +- protobuf runtime include 目录 + +再链接 protobuf C++ runtime 库。 + +### 6.2 改造原则 + +不要再在 `Com_Cdc.h` 里维护两份字段和 `data`。 + +建议改成两层: + +- 原始帧: + `Packet` +- protobuf payload: + `keyboard::cdc::HelloReqPayload` 等生成类 + +### 6.3 发送流程 + +以 `HelloReq` 为例: + +1. 创建 `keyboard::cdc::HelloReqPayload` +2. 填字段 +3. `SerializeToString()` / `SerializeToArray()` +4. 把序列化结果塞进 `Packet.data` +5. `Packet.type = Com_Type_HelloReq` +6. 用 `Com_Cdc_Func_BuildFrame()` 打外层包 + +伪代码: + +```cpp +keyboard::cdc::HelloReqPayload payload; +payload.set_protocol_version(1); + +std::string bytes; +payload.SerializeToString(&bytes); + +Packet frame; +frame.type = Com_Type_HelloReq; +frame.data = QByteArray(bytes.data(), static_cast(bytes.size())); + +QByteArray tx = Com_Cdc_Func_BuildFrame(frame); +``` + +### 6.4 接收流程 + +1. `Dri_Cdc_Read()` 读到完整 CDC 帧 +2. `Com_Cdc_Func_ParseFrame()` 拿到 `Packet` +3. 根据 `Packet.type` 选择 protobuf 消息类型 +4. 对 `Packet.data` 做 `ParseFromArray()` + +伪代码: + +```cpp +Packet frame; +if (!Com_Cdc_Func_ParseFrame(frameBytes, &frame)) +{ + return false; +} + +if (frame.type == Com_Type_HelloRsp) +{ + keyboard::cdc::HelloRspPayload payload; + if (!payload.ParseFromArray(frame.data.constData(), frame.data.size())) + { + return false; + } +} +``` + +## 7. 下位机接入方式 + +下位机不要碰外层 CDC 帧格式,直接沿用: + +- 帧头 +- 长度 +- type +- checksum + +只把 `type` 对应的 `data` 改成 nanopb 编解码。 + +建议流程: + +1. 串口 ISR / DMA 收满一帧 +2. 校验 `AA 55 / len / checksum` +3. 根据 `type` 选 `keyboard.pb.h` 里的消息描述符 +4. `pb_decode()` 解 protobuf 载荷 +5. 业务处理 +6. 回复时 `pb_encode()` 出 payload,再挂回外层 CDC 帧 + +## 8. 推荐迁移顺序 + +建议按下面顺序,不要一次全换: + +1. 只提交 `.proto` 和 `.options` +2. 上位机先接 `HelloReq / HelloRsp` +3. 下位机把 `HelloReq / HelloRsp` 改成 nanopb +4. 确认握手通了,再迁移 `Ack / Error` +5. 然后迁移 `TimeSync / ThemeRgb` +6. 最后迁移 `Bitmap / FunctionKeyEvent` + +原因: + +- `HelloReq / HelloRsp` 字段简单,最适合先打通生成链路 +- `Bitmap` 有 `bytes[29]`,更适合在 `nanopb` 选项验证完后再切 + +## 9. 你当前 COM 文件还能起什么作用 + +即使引入 protobuf,当前 `COM` 层仍然非常有价值: + +- `Com_CdcEncode` + 继续负责外层帧打包 +- `Com_CdcDecode` + 继续负责流式拆包 +- `Packet_Type` + 继续作为外层快速分发 ID + +真正被 protobuf 替代的是: + +- 旧的 `data` 手写字段布局 +- 手工小端拼 payload 的代码 + +一句话: + +- `COM` 继续保留 +- 但 `COM` 不再定义业务 payload 字段 +- payload 交给 `.proto` + +## 10. 一套最小可复现命令 + +假设你已经装好 `protoc` 和 `nanopb`,最小步骤就是: + +```powershell +Set-Location C:\Users\lst\Desktop\动态链接库版本\20260320_new_keyboard + +New-Item -ItemType Directory -Force -Path KeyBorad\generated\cpp | Out-Null +protoc --proto_path=KeyBorad\proto --cpp_out=KeyBorad\generated\cpp KeyBorad\proto\keyboard.proto + +New-Item -ItemType Directory -Force -Path KeyBorad\generated\nanopb | Out-Null +python third_party\nanopb\generator\nanopb_generator.py --output-dir=KeyBorad\generated\nanopb --strip-path KeyBorad\proto\keyboard.proto +``` + +然后做两边接入: + +1. 上位机 `vcxproj` 加入 `keyboard.pb.cc` +2. 下位机工程加入 `keyboard.pb.c` +3. 把 `HelloReq / HelloRsp` 的 `data` 改为 protobuf payload +4. 把 `Com_CdcDecode` 的固定长度校验去掉 + +## 11. 最后一个关键建议 + +如果你只是想“共享结构定义”,那就: + +- 保留外层 `type` +- 每个 `type` 对应一个 protobuf message + +不要一开始就把外层 `type` 也塞进 protobuf `oneof` 里。 + +原因很简单: + +- 外层 `type` 对调试串口抓包很直观 +- 下位机分发也更省 +- 迁移成本最小 diff --git a/KeyBorad/proto/keyboard.options b/KeyBorad/proto/keyboard.options new file mode 100644 index 0000000..cee2d2d --- /dev/null +++ b/KeyBorad/proto/keyboard.options @@ -0,0 +1 @@ +BitmapPayload.usage_bitmap max_size:29 diff --git a/KeyBorad/proto/keyboard.proto b/KeyBorad/proto/keyboard.proto new file mode 100644 index 0000000..37794ae --- /dev/null +++ b/KeyBorad/proto/keyboard.proto @@ -0,0 +1,83 @@ +syntax = "proto3"; + +package keyboard.cdc; + +// The AA55 CDC frame still lives outside protobuf: +// [head1][head2][len][type][data][checksum] +// `data` is the serialized protobuf payload for the matching `type`. +// Keep the outer `type` field for fast dispatch and debugging. + +enum CdcPacketType { + CDC_PACKET_TYPE_UNKNOWN = 0; + CDC_PACKET_TYPE_HELLO_REQ = 1; + CDC_PACKET_TYPE_HELLO_RSP = 2; + CDC_PACKET_TYPE_BITMAP = 16; + CDC_PACKET_TYPE_FUNCTION_KEY_EVENT = 32; + CDC_PACKET_TYPE_LED_STATE = 33; + CDC_PACKET_TYPE_TIME_SYNC = 48; + CDC_PACKET_TYPE_THEME_RGB = 49; + CDC_PACKET_TYPE_ACK = 126; + CDC_PACKET_TYPE_ERROR = 127; +} + +enum KeyAction { + KEY_ACTION_RELEASE = 0; + KEY_ACTION_PRESS = 1; +} + +enum ErrorCode { + ERROR_CODE_NONE = 0; + ERROR_CODE_UNKNOWN_TYPE = 1; + ERROR_CODE_INVALID_LENGTH = 2; + ERROR_CODE_INVALID_PARAM = 3; + ERROR_CODE_NOT_READY = 4; +} + +message HelloReqPayload { + uint32 protocol_version = 1; +} + +message HelloRspPayload { + uint32 protocol_version = 1; + uint32 vendor_id = 2; + uint32 product_id = 3; + uint32 firmware_major = 4; + uint32 firmware_minor = 5; + uint32 capability_flags = 6; +} + +message BitmapPayload { + bytes usage_bitmap = 1; +} + +message FunctionKeyEventPayload { + uint32 usage = 1; + KeyAction action = 2; +} + +message LedStatePayload { + uint32 led_mask = 1; +} + +message TimeSyncPayload { + uint32 version = 1; + uint32 flags = 2; + sint32 timezone_min = 3; + fixed64 utc_ms = 4; + fixed32 accuracy_ms = 5; +} + +message ThemeRgbPayload { + uint32 red = 1; + uint32 green = 2; + uint32 blue = 3; +} + +message AckPayload { + uint32 acked_type = 1; +} + +message ErrorPayload { + uint32 error_type = 1; + ErrorCode error_code = 2; +} diff --git a/app.overlay b/app.overlay new file mode 100644 index 0000000..8b7a48f --- /dev/null +++ b/app.overlay @@ -0,0 +1,47 @@ +#include + +/ { + 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>; + }; +}; diff --git a/boards/atguigu/atguigu_keyboard_dongle/Kconfig.atguigu_keyboard_dongle b/boards/atguigu/atguigu_keyboard_dongle/Kconfig.atguigu_keyboard_dongle new file mode 100644 index 0000000..3c11697 --- /dev/null +++ b/boards/atguigu/atguigu_keyboard_dongle/Kconfig.atguigu_keyboard_dongle @@ -0,0 +1,2 @@ +config BOARD_ATGUIGU_KEYBOARD_DONGLE + select SOC_NRF52833_QDAA \ No newline at end of file diff --git a/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle-pinctrl.dtsi b/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle-pinctrl.dtsi new file mode 100644 index 0000000..eb124f5 --- /dev/null +++ b/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle-pinctrl.dtsi @@ -0,0 +1,2 @@ +&pinctrl { +}; diff --git a/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle.dts b/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle.dts new file mode 100644 index 0000000..e7cf05d --- /dev/null +++ b/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle.dts @@ -0,0 +1,42 @@ +/dts-v1/; +#include +#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)>; + }; + }; +}; \ No newline at end of file diff --git a/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle.yaml b/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle.yaml new file mode 100644 index 0000000..6862e75 --- /dev/null +++ b/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle.yaml @@ -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: [] \ No newline at end of file diff --git a/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle_defconfig b/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle_defconfig new file mode 100644 index 0000000..fe65101 --- /dev/null +++ b/boards/atguigu/atguigu_keyboard_dongle/atguigu_keyboard_dongle_defconfig @@ -0,0 +1,2 @@ +CONFIG_ARM_MPU=y +CONFIG_HW_STACK_PROTECTION=y \ No newline at end of file diff --git a/boards/atguigu/atguigu_keyboard_dongle/board.cmake b/boards/atguigu/atguigu_keyboard_dongle/board.cmake new file mode 100644 index 0000000..9998f8d --- /dev/null +++ b/boards/atguigu/atguigu_keyboard_dongle/board.cmake @@ -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) \ No newline at end of file diff --git a/boards/atguigu/atguigu_keyboard_dongle/board.yml b/boards/atguigu/atguigu_keyboard_dongle/board.yml new file mode 100644 index 0000000..d011983 --- /dev/null +++ b/boards/atguigu/atguigu_keyboard_dongle/board.yml @@ -0,0 +1,5 @@ +board: + name: atguigu_keyboard_dongle + vendor: atguigu + socs: + - name: nrf52833 \ No newline at end of file diff --git a/boards/atguigu/atguigu_keyboard_dongle/pre_dt_board.cmake b/boards/atguigu/atguigu_keyboard_dongle/pre_dt_board.cmake new file mode 100644 index 0000000..519d784 --- /dev/null +++ b/boards/atguigu/atguigu_keyboard_dongle/pre_dt_board.cmake @@ -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") diff --git a/boards/atguigu/atguigu_mini_keyboard/Kconfig.atguigu_mini_keyboard b/boards/atguigu/atguigu_mini_keyboard/Kconfig.atguigu_mini_keyboard new file mode 100644 index 0000000..59baf73 --- /dev/null +++ b/boards/atguigu/atguigu_mini_keyboard/Kconfig.atguigu_mini_keyboard @@ -0,0 +1,2 @@ +config BOARD_ATGUIGU_MINI_KEYBOARD + select SOC_NRF52840_QFAA \ No newline at end of file diff --git a/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard-pinctrl.dtsi b/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard-pinctrl.dtsi new file mode 100644 index 0000000..91829d8 --- /dev/null +++ b/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard-pinctrl.dtsi @@ -0,0 +1,73 @@ +&pinctrl { + qdec_default: qdec_default { + group1 { + psels = , + ; + bias-pull-up; + }; + }; + + qdec_sleep: qdec_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + led_spi_default: led_spi_default { + group1 { + psels = ; + }; + }; + + led_spi_sleep: led_spi_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; + + lcd_spi_default: lcd_spi_default { + group1 { + psels = , + ; + }; + }; + + lcd_spi_sleep: lcd_spi_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + i2c1_default: i2c1_default { + group1 { + psels = , + ; + bias-pull-up; + }; + }; + + i2c1_sleep: i2c1_sleep { + group1 { + psels = , + ; + low-power-enable; + }; + }; + + pwm0_default: pwm0_default { + group1 { + psels = ; + }; + }; + pwm0_sleep: pwm0_sleep { + group1 { + psels = ; + low-power-enable; + }; + }; +}; diff --git a/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.dts b/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.dts new file mode 100644 index 0000000..307ac9a --- /dev/null +++ b/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.dts @@ -0,0 +1,240 @@ +/dts-v1/; +#include +#include "atguigu_mini_keyboard-pinctrl.dtsi" +#include +#include + +/ { + 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 = ; + 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 = ; + zephyr,input-positive = ; + zephyr,resolution = <12>; + }; + + channel@7 { + reg = <7>; + zephyr,gain = "ADC_GAIN_1_6"; + zephyr,reference = "ADC_REF_INTERNAL"; + zephyr,acquisition-time = ; + zephyr,input-positive = ; + 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; +}; diff --git a/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.yaml b/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.yaml new file mode 100644 index 0000000..ab1619d --- /dev/null +++ b/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.yaml @@ -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: [] \ No newline at end of file diff --git a/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard_defconfig b/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard_defconfig new file mode 100644 index 0000000..fe65101 --- /dev/null +++ b/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard_defconfig @@ -0,0 +1,2 @@ +CONFIG_ARM_MPU=y +CONFIG_HW_STACK_PROTECTION=y \ No newline at end of file diff --git a/boards/atguigu/atguigu_mini_keyboard/board.cmake b/boards/atguigu/atguigu_mini_keyboard/board.cmake new file mode 100644 index 0000000..03e8860 --- /dev/null +++ b/boards/atguigu/atguigu_mini_keyboard/board.cmake @@ -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) \ No newline at end of file diff --git a/boards/atguigu/atguigu_mini_keyboard/board.yml b/boards/atguigu/atguigu_mini_keyboard/board.yml new file mode 100644 index 0000000..6b5c87e --- /dev/null +++ b/boards/atguigu/atguigu_mini_keyboard/board.yml @@ -0,0 +1,5 @@ +board: + name: atguigu_mini_keyboard + vendor: atguigu + socs: + - name: nrf52840 \ No newline at end of file diff --git a/boards/atguigu/atguigu_mini_keyboard/pre_dt_board.cmake b/boards/atguigu/atguigu_mini_keyboard/pre_dt_board.cmake new file mode 100644 index 0000000..519d784 --- /dev/null +++ b/boards/atguigu/atguigu_mini_keyboard/pre_dt_board.cmake @@ -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") diff --git a/docs/ble_bond_design_notes.md b/docs/ble_bond_design_notes.md new file mode 100644 index 0000000..0ae467f --- /dev/null +++ b/docs/ble_bond_design_notes.md @@ -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 和重连应被视为正常流程,而不是异常 + 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/docs/bluetooth_sig_ble_profiles_summary.md b/docs/bluetooth_sig_ble_profiles_summary.md new file mode 100644 index 0000000..50c48e4 --- /dev/null +++ b/docs/bluetooth_sig_ble_profiles_summary.md @@ -0,0 +1,314 @@ +# Bluetooth SIG 规格页中的 BLE Profile 汇总 + +更新时间:2026-04-01(Asia/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` 工程的实现关注点。 diff --git a/docs/lvgl_zephyr_porting_notes.md b/docs/lvgl_zephyr_porting_notes.md new file mode 100644 index 0000000..ff159f9 --- /dev/null +++ b/docs/lvgl_zephyr_porting_notes.md @@ -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` diff --git a/docs/ncs_v3_2_3_gatt_services_summary.md b/docs/ncs_v3_2_3_gatt_services_summary.md new file mode 100644 index 0000000..3e50b83 --- /dev/null +++ b/docs/ncs_v3_2_3_gatt_services_summary.md @@ -0,0 +1,926 @@ +# NCS v3.2.3 集成的 GATT Service 汇总 + +更新时间:2026-04-01(Asia/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` | `` | 对手表/手环输出通知类别与未读数 | +| Battery Service | `CONFIG_BT_BAS` | `` | 暴露电量、电池状态 | +| Current Time Service | `CONFIG_BT_CTS` | `` | 给客户端提供当前时间/校时 | +| Device Information Service | `CONFIG_BT_DIS` | `` | 暴露厂商、型号、版本、PnP ID 等静态信息 | +| Heart Rate Service | `CONFIG_BT_HRS` | `` | 心率设备 | +| Immediate Alert Service | `CONFIG_BT_IAS` | `` | 查找设备、远程触发蜂鸣/闪灯 | +| Nordic UART Service(Zephyr 版) | `CONFIG_BT_ZEPHYR_NUS` | `` | 简单双向串口透传 | +| Object Transfer Service | `CONFIG_BT_OTS` | `` | 面向对象的数据传输 | +| Tx Power Service | `CONFIG_BT_TPS` | 无独立公共 API 头 | 暴露当前发射功率 | + +### 2.2 NCS / Nordic 扩展服务 + +| Service | Kconfig | 头文件 | 典型用途 | +| --- | --- | --- | --- | +| Bond Management Service | `CONFIG_BT_BMS` | `` | 让对端触发删 bond | +| Continuous Glucose Monitoring Service | `CONFIG_BT_CGMS` | `` | 连续血糖监测 | +| Direction and Distance Finding Service | `CONFIG_BT_DDFS` | `` | 方位/距离测量结果输出 | +| Fast Pair Provider Service | `CONFIG_BT_FAST_PAIR` + `CONFIG_BT_FAST_PAIR_GATT_SERVICE` | `` | Google Fast Pair | +| Human Interface Device Service | `CONFIG_BT_HIDS` | `` | BLE 键盘、鼠标、输入设备 | +| Latency Service | `CONFIG_BT_LATENCY` | `` | 延迟测试/回环 | +| LED Button Service | `CONFIG_BT_LBS` | `` | 示例级 LED/Button 交互 | +| Memfault Diagnostic Service | `CONFIG_BT_MDS` | `` | Memfault 诊断数据导出 | +| Nordic Status Message Service | `CONFIG_BT_NSMS` | `` | 暴露一段可读状态文本 | +| Nordic UART Service(Nordic 旧版) | `CONFIG_BT_NUS` | `` | NCS 样例常用串口透传 | +| Ranging Service | `CONFIG_BT_RAS` + `CONFIG_BT_RAS_RRSP` | `` | Channel Sounding / Ranging 服务端 | +| Running Speed and Cadence Service | `CONFIG_BT_RSCS` | `` | 跑步速度步频 | +| Throughput Service | `CONFIG_BT_THROUGHPUT` | `` | 吞吐测试 | +| Wi-Fi Provisioning Service | `CONFIG_BT_WIFI_PROV` | `` | 通过 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, ¶m)` 加入对象池 +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 键盘路线,这是最关键的服务实现 +- 比起自定义 NUS,HIDS 才是操作系统原生识别键盘的正路 + +### 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 模块拆分方式。 diff --git a/docs/nordic_ncs_官方知识索引.md b/docs/nordic_ncs_官方知识索引.md new file mode 100644 index 0000000..8908040 --- /dev/null +++ b/docs/nordic_ncs_官方知识索引.md @@ -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) +- [设备树 HOWTO(NCS 镜像)](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) +- [自旋锁 API(NCS 镜像)](https://docs.nordicsemi.com/bundle/zephyr-apis-3.2.3/page/spinlock_8h.html) +- [时钟、超时与 uptime(NCS 镜像)](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 Next,NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/connectivity/usb/device_next/usb_device.html) +- [USB HID 设备 API(NCS 镜像)](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 字节序 API(NCS 镜像)](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`,因此已从本索引移除 diff --git a/docs/zephyr_官方知识索引.md b/docs/zephyr_官方知识索引.md new file mode 100644 index 0000000..291e869 --- /dev/null +++ b/docs/zephyr_官方知识索引.md @@ -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 通用能力,不索引项目私有设计本身 diff --git a/inc/buttons_def.h b/inc/buttons_def.h new file mode 100644 index 0000000..f6f8122 --- /dev/null +++ b/inc/buttons_def.h @@ -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 + +/* + * 该符号用于保证配置文件只被链接一次: + * 若被重复包含到多个编译单元,会在链接阶段报重复定义,避免静默错配。 + */ +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 }, +}; 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_keymap_def.h b/inc/hid_keymap_def.h new file mode 100644 index 0000000..a5d4f0b --- /dev/null +++ b/inc/hid_keymap_def.h @@ -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 + +/* + * 防止该定义文件被多处 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 */ +}; diff --git a/inc/hid_report_descriptor.h b/inc/hid_report_descriptor.h new file mode 100644 index 0000000..4d9b109 --- /dev/null +++ b/inc/hid_report_descriptor.h @@ -0,0 +1,161 @@ +#ifndef HID_REPORT_DESCRIPTOR_H_ +#define HID_REPORT_DESCRIPTOR_H_ + +#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 +#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 diff --git a/inc/led_state.h b/inc/led_state.h new file mode 100644 index 0000000..0ada0b8 --- /dev/null +++ b/inc/led_state.h @@ -0,0 +1,29 @@ +#ifndef NEW_KBD_LED_STATE_H__ +#define NEW_KBD_LED_STATE_H__ + +#include + +/* 模块内系统状态:只用于本项目的 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__ */ diff --git a/inc/led_state_def.h b/inc/led_state_def.h new file mode 100644 index 0000000..42732da --- /dev/null +++ b/inc/led_state_def.h @@ -0,0 +1,39 @@ +#include "led_state.h" +#include + +/* + * 该文件仅被 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 -> 0(Num Lock) + * - led_1 -> 1(BLE 状态) + */ +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)), +}; diff --git a/inc/settings_loader_def.h b/inc/settings_loader_def.h new file mode 100644 index 0000000..8d2080e --- /dev/null +++ b/inc/settings_loader_def.h @@ -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 + +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 +} diff --git a/inc/time_manager.h b/inc/time_manager.h new file mode 100644 index 0000000..0be0877 --- /dev/null +++ b/inc/time_manager.h @@ -0,0 +1,69 @@ +#ifndef TIME_MANAGER_H__ +#define TIME_MANAGER_H__ + +#include +#include + +#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); + +/* + * 获取当前时间快照: + * - 返回 0:snapshot 已填充; + * - 返回 -EINVAL:参数为空; + * - 返回 -EAGAIN:模块未 ready; + * - 返回 -ENODATA:当前开机周期尚未完成有效校时。 + */ +int time_manager_get_snapshot(struct time_manager_snapshot *snapshot); + +#ifdef __cplusplus +} +#endif + +#endif /* TIME_MANAGER_H__ */ diff --git a/inc/time_sync_protocol.h b/inc/time_sync_protocol.h new file mode 100644 index 0000000..f4c49fa --- /dev/null +++ b/inc/time_sync_protocol.h @@ -0,0 +1,33 @@ +#ifndef TIME_SYNC_PROTOCOL_H__ +#define TIME_SYNC_PROTOCOL_H__ + +#include + +#include + +/* + * 统一定义时间同步协议帧格式,方便 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__ */ diff --git a/modules/ip5306/CMakeLists.txt b/modules/ip5306/CMakeLists.txt new file mode 100644 index 0000000..fa23967 --- /dev/null +++ b/modules/ip5306/CMakeLists.txt @@ -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) diff --git a/modules/ip5306/Kconfig b/modules/ip5306/Kconfig new file mode 100644 index 0000000..293f118 --- /dev/null +++ b/modules/ip5306/Kconfig @@ -0,0 +1,5 @@ +menu "IP5306 Module" + +rsource "drivers/power/Kconfig" + +endmenu diff --git a/modules/ip5306/drivers/power/Kconfig b/modules/ip5306/drivers/power/Kconfig new file mode 100644 index 0000000..70e0a06 --- /dev/null +++ b/modules/ip5306/drivers/power/Kconfig @@ -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. diff --git a/modules/ip5306/drivers/power/ip5306.c b/modules/ip5306/drivers/power/ip5306.c new file mode 100644 index 0000000..ea78e66 --- /dev/null +++ b/modules/ip5306/drivers/power/ip5306.c @@ -0,0 +1,152 @@ +#include +#include +#include + +#include +#include +#include +#include + +#include "ip5306_priv.h" + +#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF) +#include +#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) diff --git a/modules/ip5306/drivers/power/ip5306_keepalive_nrf.c b/modules/ip5306/drivers/power/ip5306_keepalive_nrf.c new file mode 100644 index 0000000..446038d --- /dev/null +++ b/modules/ip5306/drivers/power/ip5306_keepalive_nrf.c @@ -0,0 +1,198 @@ +#include + +#include +#include +#include + +#include + +#include +#include +#include + +#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; +} diff --git a/modules/ip5306/drivers/power/ip5306_keepalive_sw.c b/modules/ip5306/drivers/power/ip5306_keepalive_sw.c new file mode 100644 index 0000000..37a62ea --- /dev/null +++ b/modules/ip5306/drivers/power/ip5306_keepalive_sw.c @@ -0,0 +1,54 @@ +#include +#include + +#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)); +} diff --git a/modules/ip5306/drivers/power/ip5306_priv.h b/modules/ip5306/drivers/power/ip5306_priv.h new file mode 100644 index 0000000..1b3d211 --- /dev/null +++ b/modules/ip5306/drivers/power/ip5306_priv.h @@ -0,0 +1,58 @@ +#ifndef IP5306_KEEPALIVE_PRIV_H_ +#define IP5306_KEEPALIVE_PRIV_H_ + +#include +#include + +#include +#include +#include +#include +#include + +#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF) +#include +#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_ */ diff --git a/modules/ip5306/dts/bindings/power/injoinic,ip5306.yaml b/modules/ip5306/dts/bindings/power/injoinic,ip5306.yaml new file mode 100644 index 0000000..ae7eb49 --- /dev/null +++ b/modules/ip5306/dts/bindings/power/injoinic,ip5306.yaml @@ -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. diff --git a/modules/ip5306/dts/bindings/vendor-prefixes.txt b/modules/ip5306/dts/bindings/vendor-prefixes.txt new file mode 100644 index 0000000..262ee04 --- /dev/null +++ b/modules/ip5306/dts/bindings/vendor-prefixes.txt @@ -0,0 +1 @@ +injoinic Injoinic diff --git a/modules/ip5306/include/zephyr/drivers/power/ip5306.h b/modules/ip5306/include/zephyr/drivers/power/ip5306.h new file mode 100644 index 0000000..97a75e4 --- /dev/null +++ b/modules/ip5306/include/zephyr/drivers/power/ip5306.h @@ -0,0 +1,48 @@ +#ifndef ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_ +#define ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_ + +#include +#include + +#include + +#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_ */ diff --git a/modules/ip5306/sysbuild/CMakeLists.txt b/modules/ip5306/sysbuild/CMakeLists.txt new file mode 100644 index 0000000..92967a5 --- /dev/null +++ b/modules/ip5306/sysbuild/CMakeLists.txt @@ -0,0 +1,24 @@ +function(ip5306_pre_image_cmake) + if(NOT CMAKE_HOST_WIN32) + return() + endif() + + cmake_parse_arguments(HOOK "" "IMAGE" "IMAGES" ${ARGN}) + + if(NOT DEFINED HOOK_IMAGE OR NOT TARGET ${HOOK_IMAGE}) + return() + endif() + + # Device Guard blocks arm-zephyr-eabi-ranlib.exe on this host. Build indexed + # archives in a single ar invocation so child images never need ranlib. + set(archive_create " qcs ") + set(archive_append " q ") + + foreach(lang C CXX ASM) + set_property(TARGET ${HOOK_IMAGE} APPEND PROPERTY CMAKE_ARGS + "-DCMAKE_${lang}_ARCHIVE_CREATE:STRING=${archive_create}" + "-DCMAKE_${lang}_ARCHIVE_APPEND:STRING=${archive_append}" + "-DCMAKE_${lang}_ARCHIVE_FINISH:STRING=" + ) + endforeach() +endfunction() diff --git a/modules/ip5306/zephyr/module.yml b/modules/ip5306/zephyr/module.yml new file mode 100644 index 0000000..e069a64 --- /dev/null +++ b/modules/ip5306/zephyr/module.yml @@ -0,0 +1,6 @@ +build: + cmake: . + kconfig: Kconfig + sysbuild-cmake: sysbuild + settings: + dts_root: . diff --git a/pm_static.yml b/pm_static.yml new file mode 100644 index 0000000..f85e5e7 --- /dev/null +++ b/pm_static.yml @@ -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 diff --git a/prj.conf b/prj.conf new file mode 100644 index 0000000..cf0536c --- /dev/null +++ b/prj.conf @@ -0,0 +1,119 @@ +CONFIG_CAF=y +CONFIG_HEAP_MEM_POOL_SIZE=2048 +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 diff --git a/src/events/battery_status_event.c b/src/events/battery_status_event.c new file mode 100644 index 0000000..278b1e3 --- /dev/null +++ b/src/events/battery_status_event.c @@ -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)); diff --git a/src/events/battery_status_event.h b/src/events/battery_status_event.h new file mode 100644 index 0000000..35c9b47 --- /dev/null +++ b/src/events/battery_status_event.h @@ -0,0 +1,61 @@ +#ifndef BATTERY_STATUS_EVENT_H +#define BATTERY_STATUS_EVENT_H + +#include +#include + +#include +#include + +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 diff --git a/src/events/config_event.c b/src/events/config_event.c new file mode 100644 index 0000000..ca0d367 --- /dev/null +++ b/src/events/config_event.c @@ -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)); diff --git a/src/events/config_event.h b/src/events/config_event.h new file mode 100644 index 0000000..06ed7ce --- /dev/null +++ b/src/events/config_event.h @@ -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 +#include + +#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__ */ 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_boot_event.c b/src/events/hid_boot_event.c new file mode 100644 index 0000000..2320d64 --- /dev/null +++ b/src/events/hid_boot_event.c @@ -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)); diff --git a/src/events/hid_boot_event.h b/src/events/hid_boot_event.h new file mode 100644 index 0000000..25f480c --- /dev/null +++ b/src/events/hid_boot_event.h @@ -0,0 +1,45 @@ +#ifndef HID_BOOT_EVENT_H__ +#define HID_BOOT_EVENT_H__ + +#include +#include +#include + +#include +#include + +/* + * 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__ */ 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_protocol_event.c b/src/events/hid_protocol_event.c new file mode 100644 index 0000000..6e973a0 --- /dev/null +++ b/src/events/hid_protocol_event.c @@ -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)); diff --git a/src/events/hid_protocol_event.h b/src/events/hid_protocol_event.h new file mode 100644 index 0000000..386d59d --- /dev/null +++ b/src/events/hid_protocol_event.h @@ -0,0 +1,49 @@ +#ifndef HID_PROTOCOL_EVENT_H__ +#define HID_PROTOCOL_EVENT_H__ + +#include + +#include +#include + +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__ */ diff --git a/src/events/hid_report_event.c b/src/events/hid_report_event.c new file mode 100644 index 0000000..0459194 --- /dev/null +++ b/src/events/hid_report_event.c @@ -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)); diff --git a/src/events/hid_report_event.h b/src/events/hid_report_event.h new file mode 100644 index 0000000..3c7930c --- /dev/null +++ b/src/events/hid_report_event.h @@ -0,0 +1,45 @@ +#ifndef HID_REPORT_EVENT_H__ +#define HID_REPORT_EVENT_H__ + +#include +#include +#include + +#include +#include + +/* + * 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__ */ diff --git a/src/events/hid_tx_done_event.c b/src/events/hid_tx_done_event.c new file mode 100644 index 0000000..1554302 --- /dev/null +++ b/src/events/hid_tx_done_event.c @@ -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)); diff --git a/src/events/hid_tx_done_event.h b/src/events/hid_tx_done_event.h new file mode 100644 index 0000000..e2f069f --- /dev/null +++ b/src/events/hid_tx_done_event.h @@ -0,0 +1,28 @@ +#ifndef HID_TX_DONE_EVENT_H__ +#define HID_TX_DONE_EVENT_H__ + +#include +#include + +#include +#include +#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__ */ diff --git a/src/events/hid_tx_event.c b/src/events/hid_tx_event.c new file mode 100644 index 0000000..917b60b --- /dev/null +++ b/src/events/hid_tx_event.c @@ -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)); diff --git a/src/events/hid_tx_event.h b/src/events/hid_tx_event.h new file mode 100644 index 0000000..5ec1fdb --- /dev/null +++ b/src/events/hid_tx_event.h @@ -0,0 +1,69 @@ +#ifndef HID_TX_EVENT_H__ +#define HID_TX_EVENT_H__ + +#include +#include +#include + +#include +#include + +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__ */ diff --git a/src/events/hid_vendor_mask_event.c b/src/events/hid_vendor_mask_event.c new file mode 100644 index 0000000..8009953 --- /dev/null +++ b/src/events/hid_vendor_mask_event.c @@ -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)); diff --git a/src/events/hid_vendor_mask_event.h b/src/events/hid_vendor_mask_event.h new file mode 100644 index 0000000..66f4b3f --- /dev/null +++ b/src/events/hid_vendor_mask_event.h @@ -0,0 +1,39 @@ +#ifndef HID_VENDOR_MASK_EVENT_H__ +#define HID_VENDOR_MASK_EVENT_H__ + +#include +#include +#include + +#include +#include + +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__ */ diff --git a/src/events/keyboard_led_event.c b/src/events/keyboard_led_event.c new file mode 100644 index 0000000..f4797fb --- /dev/null +++ b/src/events/keyboard_led_event.c @@ -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)); diff --git a/src/events/keyboard_led_event.h b/src/events/keyboard_led_event.h new file mode 100644 index 0000000..ff77591 --- /dev/null +++ b/src/events/keyboard_led_event.h @@ -0,0 +1,73 @@ +#ifndef KEYBOARD_LED_EVENT_H__ +#define KEYBOARD_LED_EVENT_H__ + +#include +#include + +#include +#include + +/* + * 键盘 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__ */ diff --git a/src/events/mode_event.c b/src/events/mode_event.c new file mode 100644 index 0000000..f983f8e --- /dev/null +++ b/src/events/mode_event.c @@ -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)); diff --git a/src/events/mode_event.h b/src/events/mode_event.h new file mode 100644 index 0000000..ac10df7 --- /dev/null +++ b/src/events/mode_event.h @@ -0,0 +1,53 @@ +#ifndef MODE_EVENT_H +#define MODE_EVENT_H + +#include + +#include +#include + +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 diff --git a/src/events/qdec_step_event.c b/src/events/qdec_step_event.c new file mode 100644 index 0000000..9a342d7 --- /dev/null +++ b/src/events/qdec_step_event.c @@ -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)); diff --git a/src/events/qdec_step_event.h b/src/events/qdec_step_event.h new file mode 100644 index 0000000..6c6a3c6 --- /dev/null +++ b/src/events/qdec_step_event.h @@ -0,0 +1,36 @@ +#ifndef QDEC_STEP_EVENT_H__ +#define QDEC_STEP_EVENT_H__ + +#include + +#include +#include + +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__ */ diff --git a/src/events/time_sync_event.c b/src/events/time_sync_event.c new file mode 100644 index 0000000..6477e21 --- /dev/null +++ b/src/events/time_sync_event.c @@ -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)); diff --git a/src/events/time_sync_event.h b/src/events/time_sync_event.h new file mode 100644 index 0000000..fc17439 --- /dev/null +++ b/src/events/time_sync_event.h @@ -0,0 +1,35 @@ +#ifndef TIME_SYNC_EVENT_H__ +#define TIME_SYNC_EVENT_H__ + +#include +#include + +#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__ */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..f162000 --- /dev/null +++ b/src/main.c @@ -0,0 +1,18 @@ +#include + +#define MODULE main +#include + +#include +LOG_MODULE_REGISTER(MODULE); + +int main(void) +{ + if (app_event_manager_init()) { + LOG_ERR("Application Event Manager not initialized"); + } else { + module_set_state(MODULE_STATE_READY); + } + + return 0; +} diff --git a/src/modules/battery_module.c b/src/modules/battery_module.c new file mode 100644 index 0000000..362ba14 --- /dev/null +++ b/src/modules/battery_module.c @@ -0,0 +1,392 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define MODULE battery +#include + +#include "battery_status_event.h" + +#include +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); diff --git a/src/modules/ble_adv_ctrl_module.c b/src/modules/ble_adv_ctrl_module.c new file mode 100644 index 0000000..3e19b0b --- /dev/null +++ b/src/modules/ble_adv_ctrl_module.c @@ -0,0 +1,85 @@ +#include + +#define MODULE ble_adv_ctrl +#include +#include + +#include "mode_event.h" + +#include +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); + diff --git a/src/modules/ble_battery_module.c b/src/modules/ble_battery_module.c new file mode 100644 index 0000000..1fdeb15 --- /dev/null +++ b/src/modules/ble_battery_module.c @@ -0,0 +1,51 @@ +#include + +#include + +#define MODULE ble_battery +#include + +#include "battery_status_event.h" + +#include +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); diff --git a/src/modules/ble_bond_module.c b/src/modules/ble_bond_module.c new file mode 100644 index 0000000..6b54959 --- /dev/null +++ b/src/modules/ble_bond_module.c @@ -0,0 +1,583 @@ +#include +#include +#include +#include + +#include + +#define MODULE ble_bond +#include +#include +#include + +#include "config_event.h" + +#include +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); diff --git a/src/modules/ble_hid_module.c b/src/modules/ble_hid_module.c new file mode 100644 index 0000000..9281470 --- /dev/null +++ b/src/modules/ble_hid_module.c @@ -0,0 +1,404 @@ +#include +#include + +#include + +#define MODULE ble_hid +#include +#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" +#include "hid_vendor_mask_event.h" +#include "keyboard_led_event.h" +#include "mode_event.h" + +#include +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); diff --git a/src/modules/ble_slot_ctrl_module.c b/src/modules/ble_slot_ctrl_module.c new file mode 100644 index 0000000..502ec33 --- /dev/null +++ b/src/modules/ble_slot_ctrl_module.c @@ -0,0 +1,281 @@ +#include +#include + +#include + +#define MODULE ble_slot_ctrl +#include +#include +#include +#include + +#include "config_event.h" +#include "mode_event.h" + +#include +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); diff --git a/src/modules/ble_time_sync_module.c b/src/modules/ble_time_sync_module.c new file mode 100644 index 0000000..b5b4e7c --- /dev/null +++ b/src/modules/ble_time_sync_module.c @@ -0,0 +1,175 @@ +#include + +#include +#include + +#include + +#define MODULE ble_time_sync +#include + +#include "time_manager.h" +#include "time_sync_event.h" +#include "time_sync_protocol.h" + +#include +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); diff --git a/src/modules/display_module.c b/src/modules/display_module.c new file mode 100644 index 0000000..f85684b --- /dev/null +++ b/src/modules/display_module.c @@ -0,0 +1,549 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define MODULE display +#include +#include +#include + +#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 +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); 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 new file mode 100644 index 0000000..d1f5ed1 --- /dev/null +++ b/src/modules/hid_tx_manager_module.c @@ -0,0 +1,313 @@ +#include + +#include +#include + +#include + +#define MODULE hid_tx_manager +#include + +#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 +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); diff --git a/src/modules/keyboard_module.c b/src/modules/keyboard_module.c new file mode 100644 index 0000000..0fef737 --- /dev/null +++ b/src/modules/keyboard_module.c @@ -0,0 +1,440 @@ +#include +#include + +#include + +#define MODULE keyboard +#include + +#include +#include + +#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 +#include + +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); diff --git a/src/modules/led_state_module.c b/src/modules/led_state_module.c new file mode 100644 index 0000000..6399b40 --- /dev/null +++ b/src/modules/led_state_module.c @@ -0,0 +1,180 @@ +#include + +#include + +#define MODULE led_state +#include +#include +#include + +#include "keyboard_led_event.h" +#include "led_state.h" +#include "mode_event.h" +#include "led_state_def.h" + +#include +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 diff --git a/src/modules/mode_switch_module.c b/src/modules/mode_switch_module.c new file mode 100644 index 0000000..7f27bbe --- /dev/null +++ b/src/modules/mode_switch_module.c @@ -0,0 +1,219 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define MODULE mode_switch +#include + +#include "mode_event.h" + +#include +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); diff --git a/src/modules/qdec_module.c b/src/modules/qdec_module.c new file mode 100644 index 0000000..0ff4bdc --- /dev/null +++ b/src/modules/qdec_module.c @@ -0,0 +1,243 @@ +#include + +#include +#include +#include +#include +#include + +#include +#include + +#define MODULE qdec +#include + +#include "qdec_step_event.h" + +#include +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); diff --git a/src/modules/time_manager_module.c b/src/modules/time_manager_module.c new file mode 100644 index 0000000..ea05d0d --- /dev/null +++ b/src/modules/time_manager_module.c @@ -0,0 +1,374 @@ +#include +#include + +#include +#include +#include + +#include + +#define MODULE time_manager +#include +#include + +#include "time_manager.h" +#include "time_sync_event.h" + +#include +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); diff --git a/src/modules/usb_hid_module.c b/src/modules/usb_hid_module.c new file mode 100644 index 0000000..62663bc --- /dev/null +++ b/src/modules/usb_hid_module.c @@ -0,0 +1,925 @@ +#include +#include + +#include +#include +#include +#include + +#include +#include + +#define MODULE usb_hid +#include + +#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 +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_event(USB/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); diff --git a/src/ui/display_ui.c b/src/ui/display_ui.c new file mode 100644 index 0000000..b8573e7 --- /dev/null +++ b/src/ui/display_ui.c @@ -0,0 +1,306 @@ +#include + +#include +#include + +#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); +} diff --git a/src/ui/display_ui.h b/src/ui/display_ui.h new file mode 100644 index 0000000..5d01712 --- /dev/null +++ b/src/ui/display_ui.h @@ -0,0 +1,34 @@ +#ifndef DISPLAY_UI_H +#define DISPLAY_UI_H + +#include + +#include + +#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 */ diff --git a/src/ui/fonts/ui_font_keyboard_small_18.c b/src/ui/fonts/ui_font_keyboard_small_18.c new file mode 100644 index 0000000..23aa9f7 --- /dev/null +++ b/src/ui/fonts/ui_font_keyboard_small_18.c @@ -0,0 +1,379 @@ +/******************************************************************************* + * Size: 18 px + * Bpp: 4 + * Opts: --format lvgl --output E:\projects\lvgl\lv_port_pc_vscode\src\ui\fonts\ui_font_keyboard_small_18.c --size 18 --bpp 4 --lv-font-name ui_font_keyboard_small_18 --font E:\projects\lvgl\lv_port_pc_vscode\external\ttf\MapleMono-NF-CN-Medium.ttf --range 0x30-0x39 --symbols AS/% --font E:\projects\lvgl\lv_port_pc_vscode\lvgl\scripts\built_in_font\FontAwesome5-Solid+Brands+Regular.woff --range 0xF0E7-0xF0E7 --range 0xF240-0xF240 --range 0xF241-0xF241 --range 0xF242-0xF242 --range 0xF243-0xF243 --range 0xF244-0xF244 --range 0xF1E6-0xF1E6 --range 0xF287-0xF287 --range 0xF293-0xF293 + ******************************************************************************/ + +#ifdef LV_LVGL_H_INCLUDE_SIMPLE +#include "lvgl.h" +#else +#include "lvgl/lvgl.h" +#endif + +#ifndef UI_FONT_KEYBOARD_SMALL_18 +#define UI_FONT_KEYBOARD_SMALL_18 1 +#endif + +#if UI_FONT_KEYBOARD_SMALL_18 + +/*----------------- + * BITMAPS + *----------------*/ + +/*Store the image of the glyphs*/ +static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { + /* U+0025 "%" */ + 0x6, 0xef, 0xb1, 0x0, 0x90, 0xe, 0x40, 0x92, + 0xc0, 0x17, 0x63, 0xb0, 0xdd, 0x1a, 0x3, 0x22, + 0xc, 0x3, 0xc5, 0x23, 0x41, 0x61, 0xb8, 0x69, + 0x41, 0x42, 0x7, 0x20, 0x69, 0x71, 0xa8, 0x1, + 0x37, 0x7d, 0x8b, 0xcc, 0x84, 0x2, 0x3d, 0x39, + 0x84, 0x6d, 0x0, 0xb8, 0x9d, 0x8f, 0xd4, 0xcc, + 0xe, 0x4a, 0xc0, 0x81, 0xe0, 0xc3, 0x1, 0x66, + 0x4, 0x4, 0x2, 0xc1, 0x2, 0x23, 0x5c, 0x81, + 0x47, 0xb7, 0x0, 0x69, 0x98, 0xb4, 0x0, + + /* U+002F "/" */ + 0x0, 0xf4, 0x79, 0x0, 0x79, 0x80, 0x80, 0x39, + 0x45, 0x40, 0x3d, 0xc1, 0xc0, 0x1c, 0x28, 0x48, + 0x1, 0xcc, 0xa, 0x1, 0xea, 0xa, 0x0, 0xe3, + 0x23, 0x20, 0xe, 0xa0, 0xa0, 0xf, 0x38, 0x30, + 0x7, 0x28, 0xa8, 0x7, 0xb8, 0x38, 0x3, 0x85, + 0x9, 0x0, 0x39, 0x81, 0x40, 0x3d, 0x41, 0x40, + 0x1c, 0x64, 0x64, 0x1, 0xcc, 0x12, 0x1, 0xe0, + + /* U+0030 "0" */ + 0x0, 0x46, 0xfe, 0xb0, 0x5, 0x4e, 0x45, 0x4c, + 0x1, 0xa8, 0xf6, 0xe0, 0x38, 0xd0, 0x40, 0x81, + 0x98, 0x1d, 0x41, 0x0, 0x14, 0x61, 0xe2, 0x4, + 0x38, 0xa2, 0x6, 0x61, 0x2f, 0x33, 0x68, 0x9, + 0x8e, 0x8b, 0x60, 0x80, 0x88, 0x1, 0x12, 0xe, + 0x6, 0xa0, 0xee, 0x0, 0x78, 0x65, 0x3, 0x8, + 0x1a, 0x82, 0x1a, 0x8f, 0x6e, 0x4, 0x88, 0x53, + 0x89, 0x12, 0x58, 0x0, + + /* U+0031 "1" */ + 0x0, 0x1e, 0x7c, 0x80, 0x66, 0xc3, 0x7, 0x0, + 0x92, 0x42, 0x80, 0x40, 0x31, 0xea, 0x88, 0x6, + 0x4c, 0x20, 0xf, 0xff, 0x82, 0x66, 0x42, 0x19, + 0x89, 0x23, 0x38, 0x0, 0x66, 0x50, + + /* U+0032 "2" */ + 0x7, 0xdf, 0xf6, 0xc8, 0x82, 0x41, 0x10, 0xc9, + 0xb4, 0x12, 0x37, 0x59, 0xc4, 0x68, 0xe, 0x40, + 0x1, 0xa0, 0x20, 0xf, 0x70, 0x10, 0x7, 0x13, + 0xa, 0x0, 0x61, 0xf0, 0xb0, 0xc, 0x38, 0x50, + 0xa0, 0x10, 0xe1, 0x3b, 0x80, 0x21, 0xc2, 0x78, + 0x0, 0x87, 0x9, 0xe0, 0x3, 0x59, 0x2, 0xe6, + 0x66, 0xd0, 0x1, 0x19, 0xef, + + /* U+0033 "3" */ + 0x1a, 0xef, 0xf5, 0xa0, 0x1, 0x54, 0x6, 0x29, + 0x6a, 0x9, 0x9d, 0x9d, 0x21, 0x20, 0x3, 0x0, + 0x98, 0x4, 0x3, 0x86, 0x41, 0x0, 0x3, 0x9b, + 0xcc, 0xb2, 0x1, 0x19, 0x0, 0x15, 0x80, 0x3, + 0xff, 0x50, 0xc9, 0x0, 0x72, 0xc8, 0x20, 0x7, + 0xbc, 0x0, 0x28, 0x1, 0x13, 0x2, 0xd5, 0xfe, + 0x6e, 0x85, 0x4e, 0x14, 0x4c, 0x8e, 0x68, 0x0, + + /* U+0034 "4" */ + 0x0, 0xed, 0xe1, 0x0, 0xf4, 0x90, 0xa8, 0x7, + 0x1b, 0x0, 0x7e, 0xe1, 0x30, 0xf, 0x41, 0x43, + 0x0, 0x71, 0xb9, 0xb0, 0x7, 0xbc, 0x3c, 0x3, + 0xce, 0x6c, 0x60, 0x1e, 0x80, 0x4c, 0xc2, 0x4, + 0xd0, 0x20, 0x11, 0x98, 0x40, 0x90, 0x2f, 0xff, + 0x28, 0x5f, 0x0, 0x7f, 0xf1, 0x34, 0x18, 0x0, + + /* U+0035 "5" */ + 0xb, 0xff, 0xf0, 0x82, 0x1, 0x19, 0xc8, 0x22, + 0x2, 0xdc, 0xca, 0xc0, 0x21, 0x0, 0xfc, 0x20, + 0x1e, 0x10, 0x2f, 0xf6, 0xb8, 0x5, 0x8, 0x62, + 0x51, 0x60, 0x7, 0xbc, 0xec, 0x14, 0x30, 0xe, + 0x39, 0x5, 0x0, 0xf7, 0x80, 0x5, 0x0, 0x22, + 0x60, 0x5a, 0xbf, 0xcd, 0xd0, 0xa9, 0xc2, 0x89, + 0x91, 0x22, 0x80, + + /* U+0036 "6" */ + 0x0, 0x86, 0xbd, 0x0, 0x30, 0xe2, 0xc2, 0x0, + 0x6d, 0x2b, 0x70, 0xc, 0xe7, 0x28, 0x1, 0xd0, + 0x88, 0x78, 0x40, 0x3, 0x0, 0x36, 0x1e, 0xe8, + 0x34, 0x1b, 0x3f, 0x9, 0x51, 0x83, 0x8c, 0xe, + 0x3, 0x4c, 0xc, 0x2, 0x70, 0x23, 0x0, 0xe1, + 0x1, 0x70, 0x90, 0xa, 0x3, 0x20, 0x1b, 0xb3, + 0xd0, 0xd8, 0xb5, 0x40, 0xc5, 0xf0, 0x0, + + /* U+0037 "7" */ + 0xdf, 0xff, 0x91, 0x6, 0x7c, 0x0, 0xc9, 0xcc, + 0xe3, 0xb, 0x0, 0xe2, 0x33, 0x80, 0x3a, 0xc2, + 0x80, 0x3c, 0xc0, 0xc0, 0x1c, 0xc0, 0xc0, 0x1e, + 0xb0, 0xb0, 0xe, 0x42, 0x42, 0x0, 0xee, 0xe, + 0x0, 0xe1, 0x51, 0x50, 0xe, 0x90, 0x60, 0xf, + 0x10, 0xc8, 0x6, + + /* U+0038 "8" */ + 0x1, 0xae, 0xfe, 0x80, 0x0, 0xfa, 0x81, 0x8b, + 0xe0, 0x30, 0xaf, 0x67, 0x91, 0xa7, 0x87, 0x80, + 0x48, 0x0, 0xb0, 0x92, 0x3, 0x71, 0x43, 0xb1, + 0xdf, 0xc2, 0xd0, 0x6, 0x90, 0x98, 0x1a, 0x3, + 0xb3, 0x3b, 0x39, 0x21, 0x28, 0x24, 0x0, 0x3e, + 0x1a, 0x60, 0x1c, 0xe0, 0x68, 0x12, 0x1, 0x40, + 0x6c, 0xb, 0x76, 0x7a, 0x13, 0x96, 0x28, 0x18, + 0xb6, 0x0, + + /* U+0039 "9" */ + 0x2, 0xae, 0xfe, 0x91, 0x2, 0xd5, 0x33, 0x3, + 0x60, 0x40, 0x46, 0xe7, 0x21, 0x33, 0x83, 0x80, + 0x50, 0x18, 0x1, 0xe1, 0x1, 0x60, 0x50, 0xa, + 0x0, 0x7c, 0x2a, 0xe7, 0x50, 0x35, 0x28, 0x91, + 0xd5, 0x1, 0x1, 0x77, 0xfa, 0x94, 0xcc, 0x1, + 0xdc, 0x10, 0x1, 0xd4, 0x6c, 0x80, 0x10, 0xda, + 0xa4, 0x80, 0x66, 0x47, 0xb0, 0x8, + + /* U+0041 "A" */ + 0x0, 0xd7, 0xf0, 0x1, 0xf1, 0xa0, 0x38, 0x7, + 0xc8, 0x8, 0x8, 0x1, 0xeb, 0x8, 0xd, 0x0, + 0xe1, 0x34, 0x31, 0x40, 0xe, 0x40, 0xc0, 0x42, + 0x20, 0x6, 0xc0, 0x40, 0xc0, 0x40, 0xc, 0xe2, + 0x60, 0x81, 0x80, 0x11, 0x8, 0xbf, 0xda, 0xa, + 0x1, 0x20, 0x23, 0xbc, 0x60, 0x60, 0xd, 0x9, + 0x88, 0xa4, 0x2c, 0x0, 0x84, 0x40, 0x9, 0x1, + 0x0, 0x41, 0x40, 0x30, 0xa0, 0x80, + + /* U+0053 "S" */ + 0x1, 0x9e, 0xfd, 0x70, 0xb, 0x18, 0xc, 0xa2, + 0x81, 0x49, 0x3b, 0x38, 0x94, 0x48, 0x34, 0x0, + 0x3f, 0x22, 0x41, 0xa0, 0x11, 0x30, 0x29, 0xa7, + 0x4a, 0x80, 0x6c, 0x83, 0x6a, 0xe4, 0x0, 0x9f, + 0x75, 0x3, 0x64, 0x1, 0x89, 0xe4, 0x29, 0xa8, + 0x3, 0x10, 0xf, 0xac, 0x80, 0x5c, 0x1f, 0x60, + 0xdd, 0x9e, 0xa4, 0xc5, 0xaa, 0x6, 0x2d, 0x80, + + /* U+F0E7 "" */ + 0x0, 0x57, 0xff, 0x50, 0x7, 0x94, 0x3, 0x84, + 0x3, 0x84, 0x3, 0xda, 0x1, 0xc4, 0x1, 0xe7, + 0x0, 0xe6, 0x0, 0xe4, 0x10, 0xe, 0xd0, 0xe, + 0x15, 0x59, 0x0, 0x4, 0x1, 0xcb, 0x55, 0x59, + 0x83, 0x0, 0x7f, 0x19, 0x80, 0x80, 0x3f, 0xbc, + 0x3, 0xfe, 0x53, 0x0, 0x7e, 0xed, 0x80, 0x1a, + 0xc0, 0x21, 0x22, 0x9c, 0x2, 0x81, 0x0, 0xfb, + 0x0, 0x2, 0xe0, 0x1f, 0x90, 0x1, 0x60, 0x1f, + 0xc6, 0x6, 0xa0, 0x1f, 0x8c, 0x1, 0xe0, 0x1f, + 0xc8, 0xc, 0x60, 0x1f, 0xe1, 0x80, 0xf, 0xf3, + 0xf0, 0x80, 0x78, + + /* U+F1E6 "" */ + 0x0, 0x25, 0x10, 0x6, 0x79, 0x0, 0xef, 0x55, + 0x0, 0x68, 0x63, 0x0, 0xce, 0x1e, 0x1, 0xe7, + 0x0, 0xff, 0xe9, 0x9, 0x4c, 0x61, 0x16, 0x88, + 0x38, 0x87, 0x6d, 0x45, 0xee, 0xe8, 0x86, 0x73, + 0x80, 0x7f, 0xf0, 0xb8, 0x40, 0x3f, 0xd0, 0xe2, + 0x60, 0x1f, 0xe2, 0x0, 0x30, 0x7, 0xf9, 0x80, + 0x1a, 0x1, 0xfc, 0x26, 0x0, 0x63, 0x0, 0xfd, + 0x60, 0x1b, 0x4c, 0x3, 0xd2, 0xa0, 0x18, 0x72, + 0x4c, 0x0, 0xda, 0xc0, 0x1f, 0x35, 0x0, 0xc9, + 0x0, 0x7f, 0xf6, 0x5e, 0xf4, 0x3, 0xc0, + + /* U+F240 "" */ + 0x4, 0x4f, 0xfe, 0x38, 0x80, 0x2a, 0xef, 0xff, + 0x8f, 0xe2, 0xa, 0x1, 0xff, 0xc7, 0x14, 0x0, + 0xaf, 0xff, 0xff, 0x88, 0x0, 0xa3, 0x0, 0xab, + 0xff, 0xff, 0x84, 0x1, 0x98, 0x3, 0xff, 0x8f, + 0x80, 0x1f, 0xfc, 0xa3, 0x10, 0xf, 0xfe, 0x4c, + 0x8, 0x7, 0x7b, 0xbf, 0xff, 0x8, 0x1c, 0x3, + 0x8c, 0x73, 0x3f, 0xf8, 0x4a, 0x0, 0x34, 0x0, + 0x45, 0xdf, 0xff, 0xe, 0x80, 0x12, 0x32, 0x20, + 0x1f, 0xfc, 0x64, 0x50, + + /* U+F241 "" */ + 0x4, 0x4f, 0xfe, 0x38, 0x80, 0x2a, 0xef, 0xff, + 0x8f, 0xe2, 0xa, 0x1, 0xff, 0xc7, 0x14, 0x0, + 0xaf, 0xff, 0xff, 0x88, 0x0, 0xa3, 0x0, 0xb7, + 0xff, 0xfc, 0xc0, 0x1f, 0x30, 0x7, 0xff, 0x1f, + 0x0, 0x3f, 0xf9, 0x46, 0x20, 0x1f, 0xfc, 0x98, + 0x10, 0xe, 0xb7, 0x7f, 0xf2, 0x80, 0x67, 0x0, + 0xe3, 0x2c, 0xcf, 0xf3, 0xa2, 0x4a, 0x0, 0x34, + 0x0, 0x45, 0xdf, 0xff, 0xe, 0x80, 0x12, 0x32, + 0x20, 0x1f, 0xfc, 0x64, 0x50, + + /* U+F242 "" */ + 0x4, 0x4f, 0xfe, 0x38, 0x80, 0x2a, 0xef, 0xff, + 0x8f, 0xe2, 0xa, 0x1, 0xff, 0xc7, 0x14, 0x0, + 0xaf, 0xff, 0xff, 0x88, 0x0, 0xa3, 0x0, 0xb3, + 0xff, 0xe1, 0x0, 0xff, 0x30, 0x7, 0xff, 0x1f, + 0x0, 0x3f, 0xf9, 0x46, 0x20, 0x1f, 0xfc, 0x98, + 0x10, 0xe, 0xa7, 0x7f, 0x84, 0x3, 0xe7, 0x0, + 0xe3, 0x2c, 0xcf, 0x91, 0x3e, 0x50, 0x1, 0xa0, + 0x2, 0x2e, 0xff, 0xf8, 0x74, 0x0, 0x91, 0x91, + 0x0, 0xff, 0xe3, 0x22, 0x80, + + /* U+F243 "" */ + 0x4, 0x4f, 0xfe, 0x38, 0x80, 0x2a, 0xef, 0xff, + 0x8f, 0xe2, 0xa, 0x1, 0xff, 0xc7, 0x14, 0x0, + 0xaf, 0xff, 0xff, 0x88, 0x0, 0xa3, 0x0, 0xa7, + 0xfe, 0xe0, 0xf, 0xfe, 0x13, 0x0, 0x7f, 0xf1, + 0xf0, 0x3, 0xff, 0x94, 0x62, 0x1, 0xff, 0xc9, + 0x81, 0x0, 0xed, 0x77, 0xa4, 0x3, 0xfe, 0x70, + 0xe, 0x30, 0xcc, 0x8d, 0x13, 0xfc, 0xa0, 0x3, + 0x40, 0x4, 0x5d, 0xff, 0xf0, 0xe8, 0x1, 0x23, + 0x22, 0x1, 0xff, 0xc6, 0x45, 0x0, + + /* U+F244 "" */ + 0x4, 0x4f, 0xfe, 0x38, 0x80, 0x2a, 0xef, 0xff, + 0x8f, 0xe2, 0xa, 0x1, 0xff, 0xc7, 0x14, 0x0, + 0xaf, 0xff, 0xff, 0x88, 0x0, 0xa3, 0x0, 0xff, + 0xe5, 0x30, 0x7, 0xff, 0x1f, 0x0, 0x3f, 0xf9, + 0x46, 0x20, 0x1f, 0xfc, 0x98, 0x10, 0xf, 0xfe, + 0x4b, 0x80, 0x71, 0xa2, 0x7f, 0xf0, 0xd4, 0x0, + 0x68, 0x0, 0x8b, 0xbf, 0xfe, 0x1d, 0x0, 0x24, + 0x64, 0x40, 0x3f, 0xf8, 0xc8, 0xa0, + + /* U+F287 "" */ + 0x0, 0xff, 0xe0, 0x3c, 0x8, 0x7, 0xff, 0x14, + 0x9a, 0x61, 0xf0, 0x3, 0xff, 0x88, 0x9b, 0xc, + 0x0, 0x20, 0xf, 0xfe, 0x25, 0x3f, 0x68, 0x84, + 0x0, 0x7f, 0x88, 0x3, 0x30, 0x40, 0x17, 0x7b, + 0x80, 0x7e, 0x3e, 0xde, 0x30, 0x5, 0xc8, 0x7, + 0xf1, 0x50, 0x80, 0x3c, 0x40, 0x79, 0x24, 0x88, + 0xab, 0xff, 0x73, 0x81, 0x80, 0x45, 0x6c, 0x15, + 0x5f, 0xe8, 0x0, 0x42, 0xa8, 0x2, 0x5f, 0xfc, + 0x88, 0xff, 0xfb, 0x1, 0x35, 0x69, 0x8d, 0xa8, + 0x3, 0x42, 0x18, 0x7, 0x8a, 0xac, 0x80, 0x13, + 0x92, 0x1, 0xc6, 0x90, 0x5, 0x77, 0x28, 0xa8, + 0x7, 0xff, 0x6, 0x52, 0x6d, 0x12, 0xd0, 0xf, + 0xfe, 0x19, 0x61, 0x20, 0x7, 0xff, 0x20, 0xef, + 0x80, 0x3f, 0xf9, 0x47, 0xff, 0x40, 0x7, 0x0, + + /* U+F293 "" */ + 0x0, 0x85, 0xef, 0x75, 0x70, 0x40, 0x1c, 0xbd, + 0x8, 0x4, 0x8f, 0xac, 0x1, 0x25, 0x0, 0x56, + 0xe0, 0x14, 0xa0, 0x2, 0x80, 0x3a, 0x1c, 0x2, + 0x90, 0x41, 0x0, 0xf4, 0x30, 0x0, 0x8f, 0x0, + 0x12, 0x80, 0x1, 0xc9, 0x60, 0x2, 0x18, 0x5, + 0x68, 0x2, 0x44, 0xc0, 0x7, 0x30, 0x2, 0x56, + 0xd0, 0x34, 0x68, 0x0, 0x46, 0x1, 0x52, 0x90, + 0xe, 0x80, 0x4e, 0x1, 0xd2, 0x0, 0x51, 0x0, + 0xc6, 0x1, 0x16, 0x80, 0x1d, 0x80, 0x30, 0x80, + 0xf, 0x9, 0x42, 0x64, 0xa0, 0x7, 0x60, 0x7, + 0x2e, 0x18, 0x31, 0xc0, 0x0, 0xb0, 0x1, 0xf6, + 0x2, 0x14, 0x38, 0x0, 0xf7, 0x0, 0x10, 0x6, + 0x7c, 0x10, 0x3, 0x8b, 0x0, 0x71, 0x60, 0x80, + 0x18, 0x42, 0x50, 0x2, 0xdc, 0x10, 0x1, 0x40, + 0x5, 0x74, 0x60, 0xa2, 0x4, 0xfa, 0x20, 0x19, + 0x73, 0xfb, 0x9f, 0xb0, 0x1, 0x0 +}; + + +/*--------------------- + * GLYPH DESCRIPTION + *--------------------*/ + +static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { + {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, + {.bitmap_index = 0, .adv_w = 173, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 71, .adv_w = 173, .box_w = 9, .box_h = 17, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 127, .adv_w = 173, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 187, .adv_w = 173, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 217, .adv_w = 173, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 270, .adv_w = 173, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 326, .adv_w = 173, .box_w = 10, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 374, .adv_w = 173, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 425, .adv_w = 173, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 480, .adv_w = 173, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 523, .adv_w = 173, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 581, .adv_w = 173, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 635, .adv_w = 173, .box_w = 11, .box_h = 13, .ofs_x = 0, .ofs_y = 0}, + {.bitmap_index = 697, .adv_w = 173, .box_w = 9, .box_h = 13, .ofs_x = 1, .ofs_y = 0}, + {.bitmap_index = 753, .adv_w = 180, .box_w = 13, .box_h = 19, .ofs_x = -1, .ofs_y = -3}, + {.bitmap_index = 836, .adv_w = 216, .box_w = 14, .box_h = 19, .ofs_x = 0, .ofs_y = -3}, + {.bitmap_index = 915, .adv_w = 360, .box_w = 23, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 983, .adv_w = 360, .box_w = 23, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 1052, .adv_w = 360, .box_w = 23, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 1121, .adv_w = 360, .box_w = 23, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 1191, .adv_w = 360, .box_w = 23, .box_h = 12, .ofs_x = 0, .ofs_y = 1}, + {.bitmap_index = 1253, .adv_w = 360, .box_w = 23, .box_h = 15, .ofs_x = 0, .ofs_y = -1}, + {.bitmap_index = 1365, .adv_w = 252, .box_w = 14, .box_h = 19, .ofs_x = 1, .ofs_y = -3} +}; + +/*--------------------- + * CHARACTER MAPPING + *--------------------*/ + +static const uint16_t unicode_list_0[] = { + 0x0, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x1c, 0x2e, 0xf0c2, 0xf1c1, + 0xf21b, 0xf21c, 0xf21d, 0xf21e, 0xf21f, 0xf262, 0xf26e +}; + +/*Collect the unicode lists and glyph_id offsets*/ +static const lv_font_fmt_txt_cmap_t cmaps[] = +{ + { + .range_start = 37, .range_length = 62063, .glyph_id_start = 1, + .unicode_list = unicode_list_0, .glyph_id_ofs_list = NULL, .list_length = 23, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + } +}; + + + +/*-------------------- + * ALL CUSTOM DATA + *--------------------*/ + +#if LVGL_VERSION_MAJOR == 8 +/*Store all the custom data of the font*/ +static lv_font_fmt_txt_glyph_cache_t cache; +#endif + +#if LVGL_VERSION_MAJOR >= 8 +static const lv_font_fmt_txt_dsc_t font_dsc = { +#else +static lv_font_fmt_txt_dsc_t font_dsc = { +#endif + .glyph_bitmap = glyph_bitmap, + .glyph_dsc = glyph_dsc, + .cmaps = cmaps, + .kern_dsc = NULL, + .kern_scale = 0, + .cmap_num = 1, + .bpp = 4, + .kern_classes = 0, + .bitmap_format = 1, +#if LVGL_VERSION_MAJOR == 8 + .cache = &cache +#endif +}; + + + +/*----------------- + * PUBLIC FONT + *----------------*/ + +/*Initialize a public general font descriptor*/ +#if LVGL_VERSION_MAJOR >= 8 +const lv_font_t ui_font_keyboard_small_18 = { +#else +lv_font_t ui_font_keyboard_small_18 = { +#endif + .get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/ + .get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/ + .line_height = 19, /*The maximum line height required by the font*/ + .base_line = 3, /*Baseline measured from the bottom of the line*/ +#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0) + .subpx = LV_FONT_SUBPX_NONE, +#endif +#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8 + .underline_position = -3, + .underline_thickness = 1, +#endif + .dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */ +#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9 + .fallback = NULL, +#endif + .user_data = NULL, +}; + + + +#endif /*#if UI_FONT_KEYBOARD_SMALL_18*/ + diff --git a/src/ui/fonts/ui_font_keyboard_time_48.c b/src/ui/fonts/ui_font_keyboard_time_48.c new file mode 100644 index 0000000..b530773 --- /dev/null +++ b/src/ui/fonts/ui_font_keyboard_time_48.c @@ -0,0 +1,455 @@ +/******************************************************************************* + * Size: 48 px + * Bpp: 4 + * Opts: --format lvgl --output E:\projects\lvgl\lv_port_pc_vscode\src\ui\fonts\ui_font_keyboard_time_48.c --size 48 --bpp 4 --lv-font-name ui_font_keyboard_time_48 --font E:\projects\lvgl\lv_port_pc_vscode\external\ttf\MapleMono-NF-CN-SemiBold.ttf --range 0x30-0x39 --symbols : + ******************************************************************************/ + +#ifdef LV_LVGL_H_INCLUDE_SIMPLE +#include "lvgl.h" +#else +#include "lvgl/lvgl.h" +#endif + +#ifndef UI_FONT_KEYBOARD_TIME_48 +#define UI_FONT_KEYBOARD_TIME_48 1 +#endif + +#if UI_FONT_KEYBOARD_TIME_48 + +/*----------------- + * BITMAPS + *----------------*/ + +/*Store the image of the glyphs*/ +static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { + /* U+0030 "0" */ + 0x0, 0xfc, 0xb5, 0xbf, 0xf6, 0xd2, 0x80, 0x7f, + 0xf0, 0x97, 0xa9, 0x48, 0x2, 0x25, 0xae, 0x40, + 0xf, 0xfa, 0x28, 0x40, 0x3f, 0x86, 0xdc, 0x3, + 0xfa, 0x1c, 0x3, 0xff, 0x83, 0xe, 0x1, 0xf2, + 0x38, 0x7, 0xff, 0xe, 0xc, 0x3, 0xd2, 0x1, + 0xe5, 0xab, 0x94, 0x0, 0xf7, 0x80, 0x73, 0x10, + 0x7, 0x5d, 0x2a, 0x35, 0xc8, 0x7, 0x1a, 0x0, + 0x6a, 0x0, 0xe8, 0x40, 0xe, 0x65, 0x0, 0xee, + 0x0, 0x88, 0xc0, 0x31, 0x38, 0x7, 0xd6, 0x1, + 0xca, 0x20, 0x4, 0x0, 0xeb, 0x0, 0xfc, 0x2a, + 0x1, 0xca, 0x0, 0xc0, 0xe, 0x70, 0xf, 0xce, + 0xa0, 0x1c, 0x60, 0x7, 0x0, 0xc6, 0x20, 0x1f, + 0x4c, 0x0, 0x7b, 0x0, 0x4, 0x1, 0x94, 0x3, + 0xc3, 0x6c, 0x1, 0xf3, 0x0, 0x80, 0x77, 0x0, + 0x71, 0x62, 0x0, 0x7e, 0x30, 0x20, 0xe, 0x20, + 0xc, 0x98, 0x40, 0x1f, 0xc2, 0xe, 0x1, 0xc2, + 0x1, 0x35, 0x88, 0x7, 0xfc, 0x22, 0x0, 0xe7, + 0x0, 0x44, 0x80, 0x79, 0x60, 0x3, 0x8c, 0x3, + 0xf5, 0xb8, 0x7, 0xa2, 0x84, 0x3, 0xff, 0x80, + 0xd8, 0x80, 0x1e, 0xa7, 0x1, 0x0, 0xe3, 0x10, + 0xe, 0xa3, 0x0, 0xe1, 0xc5, 0x0, 0xfe, 0x70, + 0xf, 0xf8, 0xfc, 0xc0, 0x2e, 0x0, 0xe1, 0x20, + 0xf, 0xf2, 0xe0, 0x80, 0x63, 0x0, 0xc2, 0x2, + 0x1, 0xfc, 0xf4, 0x1, 0xe7, 0x0, 0xc4, 0x0, + 0x20, 0xf, 0xa6, 0x0, 0x3e, 0x20, 0xc, 0xc0, + 0x7, 0x0, 0xe1, 0xc6, 0x0, 0xf9, 0x0, 0x3b, + 0x0, 0x18, 0x1, 0xd0, 0x60, 0x1f, 0xb4, 0x3, + 0x94, 0x0, 0x80, 0x1d, 0x60, 0x1f, 0x85, 0x80, + 0x30, 0x98, 0x0, 0x8c, 0x3, 0x13, 0x80, 0x7d, + 0x60, 0x1c, 0xc0, 0x1a, 0x80, 0x3a, 0x10, 0x3, + 0x9d, 0x40, 0x3a, 0x80, 0x33, 0x10, 0x7, 0x5d, + 0x2a, 0x35, 0xc0, 0x7, 0x29, 0x0, 0x77, 0x0, + 0x79, 0x6a, 0xe5, 0x0, 0x3d, 0x0, 0x1e, 0x38, + 0x0, 0xff, 0xe1, 0xd1, 0x0, 0x7c, 0xf0, 0x1, + 0xff, 0xc1, 0xa5, 0x0, 0xfe, 0x7a, 0x10, 0xf, + 0xe2, 0xc5, 0x0, 0xff, 0x97, 0xa5, 0x48, 0x0, + 0x24, 0xd7, 0xa6, 0x1, 0xe0, + + /* U+0031 "1" */ + 0x0, 0xfc, 0xb7, 0xdf, 0xd6, 0x60, 0x1f, 0xfc, + 0x31, 0xca, 0x41, 0x1, 0x4c, 0x30, 0xf, 0xfe, + 0xa, 0x79, 0x80, 0x7d, 0x20, 0x1f, 0xfc, 0x7, + 0xb1, 0x0, 0xfc, 0xe0, 0x1f, 0xe1, 0xb8, 0x0, + 0xff, 0xe4, 0xe, 0x20, 0x7, 0xff, 0x26, 0x8, + 0x3, 0xca, 0x1, 0xff, 0xc3, 0x40, 0xe, 0x1b, + 0xa4, 0x0, 0xff, 0xe1, 0x18, 0x6, 0x3f, 0x40, + 0x10, 0xf, 0xfe, 0x11, 0x0, 0x4b, 0x82, 0x1, + 0xff, 0xc5, 0x90, 0x2a, 0xa0, 0x7, 0xff, 0x1d, + 0x7f, 0x54, 0x3, 0xff, 0xfe, 0x1, 0xff, 0xff, + 0x0, 0xff, 0xff, 0x80, 0x7f, 0xff, 0xc0, 0x3f, + 0xff, 0xe0, 0x1f, 0xfe, 0xa4, 0x55, 0xf9, 0x80, + 0x31, 0xaa, 0xf8, 0x40, 0x19, 0x75, 0x5f, 0x18, + 0x6, 0x1a, 0xaf, 0x71, 0x9c, 0x1, 0xff, 0xc9, + 0x96, 0x0, 0xff, 0xe5, 0xb, 0x80, 0x7f, 0xf2, + 0x88, 0x90, 0x20, 0x1f, 0xfc, 0x74, 0x80, + + /* U+0032 "2" */ + 0x0, 0xc2, 0xd5, 0x9d, 0xff, 0x76, 0xd3, 0x90, + 0x7, 0xf0, 0xcf, 0x4a, 0x98, 0x80, 0x42, 0x4b, + 0x1b, 0x64, 0x1, 0xe1, 0xf6, 0x0, 0xff, 0xe0, + 0xa6, 0xa8, 0x7, 0x58, 0x80, 0x7f, 0xf1, 0x29, + 0x0, 0x32, 0x0, 0x7f, 0xf1, 0xa8, 0x40, 0x24, + 0x0, 0xe5, 0x9c, 0xdc, 0xa6, 0x0, 0xf0, 0xb0, + 0x5, 0x66, 0x0, 0x7e, 0xa6, 0x32, 0x35, 0x9e, + 0x30, 0xe, 0xa0, 0x8, 0x73, 0x1d, 0x2, 0x1, + 0xf0, 0xe8, 0x7, 0x10, 0x7, 0x18, 0x80, 0x7f, + 0x85, 0x80, 0x38, 0xc0, 0x3f, 0xf8, 0xb8, 0x1, + 0xff, 0xcb, 0x10, 0xf, 0xfe, 0x59, 0x0, 0x71, + 0x0, 0x7f, 0xf1, 0x74, 0x3, 0x8, 0x80, 0x3f, + 0xf8, 0x86, 0x80, 0x19, 0xc0, 0x3f, 0xf8, 0xbe, + 0x1, 0xd4, 0x1, 0xff, 0xc4, 0x83, 0x0, 0xc8, + 0x60, 0x1f, 0xfc, 0x35, 0x70, 0xe, 0x80, 0xf, + 0xfe, 0x1a, 0x50, 0x7, 0x39, 0x80, 0x7f, 0xf0, + 0x8e, 0xc0, 0x38, 0xe0, 0x3, 0xff, 0x84, 0x78, + 0x1, 0xc5, 0xa0, 0x1f, 0xfc, 0x23, 0xc0, 0xe, + 0x1f, 0x10, 0xf, 0xfe, 0x9, 0xe0, 0x7, 0xe, + 0x90, 0x7, 0xff, 0x4, 0xf0, 0x3, 0x87, 0x4c, + 0x3, 0xff, 0x82, 0x78, 0x1, 0xc3, 0xa6, 0x1, + 0xff, 0xc1, 0x3c, 0x0, 0xe1, 0xd3, 0x0, 0xff, + 0xe0, 0x9e, 0x0, 0x70, 0xe9, 0x80, 0x7f, 0xf0, + 0x4f, 0x0, 0x38, 0x74, 0xc0, 0x3f, 0xf8, 0x27, + 0x80, 0x1c, 0x3a, 0x60, 0x1f, 0xfc, 0x13, 0xc0, + 0xe, 0x1d, 0x30, 0xf, 0xfe, 0x9, 0x60, 0x7, + 0x95, 0xd1, 0x3f, 0xc6, 0x20, 0xa, 0x0, 0xf9, + 0x2e, 0xff, 0xec, 0xe8, 0x2, 0x0, 0xff, 0xe4, + 0xb8, 0x98, 0x7, 0xff, 0x28, 0xe8, 0x3, 0xff, + 0x94, 0x47, 0x66, 0x1, 0xff, 0xc6, 0x2b, 0x0, + + /* U+0033 "3" */ + 0x0, 0xcb, 0x39, 0xdf, 0xf7, 0x64, 0xa8, 0x7, + 0xf9, 0xfe, 0x98, 0xc4, 0x2, 0x13, 0x6a, 0xf8, + 0x0, 0xf9, 0x60, 0x3, 0xff, 0x82, 0xfa, 0x40, + 0x1d, 0xc0, 0x1f, 0xfc, 0x32, 0xc2, 0x0, 0xc4, + 0x1, 0xff, 0xc4, 0x1e, 0x0, 0xd6, 0x1, 0x96, + 0x2e, 0xd5, 0x6, 0x1, 0xe3, 0x30, 0x4, 0x58, + 0xcf, 0xd4, 0xe8, 0x85, 0x7c, 0xb1, 0x0, 0xeb, + 0x0, 0xc7, 0x30, 0x20, 0x1f, 0x26, 0x80, 0x71, + 0x80, 0x7f, 0xf1, 0xc, 0x80, 0x33, 0x80, 0x7f, + 0xf1, 0x44, 0x3, 0x38, 0x7, 0xff, 0x10, 0x4c, + 0x3, 0x10, 0x7, 0xff, 0x12, 0xc0, 0x3b, 0x0, + 0x3f, 0xf8, 0x47, 0x8a, 0x1, 0x85, 0x40, 0x3f, + 0x12, 0x21, 0xa3, 0x30, 0x60, 0x1d, 0x60, 0x1f, + 0xe, 0xea, 0xed, 0x2e, 0x60, 0x1e, 0x85, 0x0, + 0xfa, 0x48, 0x3, 0xfc, 0xb8, 0xe0, 0x1f, 0x84, + 0x3, 0xfe, 0x49, 0x20, 0xf, 0xde, 0x1, 0xff, + 0xa, 0xec, 0x0, 0x7c, 0xcc, 0x0, 0xff, 0xe0, + 0xbc, 0x0, 0x7d, 0x3f, 0xf7, 0x6d, 0xc2, 0x0, + 0x79, 0xcc, 0x3, 0xfc, 0x24, 0x8f, 0x78, 0x40, + 0x1d, 0x20, 0x1f, 0xfc, 0x33, 0xc2, 0x0, 0xca, + 0x1, 0xff, 0xc4, 0x1b, 0x0, 0xe3, 0x0, 0xff, + 0xe2, 0x28, 0x7, 0x38, 0x7, 0xff, 0x11, 0xc0, + 0x38, 0x40, 0x3f, 0xf8, 0x84, 0x1, 0xc2, 0x1, + 0xff, 0xc4, 0xd0, 0xe, 0x60, 0xf, 0xfe, 0x1a, + 0xa0, 0x6, 0x12, 0x4, 0xba, 0x50, 0xf, 0xe6, + 0xa0, 0xe, 0x70, 0x4b, 0x45, 0xaf, 0xb8, 0x64, + 0x42, 0xc6, 0xc8, 0x7, 0xac, 0x38, 0x3, 0x91, + 0xe6, 0xed, 0x4e, 0x40, 0x1e, 0x82, 0x1, 0x0, + 0xff, 0xe3, 0x2b, 0x80, 0x3c, 0x3, 0xff, 0x8a, + 0xf4, 0x1, 0x26, 0x28, 0x7, 0xff, 0x5, 0x32, + 0x0, 0x38, 0xeb, 0xed, 0xd0, 0x84, 0x2, 0x24, + 0x8d, 0xb3, 0x0, 0xe0, + + /* U+0034 "4" */ + 0x0, 0xff, 0xe1, 0x26, 0x7f, 0xa8, 0x40, 0x3f, + 0xf8, 0xe7, 0x66, 0x0, 0x5d, 0x0, 0xff, 0xe3, + 0xf0, 0x7, 0x19, 0x0, 0x7f, 0xf1, 0x5c, 0x80, + 0x3c, 0xc0, 0x1f, 0xfc, 0x42, 0x80, 0xf, 0xfe, + 0x67, 0x0, 0x7f, 0xf3, 0x18, 0xc0, 0x3f, 0xf9, + 0x63, 0x20, 0x1f, 0xfc, 0xca, 0x0, 0xff, 0xe6, + 0x2a, 0x0, 0x61, 0x0, 0xff, 0xe3, 0xd, 0x80, + 0x66, 0xa0, 0xf, 0xfe, 0x35, 0x88, 0x4, 0x30, + 0x1, 0xff, 0xc6, 0x45, 0x0, 0xd0, 0x20, 0x1f, + 0xfc, 0x69, 0x0, 0xc4, 0xc0, 0x1f, 0xfc, 0x68, + 0x20, 0xd, 0xc0, 0x1f, 0xfc, 0x63, 0x70, 0xc, + 0xa6, 0x1, 0xff, 0xc6, 0xe0, 0xe, 0xb0, 0xf, + 0xfe, 0x33, 0x90, 0x6, 0x81, 0x0, 0xff, 0xe2, + 0x94, 0x0, 0x62, 0x70, 0xf, 0xfe, 0x37, 0x0, + 0x74, 0x80, 0x7f, 0xf1, 0x98, 0xc0, 0x32, 0x20, + 0x3, 0xff, 0x8a, 0x32, 0x1, 0xc2, 0x3f, 0x0, + 0x78, 0x46, 0x0, 0x9c, 0x3, 0xcb, 0xdd, 0xeb, + 0x0, 0xea, 0xee, 0x94, 0x4, 0x3, 0xff, 0x94, + 0x36, 0x2, 0x1, 0xff, 0xcb, 0x10, 0x71, 0x0, + 0xff, 0xe5, 0x8, 0xe, 0xa0, 0x7, 0xff, 0x20, + 0xa8, 0x0, 0x77, 0xff, 0xff, 0x58, 0x7, 0x5f, + 0xfb, 0x50, 0x3, 0xff, 0xfe, 0x1, 0xff, 0xe0, + 0x70, 0xe, 0x70, 0xf, 0xfe, 0x39, 0x0, 0x71, + 0x0, 0x7f, 0xf1, 0xc6, 0x88, 0x6, 0x84, 0x3, + 0x80, + + /* U+0035 "5" */ + 0x0, 0x93, 0x3f, 0xff, 0xf8, 0x74, 0x1, 0x92, + 0xcc, 0x3, 0xff, 0x86, 0xae, 0x1, 0x78, 0x7, + 0xff, 0x1b, 0x40, 0x25, 0x0, 0xff, 0xe3, 0x10, + 0x7, 0xff, 0x24, 0x68, 0x3, 0xf9, 0xee, 0xff, + 0xd9, 0xc4, 0x1, 0xf9, 0x21, 0x13, 0xfc, 0x60, + 0x1f, 0xe2, 0x0, 0xff, 0xff, 0x80, 0x7f, 0xf1, + 0x8, 0x3, 0xff, 0x94, 0x8a, 0x20, 0x1f, 0xfc, + 0x9a, 0xef, 0xf7, 0x64, 0xa8, 0x7, 0xf1, 0x80, + 0x7f, 0x9, 0xb5, 0x7c, 0x0, 0x7c, 0xc0, 0x1f, + 0xfc, 0x27, 0xd2, 0x0, 0xef, 0x0, 0xff, 0xe1, + 0x96, 0x8, 0x6, 0x4b, 0x30, 0xf, 0xfe, 0x10, + 0xd0, 0x7, 0x26, 0x7f, 0xee, 0xca, 0x61, 0x0, + 0xf2, 0x20, 0x3, 0xfc, 0x26, 0xb3, 0xd0, 0x1, + 0xee, 0x0, 0xff, 0xe1, 0xbd, 0x0, 0x72, 0x80, + 0x7f, 0xf1, 0x15, 0x40, 0x1c, 0x40, 0x1f, 0xfc, + 0x4f, 0x0, 0xe6, 0x0, 0xff, 0xe2, 0x30, 0x7, + 0x8, 0x7, 0xff, 0x11, 0xc0, 0x3f, 0xf9, 0x44, + 0x1, 0xc2, 0x1, 0xff, 0xc4, 0xa0, 0xe, 0x60, + 0xf, 0xfe, 0x1a, 0x98, 0x6, 0x12, 0x4, 0xba, + 0x50, 0xf, 0xe6, 0xa0, 0xe, 0x60, 0x4b, 0x45, + 0xaf, 0xb8, 0x64, 0x42, 0xc6, 0xc8, 0x7, 0xa4, + 0x38, 0x3, 0x91, 0xe6, 0xed, 0x4e, 0x40, 0x1e, + 0x81, 0x1, 0x0, 0xff, 0xe3, 0x33, 0x80, 0x3c, + 0x3, 0xff, 0x8a, 0xf2, 0x1, 0x26, 0x28, 0x7, + 0xff, 0x5, 0x32, 0x0, 0x38, 0xeb, 0xed, 0xd0, + 0x84, 0x2, 0x25, 0x8d, 0xb3, 0x0, 0xe0, + + /* U+0036 "6" */ + 0x0, 0xff, 0xe0, 0x14, 0xf7, 0xf2, 0x0, 0x7f, + 0xf1, 0xa3, 0x58, 0x40, 0x60, 0x3, 0xff, 0x8b, + 0x6e, 0x1, 0xc8, 0x1, 0xff, 0xc3, 0x1c, 0x40, + 0xe, 0x88, 0x0, 0x7f, 0xf0, 0x87, 0xc, 0x3, + 0x16, 0x38, 0x7, 0xff, 0xf, 0x48, 0x3, 0x26, + 0x18, 0x7, 0xff, 0xe, 0x4c, 0x3, 0x2d, 0x88, + 0x7, 0xff, 0xd, 0x58, 0x3, 0x2d, 0x0, 0x7f, + 0xf1, 0x6, 0xc0, 0x31, 0xd0, 0x7, 0xff, 0x16, + 0x4, 0x2, 0x1d, 0x0, 0xff, 0xe2, 0x93, 0x0, + 0x6b, 0x10, 0xf, 0xfe, 0x2d, 0x80, 0x65, 0x50, + 0x7, 0xff, 0x14, 0x58, 0x3, 0x40, 0x13, 0xe7, + 0x7f, 0xb6, 0x50, 0x3, 0xf3, 0x0, 0x66, 0x29, + 0xd8, 0x31, 0x0, 0x13, 0x5d, 0x84, 0x3, 0xd4, + 0x1, 0xb7, 0x4c, 0x1, 0xfc, 0x9e, 0x40, 0x18, + 0x88, 0x1, 0x14, 0x90, 0x7, 0xfc, 0x3e, 0x1, + 0x90, 0x3, 0x11, 0x0, 0x6, 0xf0, 0xc6, 0x1, + 0xe2, 0x80, 0xb, 0x0, 0x3c, 0x59, 0x88, 0x79, + 0xcd, 0x30, 0xe, 0x60, 0x9, 0xc0, 0x38, 0xf4, + 0xc0, 0x38, 0xb0, 0xc0, 0x30, 0xa8, 0x0, 0x80, + 0x3b, 0x80, 0x3f, 0xb8, 0x3, 0xb0, 0x4, 0x3, + 0x94, 0x80, 0x3f, 0x89, 0x0, 0x32, 0x81, 0x80, + 0x77, 0x80, 0x7f, 0xd8, 0x1, 0x88, 0x4, 0x3, + 0x9c, 0x3, 0xfe, 0x20, 0xc, 0x20, 0x1f, 0x18, + 0x7, 0xff, 0x30, 0xc0, 0x3f, 0xf8, 0xc4, 0x1, + 0xce, 0x1, 0xff, 0x68, 0x6, 0x20, 0x10, 0xe, + 0xe0, 0xf, 0xf9, 0x40, 0x33, 0x80, 0x10, 0x3, + 0x22, 0x0, 0x3f, 0x9c, 0x80, 0x36, 0x0, 0x34, + 0x3, 0xad, 0x0, 0x3e, 0x68, 0x0, 0xe7, 0x0, + 0x30, 0x80, 0x75, 0xd2, 0x90, 0x92, 0xdc, 0x80, + 0x73, 0x8, 0x5, 0x60, 0x1e, 0x5a, 0xde, 0xda, + 0x40, 0xe, 0x18, 0x0, 0xca, 0xc0, 0x1f, 0xfc, + 0x5d, 0x10, 0xe, 0x97, 0x0, 0xff, 0xe1, 0xe, + 0x18, 0x7, 0xd1, 0x86, 0x1, 0xff, 0x37, 0x98, + 0x7, 0xf1, 0xe6, 0x1d, 0x4, 0x2, 0x25, 0x9e, + 0x91, 0x0, 0xe0, + + /* U+0037 "7" */ + 0x8, 0xff, 0xff, 0xe3, 0xd9, 0x82, 0x38, 0x7, + 0xff, 0x1d, 0x30, 0x8c, 0x3, 0xff, 0x95, 0x44, + 0x1, 0xff, 0xca, 0x35, 0x30, 0xf, 0xfe, 0x48, + 0x86, 0x62, 0xef, 0xff, 0x83, 0x20, 0x1e, 0xd0, + 0x1, 0xa2, 0x7f, 0xf0, 0x58, 0x40, 0x39, 0x80, + 0x3f, 0xf8, 0x86, 0x20, 0x19, 0x44, 0x3, 0xff, + 0x89, 0x20, 0x1d, 0x20, 0x1f, 0xfc, 0x41, 0x50, + 0xc, 0x46, 0x1, 0xff, 0xc4, 0x90, 0xe, 0xb0, + 0xf, 0xfe, 0x2b, 0x0, 0x73, 0x0, 0x7f, 0xf1, + 0x14, 0x40, 0x33, 0x0, 0x7f, 0xf1, 0x64, 0x3, + 0xac, 0x3, 0xff, 0x88, 0x66, 0x0, 0xc8, 0x40, + 0x1f, 0xfc, 0x49, 0x0, 0xef, 0x0, 0xff, 0xe2, + 0xa, 0x80, 0x62, 0x40, 0xf, 0xfe, 0x23, 0x0, + 0x75, 0x0, 0x7f, 0xf1, 0x64, 0x3, 0x98, 0x3, + 0xff, 0x88, 0xa2, 0x1, 0x94, 0x40, 0x3f, 0xf8, + 0x92, 0x1, 0xd2, 0x1, 0xff, 0xc4, 0x23, 0x0, + 0xc6, 0x60, 0xf, 0xfe, 0x25, 0x80, 0x74, 0x80, + 0x7f, 0xf1, 0x5, 0x80, 0x30, 0xa8, 0x7, 0xff, + 0x11, 0x80, 0x39, 0x80, 0x3f, 0xf8, 0xb4, 0x1, + 0xd2, 0x1, 0xff, 0xc4, 0x42, 0x0, 0xc8, 0x20, + 0x1f, 0xfc, 0x4f, 0x0, 0xef, 0x0, 0xff, 0xe2, + 0x12, 0x0, 0x62, 0x40, 0xf, 0xfe, 0x25, 0x80, + 0x75, 0x80, 0x7f, 0xf1, 0x58, 0x3, 0x98, 0x3, + 0xff, 0x88, 0xc0, 0x1c, 0xc0, 0x1f, 0xfc, 0x5a, + 0x0, 0xeb, 0x0, 0xff, 0xe5, 0x21, 0x0, 0x7f, + 0xf1, 0x60, 0x80, 0xb, 0x40, 0x1f, 0xfc, 0x0, + + /* U+0038 "8" */ + 0x0, 0xf8, 0xe2, 0xfb, 0xfe, 0xdb, 0x72, 0x0, + 0xff, 0xe0, 0x1e, 0x61, 0xd0, 0x40, 0x22, 0x48, + 0xda, 0x20, 0xf, 0xe8, 0xc3, 0x0, 0xff, 0x97, + 0x54, 0x3, 0xe8, 0x70, 0xf, 0xfe, 0x1d, 0x28, + 0x7, 0x1b, 0x80, 0x7f, 0xf1, 0x68, 0x40, 0x34, + 0x0, 0x78, 0xe2, 0xae, 0x9c, 0x80, 0x3d, 0x20, + 0x10, 0xa0, 0x7, 0x4e, 0x3a, 0xa2, 0xc6, 0xb0, + 0x7, 0x28, 0x4, 0x40, 0x1c, 0xac, 0x1, 0xf4, + 0x98, 0x6, 0x20, 0x9, 0xc0, 0x3b, 0xc0, 0x3f, + 0xac, 0x3, 0x84, 0x0, 0xe0, 0x1c, 0x20, 0x1f, + 0xfc, 0x31, 0x0, 0x18, 0x7, 0x18, 0x7, 0xf6, + 0x80, 0x62, 0x0, 0xca, 0x1, 0xa0, 0x80, 0x3e, + 0x45, 0x0, 0xd4, 0x1, 0xa0, 0x3, 0xb6, 0xc, + 0x0, 0x29, 0x36, 0x1, 0x85, 0xc0, 0x31, 0x50, + 0x7, 0x3e, 0x7f, 0xba, 0xd8, 0x3, 0xe, 0x80, + 0x79, 0x70, 0x80, 0x3f, 0xf8, 0x29, 0x86, 0x1, + 0xf1, 0xec, 0x88, 0x7, 0xf1, 0xe5, 0x90, 0x7, + 0xe2, 0xb9, 0x10, 0xf, 0xe3, 0xca, 0x10, 0xf, + 0x97, 0x50, 0x3, 0xff, 0x82, 0xbe, 0x60, 0x1c, + 0x94, 0x1, 0xc5, 0x15, 0x74, 0xe2, 0x1, 0x87, + 0x48, 0x3, 0x50, 0x7, 0x4e, 0xba, 0xa2, 0xc7, + 0x38, 0x6, 0x19, 0x0, 0x9c, 0x40, 0x34, 0xb0, + 0x7, 0xd0, 0xc0, 0x19, 0x10, 0x0, 0xb0, 0xc, + 0x6c, 0x1, 0xfd, 0x0, 0x1d, 0xa0, 0x26, 0x1, + 0xac, 0x3, 0xfc, 0x2a, 0x1, 0x90, 0x8, 0x3, + 0x8c, 0x3, 0xfe, 0xd0, 0xc, 0x40, 0xe0, 0x1f, + 0xfc, 0xc7, 0x0, 0xe2, 0x0, 0xff, 0xb4, 0x3, + 0xc4, 0x1, 0xd4, 0x1, 0xff, 0x20, 0x6, 0x10, + 0x12, 0x0, 0xc6, 0xa0, 0x1f, 0xd2, 0x20, 0x19, + 0x0, 0xa, 0x1, 0xd4, 0xc0, 0x1f, 0x43, 0x0, + 0x77, 0x80, 0x24, 0x3, 0xd3, 0xae, 0xa8, 0xb1, + 0xce, 0x1, 0xc4, 0xa0, 0x1, 0x90, 0xf, 0x14, + 0x55, 0xd3, 0x88, 0x7, 0xb8, 0x3, 0x33, 0x0, + 0x3f, 0xf8, 0xb2, 0x60, 0x1d, 0x2e, 0x1, 0xff, + 0xc3, 0xa6, 0x0, 0xfa, 0x31, 0x0, 0x3f, 0xe5, + 0xe5, 0x0, 0xfe, 0x3b, 0xd8, 0x41, 0x0, 0x89, + 0x67, 0xa8, 0x40, 0x38, + + /* U+0039 "9" */ + 0x0, 0xf8, 0xa2, 0xfb, 0xfe, 0xda, 0x61, 0x0, + 0xff, 0xe0, 0x1e, 0x6b, 0xa0, 0x80, 0x44, 0xb3, + 0xd4, 0x20, 0x1f, 0xd1, 0x86, 0x1, 0xff, 0x2f, + 0x28, 0x7, 0xd2, 0xe0, 0x1f, 0xfc, 0x3a, 0x50, + 0xe, 0x66, 0x0, 0x7f, 0xf1, 0x68, 0x80, 0x34, + 0x0, 0x79, 0xaf, 0xb9, 0xb4, 0x80, 0x1e, 0x90, + 0x9, 0xc4, 0x3, 0x16, 0xca, 0x8, 0x89, 0x6e, + 0xc0, 0x1c, 0x86, 0x0, 0xb0, 0xc, 0x38, 0x40, + 0x1f, 0x25, 0x0, 0x75, 0x80, 0xc, 0x3, 0x30, + 0x80, 0x7f, 0x29, 0x80, 0x64, 0x2, 0x0, 0xeb, + 0x0, 0xff, 0xac, 0x3, 0x10, 0x8, 0x7, 0x10, + 0x7, 0xfc, 0x40, 0x18, 0x40, 0x3e, 0x30, 0xf, + 0xfe, 0x30, 0x80, 0x73, 0x0, 0x7f, 0xda, 0x1, + 0xe2, 0x0, 0xed, 0x0, 0xff, 0x94, 0x3, 0xe3, + 0x0, 0xcc, 0x20, 0x1f, 0xce, 0x40, 0x18, 0x80, + 0x16, 0x1, 0xda, 0x20, 0x1f, 0x34, 0x0, 0x70, + 0x80, 0x18, 0x3, 0x8f, 0xdc, 0xc0, 0x2, 0x97, + 0x20, 0x1e, 0x60, 0x0, 0xb0, 0x7, 0xc, 0x67, + 0xfb, 0xad, 0x0, 0x2, 0x1, 0xb4, 0x2, 0x93, + 0x0, 0xff, 0xe0, 0xc8, 0x80, 0x64, 0x0, 0xda, + 0x80, 0x1f, 0xe1, 0xc1, 0x0, 0xe3, 0x0, 0xc3, + 0x72, 0x40, 0x1f, 0x3f, 0x9d, 0x0, 0x65, 0x0, + 0xf9, 0xb7, 0x26, 0x21, 0x39, 0xf0, 0x2a, 0x40, + 0x1b, 0x80, 0x3f, 0x8d, 0x9d, 0xcc, 0x60, 0x14, + 0x0, 0x62, 0x40, 0xf, 0xfe, 0x2b, 0x90, 0x6, + 0xb0, 0xf, 0xfe, 0x29, 0x40, 0x6, 0x16, 0x0, + 0xff, 0xe2, 0xf0, 0x7, 0x58, 0x7, 0xff, 0x16, + 0x4c, 0x3, 0x1a, 0x80, 0x7f, 0xf1, 0x1d, 0x80, + 0x3b, 0x80, 0x3f, 0xf8, 0x8f, 0x0, 0x1d, 0x24, + 0x1, 0xff, 0xc3, 0x88, 0x0, 0x73, 0x30, 0x3, + 0xff, 0x84, 0x36, 0xe0, 0x1c, 0xb2, 0x1, 0xff, + 0xc2, 0x2c, 0x40, 0xe, 0x5a, 0x0, 0xff, 0xe1, + 0xf9, 0x0, 0x73, 0xd0, 0x7, 0xff, 0x10, 0x80, + 0x3a, 0xa0, 0x3, 0xff, 0x8b, 0x22, 0x4, 0xfc, + 0xa0, 0x1f, 0xf0, + + /* U+003A ":" */ + 0x0, 0xff, 0xe0, 0xd, 0x77, 0xf4, 0x0, 0x43, + 0xea, 0x20, 0x2f, 0x80, 0xb, 0x10, 0xe, 0x37, + 0x5, 0x0, 0xfa, 0xc0, 0x3f, 0x84, 0x3, 0xf8, + 0x41, 0x40, 0x3e, 0xb0, 0xb1, 0x0, 0xe3, 0x70, + 0x1f, 0x61, 0x1, 0x7c, 0x0, 0x86, 0x7b, 0xfa, + 0x0, 0x3f, 0xff, 0xc3, 0x5d, 0xfd, 0x0, 0x10, + 0xfa, 0x88, 0xb, 0xe0, 0x2, 0xc4, 0x3, 0x8d, + 0xc1, 0x40, 0x3e, 0xb0, 0xf, 0xe1, 0x0, 0xfe, + 0x10, 0x50, 0xf, 0xa8, 0x2c, 0x40, 0x38, 0xd8, + 0x7, 0xd8, 0x40, 0x5f, 0x0 +}; + + +/*--------------------- + * GLYPH DESCRIPTION + *--------------------*/ + +static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { + {.bitmap_index = 0, .adv_w = 0, .box_w = 0, .box_h = 0, .ofs_x = 0, .ofs_y = 0} /* id = 0 reserved */, + {.bitmap_index = 0, .adv_w = 461, .box_w = 25, .box_h = 35, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 293, .adv_w = 461, .box_w = 24, .box_h = 35, .ofs_x = 3, .ofs_y = 0}, + {.bitmap_index = 420, .adv_w = 461, .box_w = 24, .box_h = 35, .ofs_x = 3, .ofs_y = 0}, + {.bitmap_index = 660, .adv_w = 461, .box_w = 23, .box_h = 35, .ofs_x = 3, .ofs_y = 0}, + {.bitmap_index = 912, .adv_w = 461, .box_w = 26, .box_h = 35, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 1105, .adv_w = 461, .box_w = 23, .box_h = 35, .ofs_x = 3, .ofs_y = 0}, + {.bitmap_index = 1312, .adv_w = 461, .box_w = 25, .box_h = 35, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 1587, .adv_w = 461, .box_w = 24, .box_h = 35, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 1795, .adv_w = 461, .box_w = 25, .box_h = 35, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 2087, .adv_w = 461, .box_w = 25, .box_h = 35, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 2362, .adv_w = 461, .box_w = 10, .box_h = 27, .ofs_x = 9, .ofs_y = 0} +}; + +/*--------------------- + * CHARACTER MAPPING + *--------------------*/ + + + +/*Collect the unicode lists and glyph_id offsets*/ +static const lv_font_fmt_txt_cmap_t cmaps[] = +{ + { + .range_start = 48, .range_length = 11, .glyph_id_start = 1, + .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY + } +}; + + + +/*-------------------- + * ALL CUSTOM DATA + *--------------------*/ + +#if LVGL_VERSION_MAJOR == 8 +/*Store all the custom data of the font*/ +static lv_font_fmt_txt_glyph_cache_t cache; +#endif + +#if LVGL_VERSION_MAJOR >= 8 +static const lv_font_fmt_txt_dsc_t font_dsc = { +#else +static lv_font_fmt_txt_dsc_t font_dsc = { +#endif + .glyph_bitmap = glyph_bitmap, + .glyph_dsc = glyph_dsc, + .cmaps = cmaps, + .kern_dsc = NULL, + .kern_scale = 0, + .cmap_num = 1, + .bpp = 4, + .kern_classes = 0, + .bitmap_format = 1, +#if LVGL_VERSION_MAJOR == 8 + .cache = &cache +#endif +}; + + + +/*----------------- + * PUBLIC FONT + *----------------*/ + +/*Initialize a public general font descriptor*/ +#if LVGL_VERSION_MAJOR >= 8 +const lv_font_t ui_font_keyboard_time_48 = { +#else +lv_font_t ui_font_keyboard_time_48 = { +#endif + .get_glyph_dsc = lv_font_get_glyph_dsc_fmt_txt, /*Function pointer to get glyph's data*/ + .get_glyph_bitmap = lv_font_get_bitmap_fmt_txt, /*Function pointer to get glyph's bitmap*/ + .line_height = 35, /*The maximum line height required by the font*/ + .base_line = 0, /*Baseline measured from the bottom of the line*/ +#if !(LVGL_VERSION_MAJOR == 6 && LVGL_VERSION_MINOR == 0) + .subpx = LV_FONT_SUBPX_NONE, +#endif +#if LV_VERSION_CHECK(7, 4, 0) || LVGL_VERSION_MAJOR >= 8 + .underline_position = -7, + .underline_thickness = 2, +#endif + .dsc = &font_dsc, /*The custom font data. Will be accessed by `get_glyph_bitmap/dsc` */ +#if LV_VERSION_CHECK(8, 2, 0) || LVGL_VERSION_MAJOR >= 9 + .fallback = NULL, +#endif + .user_data = NULL, +}; + + + +#endif /*#if UI_FONT_KEYBOARD_TIME_48*/ + diff --git a/sysbuild.conf b/sysbuild.conf new file mode 100644 index 0000000..1e134e9 --- /dev/null +++ b/sysbuild.conf @@ -0,0 +1,2 @@ +SB_CONFIG_BOOTLOADER_MCUBOOT=y +SB_CONFIG_MCUBOOT_MODE_SINGLE_APP=y diff --git a/sysbuild/CMakeLists.txt b/sysbuild/CMakeLists.txt new file mode 100644 index 0000000..0385be5 --- /dev/null +++ b/sysbuild/CMakeLists.txt @@ -0,0 +1,20 @@ +# Copyright (c) 2026 +# +# SPDX-License-Identifier: Apache-2.0 + +set(IP5306_LOCAL_MODULE "${APP_DIR}/modules/ip5306") +set(IP5306_EXTERNAL_MODULE "E:/extra/modules/ip5306") +set(extra_zephyr_modules "$CACHE{ZEPHYR_EXTRA_MODULES}") + +if(EXISTS "${IP5306_LOCAL_MODULE}/zephyr/module.yml") + list(APPEND extra_zephyr_modules "${IP5306_LOCAL_MODULE}") +elseif(EXISTS "${IP5306_EXTERNAL_MODULE}/zephyr/module.yml") + list(APPEND extra_zephyr_modules "${IP5306_EXTERNAL_MODULE}") +endif() + +list(REMOVE_DUPLICATES extra_zephyr_modules) +set(ZEPHYR_EXTRA_MODULES "${extra_zephyr_modules}" CACHE STRING "Extra Zephyr modules" FORCE) + +find_package(Sysbuild REQUIRED HINTS $ENV{ZEPHYR_BASE}) + +project(sysbuild LANGUAGES) diff --git a/sysbuild/mcuboot.conf b/sysbuild/mcuboot.conf new file mode 100644 index 0000000..90b7843 --- /dev/null +++ b/sysbuild/mcuboot.conf @@ -0,0 +1,25 @@ +CONFIG_LOG=n +CONFIG_CONSOLE=n +CONFIG_UART_CONSOLE=n + +CONFIG_SERIAL=y +CONFIG_UART_INTERRUPT_DRIVEN=y +CONFIG_UART_LINE_CTRL=y + +CONFIG_USB_DEVICE_STACK=y +CONFIG_USB_DEVICE_REMOTE_WAKEUP=n +CONFIG_USB_CDC_ACM=y +CONFIG_USB_NRFX=y +CONFIG_USB_DEVICE_PRODUCT="MCUBOOT" +CONFIG_USB_DEVICE_MANUFACTURER="new_kbd" +CONFIG_USB_DEVICE_VID=0x1209 +CONFIG_USB_DEVICE_PID=0x0002 + +CONFIG_RETAINED_MEM=y +CONFIG_RETENTION=y +CONFIG_RETENTION_BOOT_MODE=y + +CONFIG_MCUBOOT_SERIAL=y +CONFIG_BOOT_SERIAL_CDC_ACM=y +CONFIG_BOOT_SERIAL_BOOT_MODE=y +CONFIG_BOOT_SERIAL_NO_APPLICATION=y diff --git a/sysbuild/mcuboot.overlay b/sysbuild/mcuboot.overlay new file mode 100644 index 0000000..01c42dc --- /dev/null +++ b/sysbuild/mcuboot.overlay @@ -0,0 +1,23 @@ +/ { + chosen { + zephyr,boot-mode = &boot_mode0; + }; +}; + +&usbd { + status = "okay"; + + cdc_acm_uart0: cdc_acm_uart0 { + compatible = "zephyr,cdc-acm-uart"; + }; +}; + +&gpregret1 { + status = "okay"; + + boot_mode0: boot_mode@0 { + compatible = "zephyr,retention"; + status = "okay"; + reg = <0x0 0x1>; + }; +};