Compare commits
10 Commits
277462a8fe
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 908e7a0a4d | |||
| 2356cb4fe8 | |||
| b5433f0403 | |||
| 881b36274c | |||
| e369567998 | |||
| c0a6e45911 | |||
| 5b4353d94f | |||
| 302df0230d | |||
| 82be5cae52 | |||
| 2c7eae4de1 |
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.20.0)
|
cmake_minimum_required(VERSION 3.20.0)
|
||||||
|
|
||||||
if(EXISTS "E:/extra/modules/ip5305")
|
if(EXISTS "E:/extra/modules/ip5306")
|
||||||
list(APPEND ZEPHYR_EXTRA_MODULES "E:/extra/modules/ip5305")
|
list(APPEND ZEPHYR_EXTRA_MODULES "E:/extra/modules/ip5306")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
|
||||||
@@ -10,6 +10,7 @@ project(new_kbd)
|
|||||||
|
|
||||||
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/inc)
|
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/events)
|
||||||
|
zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/ui)
|
||||||
|
|
||||||
zephyr_compile_definitions(
|
zephyr_compile_definitions(
|
||||||
LV_LVGL_H_INCLUDE_SIMPLE=1
|
LV_LVGL_H_INCLUDE_SIMPLE=1
|
||||||
@@ -27,7 +28,11 @@ target_sources(app PRIVATE
|
|||||||
src/main.c
|
src/main.c
|
||||||
src/events/battery_status_event.c
|
src/events/battery_status_event.c
|
||||||
src/events/config_event.c
|
src/events/config_event.c
|
||||||
|
src/events/display_theme_event.c
|
||||||
src/events/hid_boot_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_protocol_event.c
|
||||||
src/events/hid_report_event.c
|
src/events/hid_report_event.c
|
||||||
src/events/hid_tx_done_event.c
|
src/events/hid_tx_done_event.c
|
||||||
@@ -41,9 +46,9 @@ target_sources(app PRIVATE
|
|||||||
src/modules/ble_adv_ctrl_module.c
|
src/modules/ble_adv_ctrl_module.c
|
||||||
src/modules/ble_battery_module.c
|
src/modules/ble_battery_module.c
|
||||||
src/modules/ble_bond_module.c
|
src/modules/ble_bond_module.c
|
||||||
src/modules/ble_time_sync_module.c
|
|
||||||
src/modules/ble_slot_ctrl_module.c
|
src/modules/ble_slot_ctrl_module.c
|
||||||
src/modules/display_module.c
|
src/modules/display_module.c
|
||||||
|
src/modules/hid_host_command_module.c
|
||||||
src/modules/hid_tx_manager_module.c
|
src/modules/hid_tx_manager_module.c
|
||||||
src/modules/keyboard_module.c
|
src/modules/keyboard_module.c
|
||||||
src/modules/led_state_module.c
|
src/modules/led_state_module.c
|
||||||
@@ -52,6 +57,7 @@ target_sources(app PRIVATE
|
|||||||
src/modules/time_manager_module.c
|
src/modules/time_manager_module.c
|
||||||
src/modules/usb_hid_module.c
|
src/modules/usb_hid_module.c
|
||||||
src/modules/ble_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_small_18.c
|
||||||
src/ui/fonts/ui_font_keyboard_time_48.c
|
src/ui/fonts/ui_font_keyboard_time_48.c
|
||||||
)
|
)
|
||||||
|
|||||||
71
app.overlay
71
app.overlay
@@ -2,16 +2,7 @@
|
|||||||
|
|
||||||
/ {
|
/ {
|
||||||
chosen {
|
chosen {
|
||||||
zephyr,display = &st7789v3;
|
zephyr,boot-mode = &boot_mode0;
|
||||||
};
|
|
||||||
|
|
||||||
aliases {
|
|
||||||
backlight = &backlight;
|
|
||||||
};
|
|
||||||
|
|
||||||
zephyr,user {
|
|
||||||
vbat-en-gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>;
|
|
||||||
io-channels = <&adc 5>, <&adc 7>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
hid_dev_0: hid_dev_0 {
|
hid_dev_0: hid_dev_0 {
|
||||||
@@ -45,64 +36,12 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
&gpio0 {
|
&gpregret1 {
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&gpio1 {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&gpiote {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&led_0 {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&led_1 {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
/* 使能 SAADC,mode_switch_module 使用 channel 7 采样模式拨码电压。 */
|
|
||||||
&adc {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&i2c1 {
|
|
||||||
status = "okay";
|
status = "okay";
|
||||||
|
|
||||||
ip5305: pmic@75 {
|
boot_mode0: boot_mode@0 {
|
||||||
|
compatible = "zephyr,retention";
|
||||||
status = "okay";
|
status = "okay";
|
||||||
keepalive-interval-ms = <10000>;
|
reg = <0x0 0x1>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
&usbd {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
qdec: &qdec {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&spi3 {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&mipi_dbi {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&st7789v3 {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&pwm_leds {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|
||||||
&pwm0 {
|
|
||||||
status = "okay";
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
config BOARD_ATGUIGU_KEYBOARD_DONGLE
|
||||||
|
select SOC_NRF52833_QDAA
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
&pinctrl {
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/dts-v1/;
|
||||||
|
#include <nordic/nrf52833_qdaa.dtsi>
|
||||||
|
#include "atguigu_keyboard_dongle-pinctrl.dtsi"
|
||||||
|
|
||||||
|
/ {
|
||||||
|
model = "Keyboard Dongle";
|
||||||
|
compatible = "atguigu,atguigu-keyboard-dongle";
|
||||||
|
|
||||||
|
chosen {
|
||||||
|
zephyr,sram = &sram0;
|
||||||
|
zephyr,flash = &flash0;
|
||||||
|
zephyr,code-partition = &slot0_partition;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
&flash0 {
|
||||||
|
partitions {
|
||||||
|
compatible = "fixed-partitions";
|
||||||
|
#address-cells = <1>;
|
||||||
|
#size-cells = <1>;
|
||||||
|
|
||||||
|
boot_partition: partition@0 {
|
||||||
|
label = "mcuboot";
|
||||||
|
reg = <0x00000000 DT_SIZE_K(48)>;
|
||||||
|
};
|
||||||
|
|
||||||
|
slot0_partition: partition@c000 {
|
||||||
|
label = "image-0";
|
||||||
|
reg = <0x0000c000 DT_SIZE_K(220)>;
|
||||||
|
};
|
||||||
|
|
||||||
|
slot1_partition: partition@43000 {
|
||||||
|
label = "image-1";
|
||||||
|
reg = <0x00043000 DT_SIZE_K(220)>;
|
||||||
|
};
|
||||||
|
|
||||||
|
storage_partition: partition@7a000 {
|
||||||
|
label = "storage";
|
||||||
|
reg = <0x0007a000 DT_SIZE_K(24)>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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: []
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CONFIG_ARM_MPU=y
|
||||||
|
CONFIG_HW_STACK_PROTECTION=y
|
||||||
9
boards/atguigu/atguigu_keyboard_dongle/board.cmake
Normal file
9
boards/atguigu/atguigu_keyboard_dongle/board.cmake
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
set(OPENOCD_NRF5_SUBFAMILY "nrf52")
|
||||||
|
board_runner_args(jlink "--device=nRF52833_xxAA" "--speed=4000")
|
||||||
|
board_runner_args(pyocd "--target=nrf52833" "--frequency=4000000")
|
||||||
|
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake)
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake)
|
||||||
5
boards/atguigu/atguigu_keyboard_dongle/board.yml
Normal file
5
boards/atguigu/atguigu_keyboard_dongle/board.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
board:
|
||||||
|
name: atguigu_keyboard_dongle
|
||||||
|
vendor: atguigu
|
||||||
|
socs:
|
||||||
|
- name: nrf52833
|
||||||
@@ -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")
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
config BOARD_ATGUIGU_MINI_KEYBOARD
|
||||||
|
select SOC_NRF52840_QFAA
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
&pinctrl {
|
||||||
|
qdec_default: qdec_default {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(QDEC_A, 0, 10)>,
|
||||||
|
<NRF_PSEL(QDEC_B, 1, 6)>;
|
||||||
|
bias-pull-up;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
qdec_sleep: qdec_sleep {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(QDEC_A, 0, 10)>,
|
||||||
|
<NRF_PSEL(QDEC_B, 1, 6)>;
|
||||||
|
low-power-enable;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
led_spi_default: led_spi_default {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(SPIM_MOSI, 0, 20)>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
led_spi_sleep: led_spi_sleep {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(SPIM_MOSI, 0, 20)>;
|
||||||
|
low-power-enable;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
lcd_spi_default: lcd_spi_default {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(SPIM_SCK, 1, 13)>,
|
||||||
|
<NRF_PSEL(SPIM_MOSI, 0, 28)>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
lcd_spi_sleep: lcd_spi_sleep {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(SPIM_SCK, 1, 13)>,
|
||||||
|
<NRF_PSEL(SPIM_MOSI, 0, 28)>;
|
||||||
|
low-power-enable;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
i2c1_default: i2c1_default {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(TWIM_SDA, 1, 0)>,
|
||||||
|
<NRF_PSEL(TWIM_SCL, 0, 24)>;
|
||||||
|
bias-pull-up;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
i2c1_sleep: i2c1_sleep {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(TWIM_SDA, 1, 0)>,
|
||||||
|
<NRF_PSEL(TWIM_SCL, 0, 24)>;
|
||||||
|
low-power-enable;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pwm0_default: pwm0_default {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(PWM_OUT0, 1, 11)>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
pwm0_sleep: pwm0_sleep {
|
||||||
|
group1 {
|
||||||
|
psels = <NRF_PSEL(PWM_OUT0, 1, 11)>;
|
||||||
|
low-power-enable;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
240
boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.dts
Normal file
240
boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.dts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
/dts-v1/;
|
||||||
|
#include <nordic/nrf52840_qiaa.dtsi>
|
||||||
|
#include "atguigu_mini_keyboard-pinctrl.dtsi"
|
||||||
|
#include <zephyr/dt-bindings/pinctrl/nrf-pinctrl.h>
|
||||||
|
#include <zephyr/dt-bindings/led/led.h>
|
||||||
|
|
||||||
|
/ {
|
||||||
|
model = "Mini Keyboard, 17 keys";
|
||||||
|
compatible = "atguigu,atguigu-mini-keyboard";
|
||||||
|
|
||||||
|
chosen {
|
||||||
|
zephyr,sram = &sram0;
|
||||||
|
zephyr,flash = &flash0;
|
||||||
|
zephyr,code-partition = &slot0_partition;
|
||||||
|
zephyr,display = &st7789v3;
|
||||||
|
};
|
||||||
|
|
||||||
|
aliases {
|
||||||
|
backlight = &backlight;
|
||||||
|
};
|
||||||
|
|
||||||
|
mode_sense: mode-sense {
|
||||||
|
compatible = "voltage-divider";
|
||||||
|
io-channels = <&adc 5>;
|
||||||
|
output-ohms = <1>;
|
||||||
|
full-ohms = <1>;
|
||||||
|
};
|
||||||
|
|
||||||
|
battery_sense: battery-sense {
|
||||||
|
compatible = "voltage-divider";
|
||||||
|
io-channels = <&adc 7>;
|
||||||
|
output-ohms = <100000>;
|
||||||
|
full-ohms = <200000>;
|
||||||
|
power-gpios = <&gpio0 9 GPIO_ACTIVE_HIGH>;
|
||||||
|
power-on-sample-delay-us = <100>;
|
||||||
|
};
|
||||||
|
|
||||||
|
pwm_leds: pwm_leds {
|
||||||
|
compatible = "pwm-leds";
|
||||||
|
status = "okay";
|
||||||
|
backlight: pwm_led_0 {
|
||||||
|
pwms = <&pwm0 0 PWM_MSEC(10) PWM_POLARITY_INVERTED>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
led_0: led_0 {
|
||||||
|
compatible = "gpio-leds";
|
||||||
|
status = "okay";
|
||||||
|
|
||||||
|
chan0 {
|
||||||
|
gpios = <&gpio0 17 GPIO_ACTIVE_LOW>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
led_1: led_1 {
|
||||||
|
compatible = "gpio-leds";
|
||||||
|
status = "okay";
|
||||||
|
|
||||||
|
chan0 {
|
||||||
|
gpios = <&gpio1 2 GPIO_ACTIVE_LOW>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mipi_dbi: mipi_dbi {
|
||||||
|
compatible = "zephyr,mipi-dbi-spi";
|
||||||
|
status = "okay";
|
||||||
|
spi-dev = <&spi3>;
|
||||||
|
dc-gpios = <&gpio0 3 GPIO_ACTIVE_HIGH>;
|
||||||
|
reset-gpios = <&gpio1 10 GPIO_ACTIVE_LOW>;
|
||||||
|
write-only;
|
||||||
|
#address-cells = <1>;
|
||||||
|
#size-cells = <0>;
|
||||||
|
|
||||||
|
st7789v3: st7789v@0 {
|
||||||
|
compatible = "sitronix,st7789v";
|
||||||
|
status = "okay";
|
||||||
|
reg = <0>;
|
||||||
|
mipi-max-frequency = <32000000>;
|
||||||
|
width = <320>;
|
||||||
|
height = <172>;
|
||||||
|
x-offset = <0>;
|
||||||
|
y-offset = <34>;
|
||||||
|
vcom = <0x2b>;
|
||||||
|
gctrl = <0x35>;
|
||||||
|
vrhs = <0x11>;
|
||||||
|
vdvs = <0x20>;
|
||||||
|
mdac = <0xA0>;
|
||||||
|
lcm = <0x2c>;
|
||||||
|
colmod = <0x55>;
|
||||||
|
gamma = <0x01>;
|
||||||
|
porch-param = [ 0c 0c 00 33 33 ];
|
||||||
|
cmd2en-param = [ 5a 69 02 01 ];
|
||||||
|
pwctrl1-param = [ a4 a1 ];
|
||||||
|
pvgam-param = [ d0 00 02 07 0a 28 32 44 42 06 0e 12 14 17 ];
|
||||||
|
nvgam-param = [ d0 00 02 07 0a 28 31 54 47 0e 1c 17 1b 1e ];
|
||||||
|
ram-param = [ 00 f0 ];
|
||||||
|
rgb-param = [ c0 02 14 ];
|
||||||
|
mipi-mode = "MIPI_DBI_MODE_SPI_4WIRE";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
&spi2 {
|
||||||
|
status = "okay";
|
||||||
|
pinctrl-0 = <&led_spi_default>;
|
||||||
|
pinctrl-1 = <&led_spi_sleep>;
|
||||||
|
pinctrl-names = "default", "sleep";
|
||||||
|
cs-gpios = <&gpio0 21 GPIO_ACTIVE_LOW>;
|
||||||
|
|
||||||
|
led_strip: ws2812@0 {
|
||||||
|
compatible = "worldsemi,ws2812-spi";
|
||||||
|
supply-gpios = <&gpio0 13 GPIO_ACTIVE_HIGH>;
|
||||||
|
reg = <0>;
|
||||||
|
spi-max-frequency = <8000000>;
|
||||||
|
chain-length = <17>;
|
||||||
|
color-mapping = <LED_COLOR_ID_GREEN LED_COLOR_ID_RED LED_COLOR_ID_BLUE>;
|
||||||
|
spi-one-frame = <0xFC>;
|
||||||
|
spi-zero-frame = <0xC0>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
&i2c1 {
|
||||||
|
status = "okay";
|
||||||
|
pinctrl-0 = <&i2c1_default>;
|
||||||
|
pinctrl-1 = <&i2c1_sleep>;
|
||||||
|
pinctrl-names = "default", "sleep";
|
||||||
|
clock-frequency = <400000>;
|
||||||
|
|
||||||
|
ip5306: pmic@75 {
|
||||||
|
status = "okay";
|
||||||
|
compatible = "injoinic,ip5306";
|
||||||
|
reg = <0x75>;
|
||||||
|
keepalive-gpios = <&gpio0 22 GPIO_ACTIVE_LOW>;
|
||||||
|
keepalive-interval-ms = <10000>;
|
||||||
|
keepalive-offload;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 编码器 */
|
||||||
|
&qdec {
|
||||||
|
status = "okay";
|
||||||
|
/* 引用上面定义的标签 */
|
||||||
|
pinctrl-0 = <&qdec_default>;
|
||||||
|
pinctrl-1 = <&qdec_sleep>;
|
||||||
|
/* 指定 pinctrl-0 为默认,pinctrl-1 为睡眠 */
|
||||||
|
pinctrl-names = "default", "sleep";
|
||||||
|
|
||||||
|
/* 别忘了 QDEC 必须的两个属性 */
|
||||||
|
steps = <40>;
|
||||||
|
led-pre = <0>;
|
||||||
|
};
|
||||||
|
|
||||||
|
&flash0 {
|
||||||
|
partitions {
|
||||||
|
compatible = "fixed-partitions";
|
||||||
|
#address-cells = <1>;
|
||||||
|
#size-cells = <1>;
|
||||||
|
|
||||||
|
boot_partition: partition@0 {
|
||||||
|
label = "mcuboot";
|
||||||
|
reg = <0x00000000 DT_SIZE_K(48)>;
|
||||||
|
};
|
||||||
|
|
||||||
|
slot0_partition: partition@c000 {
|
||||||
|
label = "image-0";
|
||||||
|
reg = <0x0000c000 DT_SIZE_K(472)>;
|
||||||
|
};
|
||||||
|
|
||||||
|
slot1_partition: partition@82000 {
|
||||||
|
label = "image-1";
|
||||||
|
reg = <0x00082000 DT_SIZE_K(472)>;
|
||||||
|
};
|
||||||
|
|
||||||
|
storage_partition: partition@f8000 {
|
||||||
|
label = "storage";
|
||||||
|
reg = <0x000f8000 DT_SIZE_K(32)>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
&spi3 {
|
||||||
|
status = "okay";
|
||||||
|
pinctrl-0 = <&lcd_spi_default>;
|
||||||
|
pinctrl-1 = <&lcd_spi_sleep>;
|
||||||
|
pinctrl-names = "default", "sleep";
|
||||||
|
cs-gpios = <&gpio0 2 GPIO_ACTIVE_LOW>;
|
||||||
|
};
|
||||||
|
|
||||||
|
&adc {
|
||||||
|
status = "okay";
|
||||||
|
#address-cells = <1>;
|
||||||
|
#size-cells = <0>;
|
||||||
|
|
||||||
|
channel@5 {
|
||||||
|
reg = <5>;
|
||||||
|
zephyr,gain = "ADC_GAIN_1_6";
|
||||||
|
zephyr,reference = "ADC_REF_INTERNAL";
|
||||||
|
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 20)>;
|
||||||
|
zephyr,input-positive = <NRF_SAADC_AIN5>;
|
||||||
|
zephyr,resolution = <12>;
|
||||||
|
};
|
||||||
|
|
||||||
|
channel@7 {
|
||||||
|
reg = <7>;
|
||||||
|
zephyr,gain = "ADC_GAIN_1_6";
|
||||||
|
zephyr,reference = "ADC_REF_INTERNAL";
|
||||||
|
zephyr,acquisition-time = <ADC_ACQ_TIME(ADC_ACQ_TIME_MICROSECONDS, 20)>;
|
||||||
|
zephyr,input-positive = <NRF_SAADC_AIN7>;
|
||||||
|
zephyr,resolution = <12>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
&pwm0 {
|
||||||
|
status = "okay";
|
||||||
|
pinctrl-0 = <&pwm0_default>;
|
||||||
|
pinctrl-1 = <&pwm0_sleep>;
|
||||||
|
pinctrl-names = "default", "sleep";
|
||||||
|
};
|
||||||
|
|
||||||
|
&gpio0 {
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&gpio1 {
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&gpiote {
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&usbd {
|
||||||
|
status = "okay";
|
||||||
|
};
|
||||||
|
|
||||||
|
&uicr {
|
||||||
|
nfct-pins-as-gpios;
|
||||||
|
gpio-as-nreset;
|
||||||
|
};
|
||||||
@@ -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: []
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CONFIG_ARM_MPU=y
|
||||||
|
CONFIG_HW_STACK_PROTECTION=y
|
||||||
9
boards/atguigu/atguigu_mini_keyboard/board.cmake
Normal file
9
boards/atguigu/atguigu_mini_keyboard/board.cmake
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
set(OPENOCD_NRF5_SUBFAMILY "nrf52")
|
||||||
|
board_runner_args(jlink "--device=nRF52840_xxAA" "--speed=4000")
|
||||||
|
board_runner_args(pyocd "--target=nrf52840" "--frequency=4000000")
|
||||||
|
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/nrfutil.board.cmake)
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/nrfjprog.board.cmake)
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/pyocd.board.cmake)
|
||||||
|
include(${ZEPHYR_BASE}/boards/common/openocd-nrf5.board.cmake)
|
||||||
5
boards/atguigu/atguigu_mini_keyboard/board.yml
Normal file
5
boards/atguigu/atguigu_mini_keyboard/board.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
board:
|
||||||
|
name: atguigu_mini_keyboard
|
||||||
|
vendor: atguigu
|
||||||
|
socs:
|
||||||
|
- name: nrf52840
|
||||||
2
boards/atguigu/atguigu_mini_keyboard/pre_dt_board.cmake
Normal file
2
boards/atguigu/atguigu_mini_keyboard/pre_dt_board.cmake
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Suppress "unique_unit_address_if_enabled" to handle some overlaps
|
||||||
|
list(APPEND EXTRA_DTC_FLAGS "-Wno-unique_unit_address_if_enabled")
|
||||||
281
docs/ble_time_sync_pc_host.md
Normal file
281
docs/ble_time_sync_pc_host.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# BLE 时间同步服务 PC 上位机接入文档
|
||||||
|
|
||||||
|
## 1. 概述
|
||||||
|
|
||||||
|
当前项目实现了一套独立的 BLE 时间同步服务,供 PC 上位机在蓝牙连接后向键盘写入当前 UTC 时间、时区和时间精度。
|
||||||
|
|
||||||
|
这套服务的设计目标是:
|
||||||
|
|
||||||
|
- 不走 HID 报告通道,避免与键盘输入链路耦合。
|
||||||
|
- BLE 侧只负责协议适配,实际时间状态统一交给 `time_manager` 管理。
|
||||||
|
- 主机只负责“下发时间”,设备不通过此服务回读时间,也不通过 notify 返回确认。
|
||||||
|
|
||||||
|
结论上,这是一条:
|
||||||
|
|
||||||
|
- 自定义 GATT Service
|
||||||
|
- 单个可写 Characteristic
|
||||||
|
- 仅支持写入
|
||||||
|
- 需要加密连接
|
||||||
|
|
||||||
|
的单向时间同步通道。
|
||||||
|
|
||||||
|
## 2. GATT 定义
|
||||||
|
|
||||||
|
### 2.1 Service UUID
|
||||||
|
|
||||||
|
`0b7f5000-38d2-4f62-8f6f-36c4fd73a110`
|
||||||
|
|
||||||
|
### 2.2 Characteristic UUID
|
||||||
|
|
||||||
|
`0b7f5001-38d2-4f62-8f6f-36c4fd73a110`
|
||||||
|
|
||||||
|
### 2.3 Characteristic 属性
|
||||||
|
|
||||||
|
- `Write`
|
||||||
|
- `Write Without Response`
|
||||||
|
|
||||||
|
### 2.4 Characteristic 权限
|
||||||
|
|
||||||
|
- `Write Encrypted`
|
||||||
|
|
||||||
|
也就是说,上位机在写入前必须先完成配对/加密。未加密链路下,写入会被 GATT 层拒绝。
|
||||||
|
|
||||||
|
## 3. 设备端处理逻辑
|
||||||
|
|
||||||
|
### 3.1 依赖条件
|
||||||
|
|
||||||
|
设备端只有在以下两个条件同时满足时,才接受时间写入:
|
||||||
|
|
||||||
|
- BLE 栈已经 ready
|
||||||
|
- `time_manager` 已经 ready
|
||||||
|
|
||||||
|
如果模块尚未 ready,写入会返回 ATT 错误。
|
||||||
|
|
||||||
|
### 3.2 写入成功后的行为
|
||||||
|
|
||||||
|
设备端收到合法 payload 后会:
|
||||||
|
|
||||||
|
1. 校验版本、长度和 flags。
|
||||||
|
2. 解析 `utc_ms`、`timezone_min`、`accuracy_ms`。
|
||||||
|
3. 自动把同步来源标记为 `BLE`。
|
||||||
|
4. 投递 `time_sync_event` 给 `time_manager`。
|
||||||
|
5. `time_manager` 更新运行时钟状态,并延迟异步写入 settings。
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
- 当前 BLE 时间同步服务没有 `Read` 或 `Notify` 能力。
|
||||||
|
- 主机拿到 GATT 写成功,只能说明设备接受了这次写入。
|
||||||
|
- 设备不会通过这个服务主动回传“当前时间”。
|
||||||
|
|
||||||
|
## 4. 数据包格式
|
||||||
|
|
||||||
|
### 4.1 总长度
|
||||||
|
|
||||||
|
固定 `16` 字节。
|
||||||
|
|
||||||
|
### 4.2 字段布局
|
||||||
|
|
||||||
|
| 偏移 | 长度 | 字段名 | 类型 | 字节序 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 0 | 1 | `version` | `uint8` | - |
|
||||||
|
| 1 | 1 | `flags` | `uint8` | - |
|
||||||
|
| 2 | 2 | `timezone_min` | `int16` | little-endian |
|
||||||
|
| 4 | 8 | `utc_ms` | `uint64` | little-endian |
|
||||||
|
| 12 | 4 | `accuracy_ms` | `uint32` | little-endian |
|
||||||
|
|
||||||
|
### 4.3 当前协议版本
|
||||||
|
|
||||||
|
- `version = 1`
|
||||||
|
|
||||||
|
### 4.4 flags 定义
|
||||||
|
|
||||||
|
当前只定义 1 个 bit:
|
||||||
|
|
||||||
|
- `BIT(0)` = `TIMEZONE_VALID`
|
||||||
|
|
||||||
|
因此当前版本必须满足:
|
||||||
|
|
||||||
|
- `flags & 0x01 != 0`
|
||||||
|
|
||||||
|
推荐主机固定写:
|
||||||
|
|
||||||
|
- `flags = 0x01`
|
||||||
|
|
||||||
|
## 5. 字段语义
|
||||||
|
|
||||||
|
### 5.1 `timezone_min`
|
||||||
|
|
||||||
|
单位:分钟。
|
||||||
|
|
||||||
|
含义:本地时区相对 UTC 的偏移。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
|
||||||
|
- 中国标准时间 UTC+8 -> `480`
|
||||||
|
- 印度 UTC+5:30 -> `330`
|
||||||
|
- UTC-5 -> `-300`
|
||||||
|
|
||||||
|
设备端当前接受范围:
|
||||||
|
|
||||||
|
- `-1440 ~ +1440`
|
||||||
|
|
||||||
|
### 5.2 `utc_ms`
|
||||||
|
|
||||||
|
单位:毫秒。
|
||||||
|
|
||||||
|
含义:UTC 时间戳,不带本地时区偏移。
|
||||||
|
|
||||||
|
要求:
|
||||||
|
|
||||||
|
- 必须大于 `0`
|
||||||
|
|
||||||
|
### 5.3 `accuracy_ms`
|
||||||
|
|
||||||
|
单位:毫秒。
|
||||||
|
|
||||||
|
含义:本次时间来源的估计精度。
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
- 若上位机没有可靠精度信息,可直接写 `0`
|
||||||
|
- 若来自系统时间并已做网络对时,也可以给一个保守值,例如 `50` 或 `100`
|
||||||
|
|
||||||
|
## 6. 主机写入建议
|
||||||
|
|
||||||
|
### 6.1 推荐使用 Write Request
|
||||||
|
|
||||||
|
虽然设备同时支持:
|
||||||
|
|
||||||
|
- Write
|
||||||
|
- Write Without Response
|
||||||
|
|
||||||
|
但 PC 上位机侧建议优先使用 **Write Request(带响应写)**,原因是:
|
||||||
|
|
||||||
|
- 更容易拿到 ATT 层成功/失败结果
|
||||||
|
- 更方便在开发阶段定位协议错误
|
||||||
|
- 更适合作为配置/同步类命令
|
||||||
|
|
||||||
|
### 6.2 推荐时序
|
||||||
|
|
||||||
|
1. 扫描并连接设备。
|
||||||
|
2. 完成配对/加密。
|
||||||
|
3. 发现时间同步 Service 和 Characteristic。
|
||||||
|
4. 组包为固定 16 字节 payload。
|
||||||
|
5. 执行一次带响应写入。
|
||||||
|
6. 若写成功,可视为设备已接受此次同步请求。
|
||||||
|
|
||||||
|
## 7. Python 示例
|
||||||
|
|
||||||
|
下面示例基于 `bleak`,演示如何向设备写入当前系统时间。
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
import struct
|
||||||
|
import time
|
||||||
|
from bleak import BleakClient
|
||||||
|
|
||||||
|
TIME_SYNC_CHAR_UUID = "0b7f5001-38d2-4f62-8f6f-36c4fd73a110"
|
||||||
|
|
||||||
|
|
||||||
|
def build_time_sync_payload(timezone_min: int, accuracy_ms: int = 0) -> bytes:
|
||||||
|
version = 1
|
||||||
|
flags = 0x01 # TIMEZONE_VALID
|
||||||
|
utc_ms = int(time.time() * 1000)
|
||||||
|
return struct.pack("<BBhQI", version, flags, timezone_min, utc_ms, accuracy_ms)
|
||||||
|
|
||||||
|
|
||||||
|
async def main(address: str):
|
||||||
|
payload = build_time_sync_payload(timezone_min=480, accuracy_ms=50)
|
||||||
|
|
||||||
|
async with BleakClient(address) as client:
|
||||||
|
await client.write_gatt_char(
|
||||||
|
TIME_SYNC_CHAR_UUID,
|
||||||
|
payload,
|
||||||
|
response=True,
|
||||||
|
)
|
||||||
|
print("time sync write done")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main("XX:XX:XX:XX:XX:XX"))
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `struct.pack("<BBhQI", ...)` 对应协议定义的 little-endian 格式。
|
||||||
|
- `timezone_min=480` 表示 UTC+8。
|
||||||
|
- `response=True` 表示使用带响应写入。
|
||||||
|
|
||||||
|
## 8. 常见错误与排查
|
||||||
|
|
||||||
|
### 8.1 写入被拒绝
|
||||||
|
|
||||||
|
常见原因:
|
||||||
|
|
||||||
|
- 还没有完成配对/加密
|
||||||
|
- 写入长度不是 16 字节
|
||||||
|
- `version != 1`
|
||||||
|
- `flags` 未设置 `TIMEZONE_VALID`
|
||||||
|
- 使用了 prepare write / long write
|
||||||
|
|
||||||
|
### 8.2 写入成功但设备时间没更新
|
||||||
|
|
||||||
|
优先检查:
|
||||||
|
|
||||||
|
- 设备日志是否出现 `Accepted BLE time sync ...`
|
||||||
|
- 设备日志是否出现 `Time synchronized src=1 ...`
|
||||||
|
- 上位机是否错误地把本地时间直接当成 UTC 写入
|
||||||
|
|
||||||
|
### 8.3 时区显示错误
|
||||||
|
|
||||||
|
最常见原因:
|
||||||
|
|
||||||
|
- 主机把“本地毫秒时间戳”写进了 `utc_ms`
|
||||||
|
- 同时又传了 `timezone_min`
|
||||||
|
|
||||||
|
正确做法是:
|
||||||
|
|
||||||
|
- `utc_ms` 始终写 UTC 毫秒
|
||||||
|
- 本地时区单独放在 `timezone_min`
|
||||||
|
|
||||||
|
## 9. ATT 错误码语义
|
||||||
|
|
||||||
|
设备端当前会返回的典型 ATT 错误如下:
|
||||||
|
|
||||||
|
- `BT_ATT_ERR_UNLIKELY`
|
||||||
|
- 模块尚未 ready,例如 BLE 栈或 `time_manager` 还未初始化完成
|
||||||
|
- `BT_ATT_ERR_INVALID_OFFSET`
|
||||||
|
- 非零 offset 写入
|
||||||
|
- `BT_ATT_ERR_ATTRIBUTE_NOT_LONG`
|
||||||
|
- 使用 prepare write / 长写流程
|
||||||
|
- `BT_ATT_ERR_VALUE_NOT_ALLOWED`
|
||||||
|
- payload 长度、版本、flags 或字段内容不合法
|
||||||
|
|
||||||
|
## 10. 当前服务边界
|
||||||
|
|
||||||
|
当前版本仅支持:
|
||||||
|
|
||||||
|
- 主机 -> 设备 单向校时
|
||||||
|
|
||||||
|
当前不支持:
|
||||||
|
|
||||||
|
- BLE 读回当前设备时间
|
||||||
|
- 校时结果通知
|
||||||
|
- 历史同步记录查询
|
||||||
|
- DST 单独字段
|
||||||
|
- 通过 payload 指定同步来源
|
||||||
|
|
||||||
|
同步来源在设备端固定记为:
|
||||||
|
|
||||||
|
- `TIME_SYNC_SOURCE_BLE`
|
||||||
|
|
||||||
|
## 11. 对接建议
|
||||||
|
|
||||||
|
对 PC 上位机实现建议如下:
|
||||||
|
|
||||||
|
- 首选带响应写入,不要默认用 write without response
|
||||||
|
- 时间戳统一使用 UTC 毫秒
|
||||||
|
- 时区单独使用分钟偏移
|
||||||
|
- 每次建立加密连接后可主动同步一次时间
|
||||||
|
- 若 PC 有系统授时状态,可把估计精度填入 `accuracy_ms`
|
||||||
|
|
||||||
314
docs/bluetooth_sig_ble_profiles_summary.md
Normal file
314
docs/bluetooth_sig_ble_profiles_summary.md
Normal file
@@ -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` 工程的实现关注点。
|
||||||
926
docs/ncs_v3_2_3_gatt_services_summary.md
Normal file
926
docs/ncs_v3_2_3_gatt_services_summary.md
Normal file
@@ -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` | `<zephyr/bluetooth/services/ans.h>` | 对手表/手环输出通知类别与未读数 |
|
||||||
|
| Battery Service | `CONFIG_BT_BAS` | `<zephyr/bluetooth/services/bas.h>` | 暴露电量、电池状态 |
|
||||||
|
| Current Time Service | `CONFIG_BT_CTS` | `<zephyr/bluetooth/services/cts.h>` | 给客户端提供当前时间/校时 |
|
||||||
|
| Device Information Service | `CONFIG_BT_DIS` | `<zephyr/bluetooth/services/dis.h>` | 暴露厂商、型号、版本、PnP ID 等静态信息 |
|
||||||
|
| Heart Rate Service | `CONFIG_BT_HRS` | `<zephyr/bluetooth/services/hrs.h>` | 心率设备 |
|
||||||
|
| Immediate Alert Service | `CONFIG_BT_IAS` | `<zephyr/bluetooth/services/ias.h>` | 查找设备、远程触发蜂鸣/闪灯 |
|
||||||
|
| Nordic UART Service(Zephyr 版) | `CONFIG_BT_ZEPHYR_NUS` | `<zephyr/bluetooth/services/nus.h>` | 简单双向串口透传 |
|
||||||
|
| Object Transfer Service | `CONFIG_BT_OTS` | `<zephyr/bluetooth/services/ots.h>` | 面向对象的数据传输 |
|
||||||
|
| Tx Power Service | `CONFIG_BT_TPS` | 无独立公共 API 头 | 暴露当前发射功率 |
|
||||||
|
|
||||||
|
### 2.2 NCS / Nordic 扩展服务
|
||||||
|
|
||||||
|
| Service | Kconfig | 头文件 | 典型用途 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Bond Management Service | `CONFIG_BT_BMS` | `<bluetooth/services/bms.h>` | 让对端触发删 bond |
|
||||||
|
| Continuous Glucose Monitoring Service | `CONFIG_BT_CGMS` | `<bluetooth/services/cgms.h>` | 连续血糖监测 |
|
||||||
|
| Direction and Distance Finding Service | `CONFIG_BT_DDFS` | `<bluetooth/services/ddfs.h>` | 方位/距离测量结果输出 |
|
||||||
|
| Fast Pair Provider Service | `CONFIG_BT_FAST_PAIR` + `CONFIG_BT_FAST_PAIR_GATT_SERVICE` | `<bluetooth/services/fast_pair/fast_pair.h>` | Google Fast Pair |
|
||||||
|
| Human Interface Device Service | `CONFIG_BT_HIDS` | `<bluetooth/services/hids.h>` | BLE 键盘、鼠标、输入设备 |
|
||||||
|
| Latency Service | `CONFIG_BT_LATENCY` | `<bluetooth/services/latency.h>` | 延迟测试/回环 |
|
||||||
|
| LED Button Service | `CONFIG_BT_LBS` | `<bluetooth/services/lbs.h>` | 示例级 LED/Button 交互 |
|
||||||
|
| Memfault Diagnostic Service | `CONFIG_BT_MDS` | `<bluetooth/services/mds.h>` | Memfault 诊断数据导出 |
|
||||||
|
| Nordic Status Message Service | `CONFIG_BT_NSMS` | `<bluetooth/services/nsms.h>` | 暴露一段可读状态文本 |
|
||||||
|
| Nordic UART Service(Nordic 旧版) | `CONFIG_BT_NUS` | `<bluetooth/services/nus.h>` | NCS 样例常用串口透传 |
|
||||||
|
| Ranging Service | `CONFIG_BT_RAS` + `CONFIG_BT_RAS_RRSP` | `<bluetooth/services/ras.h>` | Channel Sounding / Ranging 服务端 |
|
||||||
|
| Running Speed and Cadence Service | `CONFIG_BT_RSCS` | `<bluetooth/services/rscs.h>` | 跑步速度步频 |
|
||||||
|
| Throughput Service | `CONFIG_BT_THROUGHPUT` | `<bluetooth/services/throughput.h>` | 吞吐测试 |
|
||||||
|
| Wi-Fi Provisioning Service | `CONFIG_BT_WIFI_PROV` | `<bluetooth/services/wifi_provisioning.h>` | 通过 BLE 给设备配 Wi-Fi |
|
||||||
|
|
||||||
|
## 3. 使用方式总规律
|
||||||
|
|
||||||
|
大多数服务都遵循类似流程:
|
||||||
|
|
||||||
|
1. 在 `prj.conf` 里打开对应 `CONFIG_BT_*`
|
||||||
|
2. 包含头文件
|
||||||
|
3. 若服务需要运行时初始化,则在 `bt_enable()` 前后调用对应 `init/register`
|
||||||
|
4. 连接建立后,通过通知/读写回调/API 更新服务数据
|
||||||
|
5. 在广告里决定是否放服务 UUID,但这不是注册服务的必要条件
|
||||||
|
|
||||||
|
有三类例外要先记住:
|
||||||
|
|
||||||
|
- **纯 Kconfig 静态服务**:例如 `DIS`、`TPS`,启用配置后就自动挂到 GATT DB,上层几乎没有运行时 API
|
||||||
|
- **宏定义式实例服务**:例如 `NSMS`、`Zephyr NUS` 多实例、`HIDS`,需要先用宏定义实例,再初始化或更新
|
||||||
|
- **复杂协议服务**:例如 `OTS`、`RAS`、`Fast Pair`,除了开 Kconfig,还要按它自己的状态机/回调/缓冲机制接入
|
||||||
|
|
||||||
|
## 4. 逐项说明
|
||||||
|
|
||||||
|
## 4.1 Zephyr 标准服务
|
||||||
|
|
||||||
|
### 4.1.1 Alert Notification Service, `ANS`
|
||||||
|
|
||||||
|
源码位置:
|
||||||
|
|
||||||
|
- `zephyr/subsys/bluetooth/services/ans.c`
|
||||||
|
- `zephyr/include/zephyr/bluetooth/services/ans.h`
|
||||||
|
|
||||||
|
启用方法:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT=y
|
||||||
|
CONFIG_BT_PERIPHERAL=y
|
||||||
|
CONFIG_BT_ANS=y
|
||||||
|
```
|
||||||
|
|
||||||
|
怎么用:
|
||||||
|
|
||||||
|
- 先在 Kconfig 里决定支持哪些提醒类别,例如 `CONFIG_BT_ANS_NALRT_CAT_EMAIL=y`
|
||||||
|
- 运行时如果还想动态设置支持位图,可以调用 `bt_ans_set_new_alert_support_category()` 和 `bt_ans_set_unread_support_category()`
|
||||||
|
- 有新提醒时调用 `bt_ans_notify_new_alert(conn, category, num_new, text)`
|
||||||
|
- 未读数变化时调用 `bt_ans_set_unread_count(conn, category, unread)`
|
||||||
|
|
||||||
|
适合场景:
|
||||||
|
|
||||||
|
- 手环、手表、桌面提醒器
|
||||||
|
- 你只想同步“有提醒/未读数/提醒类别”,而不是完整消息正文
|
||||||
|
|
||||||
|
注意点:
|
||||||
|
|
||||||
|
- 头文件明确写了 `bt_ans_notify_new_alert()` / `bt_ans_set_unread_count()` 会拿互斥锁,**不要从 BT RX 线程或 System Workqueue 里直接调用**
|
||||||
|
- 这个服务偏“提醒镜像”,不适合做完整消息中心
|
||||||
|
|
||||||
|
### 4.1.2 Battery Service, `BAS`
|
||||||
|
|
||||||
|
源码位置:
|
||||||
|
|
||||||
|
- `zephyr/subsys/bluetooth/services/bas/`
|
||||||
|
- `zephyr/include/zephyr/bluetooth/services/bas.h`
|
||||||
|
|
||||||
|
启用方法:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT=y
|
||||||
|
CONFIG_BT_PERIPHERAL=y
|
||||||
|
CONFIG_BT_BAS=y
|
||||||
|
```
|
||||||
|
|
||||||
|
如果要扩展电池状态:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT_BAS_BLS=y
|
||||||
|
CONFIG_BT_BAS_BCS=y
|
||||||
|
```
|
||||||
|
|
||||||
|
怎么用:
|
||||||
|
|
||||||
|
- 最基本只需要周期性调用 `bt_bas_set_battery_level(level)`,把 0..100 的电量同步给客户端
|
||||||
|
- 读取当前缓存值可用 `bt_bas_get_battery_level()`
|
||||||
|
- 如果启用了扩展状态特征,可继续设置:
|
||||||
|
- `bt_bas_bls_set_battery_present()`
|
||||||
|
- `bt_bas_bls_set_battery_charge_state()`
|
||||||
|
- `bt_bas_bls_set_battery_charge_level()`
|
||||||
|
- `bt_bas_bls_set_service_required()`
|
||||||
|
- `bt_bas_bls_set_battery_fault()`
|
||||||
|
|
||||||
|
适合场景:
|
||||||
|
|
||||||
|
- 键盘、鼠标、耳机、手表、遥控器等所有电池供电外设
|
||||||
|
|
||||||
|
对你当前项目的价值:
|
||||||
|
|
||||||
|
- 这是 `new_kbd` 最应该启用的标准服务之一
|
||||||
|
- 你已经有 `ble_battery_module.c`,它天然适合映射到 BAS
|
||||||
|
|
||||||
|
### 4.1.3 Current Time Service, `CTS`
|
||||||
|
|
||||||
|
源码位置:
|
||||||
|
|
||||||
|
- `zephyr/subsys/bluetooth/services/cts.c`
|
||||||
|
- `zephyr/include/zephyr/bluetooth/services/cts.h`
|
||||||
|
|
||||||
|
启用方法:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT=y
|
||||||
|
CONFIG_BT_PERIPHERAL=y
|
||||||
|
CONFIG_BT_CTS=y
|
||||||
|
```
|
||||||
|
|
||||||
|
可选工具 API:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT_CTS_HELPER_API=y
|
||||||
|
```
|
||||||
|
|
||||||
|
怎么用:
|
||||||
|
|
||||||
|
- 定义 `struct bt_cts_cb`
|
||||||
|
- 至少实现 `fill_current_cts_time()`,因为服务在被读或发通知时要靠它拿当前时间
|
||||||
|
- 如果允许对端写时间,再实现 `cts_time_write()`
|
||||||
|
- 如果关心客户端是否订阅时间更新,实现 `notification_changed()`
|
||||||
|
- 初始化时调用 `bt_cts_init(&cb)`
|
||||||
|
- 时间变化后调用 `bt_cts_send_notification(reason)`
|
||||||
|
|
||||||
|
最小使用思路:
|
||||||
|
|
||||||
|
1. 系统自己维护 RTC / Unix 时间
|
||||||
|
2. CTS 被读时把当前时间填进 `struct bt_cts_time_format`
|
||||||
|
3. 如果时间是由手机写入的,就在 `cts_time_write()` 里反写到系统时钟
|
||||||
|
|
||||||
|
### 4.1.4 Device Information Service, `DIS`
|
||||||
|
|
||||||
|
源码位置:
|
||||||
|
|
||||||
|
- `zephyr/subsys/bluetooth/services/dis.c`
|
||||||
|
- `zephyr/subsys/bluetooth/services/Kconfig.dis`
|
||||||
|
|
||||||
|
启用方法:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT=y
|
||||||
|
CONFIG_BT_PERIPHERAL=y
|
||||||
|
CONFIG_BT_DIS=y
|
||||||
|
CONFIG_BT_DIS_MANUF_NAME=y
|
||||||
|
CONFIG_BT_DIS_MANUF_NAME_STR="Your Company"
|
||||||
|
CONFIG_BT_DIS_MODEL_NUMBER=y
|
||||||
|
CONFIG_BT_DIS_MODEL_NUMBER_STR="KBD-01"
|
||||||
|
CONFIG_BT_DIS_PNP=y
|
||||||
|
```
|
||||||
|
|
||||||
|
怎么用:
|
||||||
|
|
||||||
|
- `DIS` 基本是**纯配置型服务**
|
||||||
|
- 你在 `prj.conf` 里打开需要的特征值并填字符串/ID,服务会静态注册
|
||||||
|
- 常用字段包括:
|
||||||
|
- 厂商名
|
||||||
|
- 型号
|
||||||
|
- 序列号
|
||||||
|
- FW/HW/SW 版本
|
||||||
|
- PnP ID
|
||||||
|
- 医疗设备 UDI
|
||||||
|
|
||||||
|
适合场景:
|
||||||
|
|
||||||
|
- 几乎所有 BLE 外设都建议启用
|
||||||
|
|
||||||
|
对键盘项目的建议:
|
||||||
|
|
||||||
|
- 至少填 `Manufacturer Name`、`Model Number`、`FW Revision`
|
||||||
|
- 如果你将来打算做 HID 认证/系统识别,`PnP ID` 也建议补齐
|
||||||
|
|
||||||
|
### 4.1.5 Heart Rate Service, `HRS`
|
||||||
|
|
||||||
|
源码位置:
|
||||||
|
|
||||||
|
- `zephyr/subsys/bluetooth/services/hrs.c`
|
||||||
|
- `zephyr/include/zephyr/bluetooth/services/hrs.h`
|
||||||
|
|
||||||
|
启用方法:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT=y
|
||||||
|
CONFIG_BT_PERIPHERAL=y
|
||||||
|
CONFIG_BT_HRS=y
|
||||||
|
```
|
||||||
|
|
||||||
|
怎么用:
|
||||||
|
|
||||||
|
- 若要处理控制点请求,定义 `struct bt_hrs_cb` 并注册 `bt_hrs_cb_register(&cb)`
|
||||||
|
- 上报新心率时调用 `bt_hrs_notify(heartrate)`
|
||||||
|
- 如果支持 Energy Expended 重置,则在 `ctrl_point_write()` 里处理 `BT_HRS_CONTROL_POINT_RESET_ENERGY_EXPANDED_REQ`
|
||||||
|
|
||||||
|
适合场景:
|
||||||
|
|
||||||
|
- 心率带、健康设备、运动设备
|
||||||
|
|
||||||
|
### 4.1.6 Immediate Alert Service, `IAS`
|
||||||
|
|
||||||
|
源码位置:
|
||||||
|
|
||||||
|
- `zephyr/subsys/bluetooth/services/ias/ias.c`
|
||||||
|
- `zephyr/include/zephyr/bluetooth/services/ias.h`
|
||||||
|
- 参考样例:`zephyr/samples/bluetooth/peripheral/src/main.c`
|
||||||
|
|
||||||
|
启用方法:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT=y
|
||||||
|
CONFIG_BT_PERIPHERAL=y
|
||||||
|
CONFIG_BT_IAS=y
|
||||||
|
```
|
||||||
|
|
||||||
|
怎么用:
|
||||||
|
|
||||||
|
- 用 `BT_IAS_CB_DEFINE(name)` 定义回调对象
|
||||||
|
- 在回调里实现:
|
||||||
|
- `no_alert()`
|
||||||
|
- `mild_alert()`
|
||||||
|
- `high_alert()`
|
||||||
|
- 远端写 Alert Level 时,服务会自动回调你
|
||||||
|
- 如果本机已经在报警,且你想主动停掉当前告警,可调用 `bt_ias_local_alert_stop()`
|
||||||
|
|
||||||
|
典型逻辑:
|
||||||
|
|
||||||
|
- `mild_alert()` 里短鸣、低频闪烁
|
||||||
|
- `high_alert()` 里持续蜂鸣或高亮闪烁
|
||||||
|
- `no_alert()` 里停蜂鸣、灭灯
|
||||||
|
|
||||||
|
### 4.1.7 Nordic UART Service, `Zephyr 版 NUS`
|
||||||
|
|
||||||
|
源码位置:
|
||||||
|
|
||||||
|
- `zephyr/subsys/bluetooth/services/nus/`
|
||||||
|
- `zephyr/include/zephyr/bluetooth/services/nus.h`
|
||||||
|
- 参考样例:`zephyr/samples/bluetooth/peripheral_nus`
|
||||||
|
|
||||||
|
启用方法:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT=y
|
||||||
|
CONFIG_BT_PERIPHERAL=y
|
||||||
|
CONFIG_BT_ZEPHYR_NUS=y
|
||||||
|
```
|
||||||
|
|
||||||
|
可选:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT_ZEPHYR_NUS_DEFAULT_INSTANCE=y
|
||||||
|
CONFIG_BT_ZEPHYR_NUS_AUTO_START_BLUETOOTH=y
|
||||||
|
```
|
||||||
|
|
||||||
|
怎么用:
|
||||||
|
|
||||||
|
- 最简单:使用默认实例
|
||||||
|
- 定义 `struct bt_nus_cb`
|
||||||
|
- 注册 `bt_nus_cb_register(&cb, ctx)`
|
||||||
|
- 对端写 RX 特征值时,你的 `received()` 回调会收到数据
|
||||||
|
- 要发数据给对端时,调用 `bt_nus_send(conn, data, len)`
|
||||||
|
|
||||||
|
高级用法:
|
||||||
|
|
||||||
|
- 这个版本支持多实例
|
||||||
|
- 你可以先用 `BT_NUS_INST_DEFINE(name)` 定义多个 NUS 端点
|
||||||
|
- 然后对每个实例调用 `bt_nus_inst_cb_register()` / `bt_nus_inst_send()`
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
- 新代码如果想做“可扩展串口通道”,Zephyr 版 NUS 比 Nordic 旧版更现代
|
||||||
|
|
||||||
|
### 4.1.8 Object Transfer Service, `OTS`
|
||||||
|
|
||||||
|
源码位置:
|
||||||
|
|
||||||
|
- `zephyr/subsys/bluetooth/services/ots/`
|
||||||
|
- `zephyr/include/zephyr/bluetooth/services/ots.h`
|
||||||
|
- 参考样例:`zephyr/samples/bluetooth/peripheral_ots`
|
||||||
|
|
||||||
|
启用方法:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT=y
|
||||||
|
CONFIG_BT_PERIPHERAL=y
|
||||||
|
CONFIG_BT_OTS=y
|
||||||
|
CONFIG_BT_L2CAP_DYNAMIC_CHANNEL=y
|
||||||
|
CONFIG_BT_GATT_DYNAMIC_DB=y
|
||||||
|
CONFIG_BT_SMP=y
|
||||||
|
```
|
||||||
|
|
||||||
|
按需求补充:
|
||||||
|
|
||||||
|
```conf
|
||||||
|
CONFIG_BT_OTS_DIR_LIST_OBJ=y
|
||||||
|
CONFIG_BT_OTS_OACP_WRITE_SUPPORT=y
|
||||||
|
CONFIG_BT_OTS_OACP_PATCH_SUPPORT=y
|
||||||
|
CONFIG_BT_OTS_OACP_CREATE_SUPPORT=y
|
||||||
|
CONFIG_BT_OTS_OACP_DELETE_SUPPORT=y
|
||||||
|
```
|
||||||
|
|
||||||
|
怎么用:
|
||||||
|
|
||||||
|
1. 先拿一个实例:`bt_ots_free_instance_get()`
|
||||||
|
2. 填 `struct bt_ots_init_param`,配置支持的 OACP/OLCP Feature 与回调
|
||||||
|
3. 调用 `bt_ots_init(ots, &ots_init)`
|
||||||
|
4. 准备对象元数据与对象数据,再用 `bt_ots_obj_add(ots, ¶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 模块拆分方式。
|
||||||
118
docs/nordic_ncs_官方知识索引.md
Normal file
118
docs/nordic_ncs_官方知识索引.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# new_kbd 项目 Nordic NCS 官方知识索引
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 生成时间:2026-03-30
|
||||||
|
- 文档来源:仅收录 `docs.nordicsemi.com` 上可直接访问的 Nordic 官方文档
|
||||||
|
- 版本基线:按项目当前环境优先核对 `ncs-3.2.3`
|
||||||
|
- 项目芯片基线:`atguigu_mini_keyboard/nrf52840`
|
||||||
|
- 芯片依据:项目板级文件 [atguigu_mini_keyboard.yaml](E:/extra/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.yaml) 与 [atguigu_mini_keyboard.dts](E:/extra/boards/atguigu/atguigu_mini_keyboard/atguigu_mini_keyboard.dts) 明确基于 `nrf52840`
|
||||||
|
- 收录范围:仅收录 `E:\projects\new_kbd` 实际使用到的 NCS 官方知识,以及 Nordic 站点中同时存在的 Zephyr 镜像文档
|
||||||
|
- 不收录原则:如果没有核到 Nordic 官方页面,则不写入本索引
|
||||||
|
- 本版校验方式:使用浏览器渲染后再判定,凡标题或正文渲染为 `Error 404 - Page Not Found` / `ERROR CODE: 404` 的页面,均视为无效并移除
|
||||||
|
|
||||||
|
## NCS 托管的 Zephyr 文档镜像
|
||||||
|
|
||||||
|
### 构建与配置
|
||||||
|
|
||||||
|
- [应用开发总览(NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/develop/application/index.html)
|
||||||
|
- [CMake 构建系统(NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/cmake/index.html)
|
||||||
|
- [Kconfig 配置系统(NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/kconfig/index.html)
|
||||||
|
- [Sysbuild 系统构建(NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/sysbuild/index.html)
|
||||||
|
|
||||||
|
### 设备树与设备模型
|
||||||
|
|
||||||
|
- [设备树入门(NCS 镜像)](https://docs.nordicsemi.com/bundle/ncs-3.2.3/page/zephyr/build/dts/intro.html)
|
||||||
|
- [设备树 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`,因此已从本索引移除
|
||||||
96
docs/zephyr_官方知识索引.md
Normal file
96
docs/zephyr_官方知识索引.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# new_kbd 项目 Zephyr 官方知识索引
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 生成时间:2026-03-30
|
||||||
|
- 扫描范围:`src/`、`inc/`、`prj.conf`、`sysbuild.conf`、`app.overlay`、`pm_static.yml`
|
||||||
|
- 收录原则:仅收录 `E:\projects\new_kbd` 当前实际用到、且已确认页面真实存在的 Zephyr 官方文档
|
||||||
|
- 链接范围:`https://docs.zephyrproject.org/latest/`、`https://docs.zephyrproject.org/apidoc/latest/`、`https://docs.zephyrproject.org/latest/doxygen/html/`
|
||||||
|
- 不收录范围:Nordic NCS/CAF/App Event Manager/Partition Manager 文档,以及项目自定义驱动或项目私有协议说明
|
||||||
|
- Nordic NCS 官方补充索引见:[new_kbd 项目 Nordic NCS 官方知识索引](./nordic_ncs_官方知识索引.md)
|
||||||
|
|
||||||
|
## 构建与配置
|
||||||
|
|
||||||
|
- [应用开发总览](https://docs.zephyrproject.org/latest/develop/application/index.html)
|
||||||
|
- [CMake 构建系统](https://docs.zephyrproject.org/latest/build/cmake/index.html)
|
||||||
|
- [Kconfig 配置系统](https://docs.zephyrproject.org/latest/build/kconfig/index.html)
|
||||||
|
- [Sysbuild 系统构建](https://docs.zephyrproject.org/latest/build/sysbuild/index.html)
|
||||||
|
|
||||||
|
## 设备树与设备模型
|
||||||
|
|
||||||
|
- [设备树入门](https://docs.zephyrproject.org/latest/build/dts/intro.html)
|
||||||
|
- [设备树 HOWTO](https://docs.zephyrproject.org/latest/build/dts/howtos.html)
|
||||||
|
- [设备树 C/C++ API 用法](https://docs.zephyrproject.org/latest/build/dts/api-usage.html)
|
||||||
|
- [zephyr,user 自定义节点](https://docs.zephyrproject.org/latest/build/dts/zephyr-user-node.html)
|
||||||
|
- [设备模型 API](https://docs.zephyrproject.org/apidoc/latest/group__device__model.html)
|
||||||
|
- [设备树通用标识符 API](https://docs.zephyrproject.org/apidoc/latest/group__devicetree-generic-id.html)
|
||||||
|
|
||||||
|
## 内核与并发
|
||||||
|
|
||||||
|
- [工作队列与延迟工作](https://docs.zephyrproject.org/latest/kernel/services/threads/workqueue.html)
|
||||||
|
- [原子操作](https://docs.zephyrproject.org/latest/kernel/services/other/atomic.html)
|
||||||
|
- [自旋锁 API](https://docs.zephyrproject.org/apidoc/latest/spinlock_8h.html)
|
||||||
|
- [时钟、超时与 uptime](https://docs.zephyrproject.org/latest/kernel/services/timing/clocks.html)
|
||||||
|
|
||||||
|
## 日志与存储
|
||||||
|
|
||||||
|
- [日志子系统](https://docs.zephyrproject.org/latest/services/logging/index.html)
|
||||||
|
- [Settings 设置子系统](https://docs.zephyrproject.org/latest/services/storage/settings/index.html)
|
||||||
|
- [NVS 非易失存储](https://docs.zephyrproject.org/latest/services/storage/nvs/nvs.html)
|
||||||
|
- [Flash Map 闪存映射](https://docs.zephyrproject.org/latest/services/storage/flash_map/flash_map.html)
|
||||||
|
|
||||||
|
## 蓝牙 Low Energy
|
||||||
|
|
||||||
|
- [Bluetooth GAP 概览](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/gap.html)
|
||||||
|
- [Bluetooth GAP API](https://docs.zephyrproject.org/apidoc/latest/group__bt__gap.html)
|
||||||
|
- [Bluetooth 连接管理](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/connection_mgmt.html)
|
||||||
|
- [Bluetooth 连接管理 API](https://docs.zephyrproject.org/apidoc/latest/group__bt__conn.html)
|
||||||
|
- [Bluetooth GATT 概览](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/gatt.html)
|
||||||
|
- [Bluetooth GATT Server API](https://docs.zephyrproject.org/apidoc/latest/group__bt__gatt__server.html)
|
||||||
|
- [Bluetooth UUID API](https://docs.zephyrproject.org/apidoc/latest/group__bt__uuid.html)
|
||||||
|
- [Bluetooth Battery Service (BAS) API](https://docs.zephyrproject.org/apidoc/latest/group__bt__bas.html)
|
||||||
|
- [Bluetooth 标准服务总览](https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/services.html)
|
||||||
|
- [BLE HID 外设官方示例](https://docs.zephyrproject.org/latest/samples/bluetooth/peripheral_hids/README.html)
|
||||||
|
|
||||||
|
## USB 与 HID
|
||||||
|
|
||||||
|
- [USB 设备栈(Device Stack Next)](https://docs.zephyrproject.org/latest/connectivity/usb/device_next/usb_device.html)
|
||||||
|
- [USB Device API](https://docs.zephyrproject.org/apidoc/latest/group__usbd__api.html)
|
||||||
|
- [USB HID 设备 API](https://docs.zephyrproject.org/latest/connectivity/usb/device_next/api/usbd_hid_device.html)
|
||||||
|
- [USB HID Device API 参考](https://docs.zephyrproject.org/apidoc/latest/group__usbd__hid__device.html)
|
||||||
|
- [HID 通用定义与报告描述符](https://docs.zephyrproject.org/latest/connectivity/usb/api/hid.html)
|
||||||
|
- [zephyr,hid-device 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/usb/zephyr%2Chid-device.html)
|
||||||
|
|
||||||
|
## 外设驱动
|
||||||
|
|
||||||
|
- [ADC 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/adc.html)
|
||||||
|
- [ADC API](https://docs.zephyrproject.org/apidoc/latest/group__adc__interface.html)
|
||||||
|
- [GPIO 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/gpio.html)
|
||||||
|
- [GPIO API](https://docs.zephyrproject.org/apidoc/latest/group__gpio__interface.html)
|
||||||
|
- [I2C 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/i2c.html)
|
||||||
|
- [传感器子系统概览](https://docs.zephyrproject.org/latest/hardware/peripherals/sensor/index.html)
|
||||||
|
- [传感器 API](https://docs.zephyrproject.org/apidoc/latest/group__sensor__interface.html)
|
||||||
|
- [LED 外设概览](https://docs.zephyrproject.org/latest/hardware/peripherals/led.html)
|
||||||
|
- [LED API](https://docs.zephyrproject.org/apidoc/latest/group__led__interface.html)
|
||||||
|
- [Nordic nRF QDEC 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/sensor/nordic%2Cnrf-qdec.html)
|
||||||
|
|
||||||
|
## 显示与 GUI
|
||||||
|
|
||||||
|
- [显示子系统概览](https://docs.zephyrproject.org/latest/hardware/peripherals/display/index.html)
|
||||||
|
- [MIPI-DBI SPI 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/mipi-dbi/zephyr%2Cmipi-dbi-spi.html)
|
||||||
|
- [ST7789V 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/display/sitronix%2Cst7789v.html)
|
||||||
|
- [PWM LEDs 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/led/pwm-leds.html)
|
||||||
|
- [GPIO LEDs 设备树绑定](https://docs.zephyrproject.org/latest/build/dts/api/bindings/led/gpio-leds.html)
|
||||||
|
- [LVGL 官方示例入口](https://docs.zephyrproject.org/latest/samples/modules/lvgl/lvgl.html)
|
||||||
|
|
||||||
|
## 通用工具宏与字节序
|
||||||
|
|
||||||
|
- [sys/util 通用工具宏 API](https://docs.zephyrproject.org/apidoc/latest/group__sys-util.html)
|
||||||
|
- [sys/byteorder 字节序 API](https://docs.zephyrproject.org/latest/doxygen/html/sys_2byteorder_8h.html)
|
||||||
|
|
||||||
|
## 未纳入本索引的项目项
|
||||||
|
|
||||||
|
- `zephyr/drivers/power/ip5306.h`、`CONFIG_IP5306`:当前项目使用的是自定义驱动/自定义绑定,Zephyr 官方 latest 站点未核到对应官方页面,因此未纳入
|
||||||
|
- `pm_static.yml` 对应的 Partition Manager 规划:属于 Nordic NCS 范畴,不属于 Zephyr 官方 docs 站点范围;Nordic 官方索引见 [nordic_ncs_官方知识索引.md](./nordic_ncs_官方知识索引.md)
|
||||||
|
- `CAF`、`App Event Manager`、`settings_loader`、`ble_state`、`ble_common_event`、`power_manager`、`buttons_def.h` 等:属于 Nordic NCS/CAF 文档范围,不属于本 Zephyr 官方索引;Nordic 官方索引见 [nordic_ncs_官方知识索引.md](./nordic_ncs_官方知识索引.md)
|
||||||
|
- 项目私有协议与实现,如时间同步私有 GATT 服务、主机 HID 命令协议、显示主题持久化逻辑等:只索引其依赖的 Zephyr 通用能力,不索引项目私有设计本身
|
||||||
16
inc/hid_host_command_protocol.h
Normal file
16
inc/hid_host_command_protocol.h
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#ifndef HID_HOST_COMMAND_PROTOCOL_H__
|
||||||
|
#define HID_HOST_COMMAND_PROTOCOL_H__
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define HID_HOST_CMD_DATA_SIZE 8U
|
||||||
|
#define HID_HOST_CMD_OUTPUT_PAYLOAD_SIZE (1U + HID_HOST_CMD_DATA_SIZE)
|
||||||
|
#define HID_HOST_CMD_ACK_PAYLOAD_SIZE 1U
|
||||||
|
|
||||||
|
#define HID_HOST_CMD_ID_THEME_COLOR 0x01U
|
||||||
|
#define HID_HOST_CMD_ID_TIME_SYNC 0x02U
|
||||||
|
|
||||||
|
#define HID_HOST_CMD_THEME_PARAM_SIZE 3U
|
||||||
|
#define HID_HOST_CMD_TIME_SYNC_PARAM_SIZE 8U
|
||||||
|
|
||||||
|
#endif /* HID_HOST_COMMAND_PROTOCOL_H__ */
|
||||||
9
inc/hid_host_transport.h
Normal file
9
inc/hid_host_transport.h
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#ifndef HID_HOST_TRANSPORT_H__
|
||||||
|
#define HID_HOST_TRANSPORT_H__
|
||||||
|
|
||||||
|
enum hid_host_transport {
|
||||||
|
HID_HOST_TRANSPORT_USB = 0,
|
||||||
|
HID_HOST_TRANSPORT_BLE,
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* HID_HOST_TRANSPORT_H__ */
|
||||||
@@ -3,11 +3,14 @@
|
|||||||
|
|
||||||
#include <zephyr/usb/class/usbd_hid.h>
|
#include <zephyr/usb/class/usbd_hid.h>
|
||||||
|
|
||||||
|
#include "hid_host_command_protocol.h"
|
||||||
|
|
||||||
/* 与 HID Report Map 对齐的 Report ID。 */
|
/* 与 HID Report Map 对齐的 Report ID。 */
|
||||||
enum {
|
enum {
|
||||||
REPORT_ID_KEYBOARD = 1,
|
REPORT_ID_KEYBOARD = 1,
|
||||||
REPORT_ID_CONSUMER = 3,
|
REPORT_ID_CONSUMER = 3,
|
||||||
REPORT_ID_VENDOR = 4,
|
REPORT_ID_VENDOR = 4,
|
||||||
|
REPORT_ID_VENDOR_CMD = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
#define HID_KBD_USAGE_MAX 0x00E7U
|
#define HID_KBD_USAGE_MAX 0x00E7U
|
||||||
@@ -18,6 +21,7 @@ enum {
|
|||||||
#define HID_BOOT_KBD_PAYLOAD_SIZE 8U
|
#define HID_BOOT_KBD_PAYLOAD_SIZE 8U
|
||||||
#define HID_CONSUMER_PAYLOAD_SIZE 2U
|
#define HID_CONSUMER_PAYLOAD_SIZE 2U
|
||||||
#define HID_VENDOR_PAYLOAD_SIZE HID_KBD_PAYLOAD_SIZE
|
#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_KBD_LED_PAYLOAD_SIZE 1U
|
||||||
#define HID_FULL_REPORT_SIZE(payload) (1U + (payload))
|
#define HID_FULL_REPORT_SIZE(payload) (1U + (payload))
|
||||||
|
|
||||||
@@ -33,9 +37,10 @@ enum {
|
|||||||
* 键盘(NKRO) + Consumer + Vendor 的复合 Report 描述符:
|
* 键盘(NKRO) + Consumer + Vendor 的复合 Report 描述符:
|
||||||
* - USB Report 接口和 BLE HIDS Report Map 统一使用这份定义,
|
* - USB Report 接口和 BLE HIDS Report Map 统一使用这份定义,
|
||||||
* 避免两边手写常量后长期演进出现不一致。
|
* 避免两边手写常量后长期演进出现不一致。
|
||||||
* - Vendor Report 复用与 NKRO 键盘状态相同的 payload 结构:
|
* - Report ID 0x04 继续复用 NKRO payload,承载私有状态/遮罩语义。
|
||||||
* [modifier(1B) | usage_bitmap(29B)]。
|
* - Report ID 0x05 预留给“主机命令 + 设备 ACK”通道:
|
||||||
* 这样主机可以下发“屏蔽遮罩”,设备也可以上报“真实键盘状态”。
|
* - Output payload 固定 9 字节:[cmd(1) | data(8)]
|
||||||
|
* - Input payload 固定 1 字节:[cmd]
|
||||||
*/
|
*/
|
||||||
#define HID_DESC_KEYBOARD_NKRO_CONSUMER() \
|
#define HID_DESC_KEYBOARD_NKRO_CONSUMER() \
|
||||||
{ \
|
{ \
|
||||||
@@ -105,6 +110,22 @@ enum {
|
|||||||
HID_REPORT_COUNT(HID_VENDOR_PAYLOAD_SIZE), \
|
HID_REPORT_COUNT(HID_VENDOR_PAYLOAD_SIZE), \
|
||||||
HID_USAGE(0x02U), \
|
HID_USAGE(0x02U), \
|
||||||
HID_OUTPUT(0x02), \
|
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, \
|
HID_END_COLLECTION, \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ enum time_sync_source {
|
|||||||
TIME_SYNC_SOURCE_BLE,
|
TIME_SYNC_SOURCE_BLE,
|
||||||
TIME_SYNC_SOURCE_USB,
|
TIME_SYNC_SOURCE_USB,
|
||||||
TIME_SYNC_SOURCE_MANUAL,
|
TIME_SYNC_SOURCE_MANUAL,
|
||||||
|
TIME_SYNC_SOURCE_HID,
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
5
modules/ip5306/CMakeLists.txt
Normal file
5
modules/ip5306/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
zephyr_library()
|
||||||
|
zephyr_library_sources_ifdef(CONFIG_IP5306 drivers/power/ip5306.c)
|
||||||
|
zephyr_library_sources_ifdef(CONFIG_IP5306 drivers/power/ip5306_keepalive_sw.c)
|
||||||
|
zephyr_library_sources_ifdef(CONFIG_IP5306_KEEPALIVE_HW_NRF drivers/power/ip5306_keepalive_nrf.c)
|
||||||
|
zephyr_include_directories(include)
|
||||||
5
modules/ip5306/Kconfig
Normal file
5
modules/ip5306/Kconfig
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
menu "IP5306 Module"
|
||||||
|
|
||||||
|
rsource "drivers/power/Kconfig"
|
||||||
|
|
||||||
|
endmenu
|
||||||
17
modules/ip5306/drivers/power/Kconfig
Normal file
17
modules/ip5306/drivers/power/Kconfig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
config IP5306
|
||||||
|
bool "Injoinic IP5306 PMIC driver"
|
||||||
|
depends on I2C && GPIO
|
||||||
|
default n
|
||||||
|
help
|
||||||
|
Enable IP5306 PMIC driver over I2C.
|
||||||
|
|
||||||
|
config IP5306_KEEPALIVE_HW_NRF
|
||||||
|
bool "Enable nRF keepalive HW offload backend"
|
||||||
|
depends on IP5306 && SOC_FAMILY_NORDIC_NRF
|
||||||
|
select NRFX_GPIOTE
|
||||||
|
select NRFX_GPPI
|
||||||
|
select NRFX_RTC2
|
||||||
|
default y
|
||||||
|
help
|
||||||
|
Enable low-power keepalive offload backend using RTC2 + GPIOTE + GPPI.
|
||||||
|
Runtime selection is controlled by devicetree property keepalive-offload.
|
||||||
152
modules/ip5306/drivers/power/ip5306.c
Normal file
152
modules/ip5306/drivers/power/ip5306.c
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
#include <errno.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <zephyr/device.h>
|
||||||
|
#include <zephyr/drivers/power/ip5306.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <zephyr/sys/util.h>
|
||||||
|
|
||||||
|
#include "ip5306_priv.h"
|
||||||
|
|
||||||
|
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
|
||||||
|
#include <soc_nrf_common.h>
|
||||||
|
#define IP5306_CFG_KEEPALIVE_PSEL_INIT(inst) \
|
||||||
|
.keepalive_psel = NRF_DT_GPIOS_TO_PSEL_OR(DT_DRV_INST(inst), keepalive_gpios, 0),
|
||||||
|
#else
|
||||||
|
#define IP5306_CFG_KEEPALIVE_PSEL_INIT(inst)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
LOG_MODULE_REGISTER(ip5306, LOG_LEVEL_INF);
|
||||||
|
|
||||||
|
#define DT_DRV_COMPAT injoinic_ip5306
|
||||||
|
|
||||||
|
#define IP5306_REG_READ0 0x70
|
||||||
|
#define IP5306_REG_READ1 0x71
|
||||||
|
#define IP5306_STATUS_BIT BIT(3)
|
||||||
|
|
||||||
|
static int ip5306_read_reg(const struct device *dev, uint8_t reg, uint8_t *val)
|
||||||
|
{
|
||||||
|
const struct ip5306_config *cfg = dev->config;
|
||||||
|
|
||||||
|
return i2c_reg_read_byte_dt(&cfg->i2c, reg, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ip5306_get_status_bit(const struct device *dev, uint8_t reg, bool *flag)
|
||||||
|
{
|
||||||
|
uint8_t value = 0U;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
if (flag == NULL) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = ip5306_read_reg(dev, reg, &value);
|
||||||
|
if (ret != 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
*flag = ((value & IP5306_STATUS_BIT) != 0U);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ip5306_api_is_charging(const struct device *dev, bool *charging)
|
||||||
|
{
|
||||||
|
return ip5306_get_status_bit(dev, IP5306_REG_READ0, charging);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ip5306_api_is_charge_full(const struct device *dev, bool *full)
|
||||||
|
{
|
||||||
|
return ip5306_get_status_bit(dev, IP5306_REG_READ1, full);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ip5306_keepalive_start(const struct device *dev)
|
||||||
|
{
|
||||||
|
struct ip5306_data *data = dev->data;
|
||||||
|
const struct ip5306_config *cfg = dev->config;
|
||||||
|
|
||||||
|
if (!cfg->has_keepalive_gpio || (data->keepalive_interval_ms == 0U)) {
|
||||||
|
data->backend = IP5306_KEEPALIVE_BACKEND_NONE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 选择策略:
|
||||||
|
* 1) DTS 显式请求硬件 offload 时,先尝试硬件后端;
|
||||||
|
* 2) 若硬件依赖不可用或资源被占用,则告警后回退软件后端。
|
||||||
|
*/
|
||||||
|
if (cfg->keepalive_offload) {
|
||||||
|
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
|
||||||
|
int ret = ip5306_keepalive_hw_nrf_start(dev);
|
||||||
|
|
||||||
|
if (ret == 0) {
|
||||||
|
LOG_INF("Keepalive backend=HW(nRF), pulse=%ums interval=%ums",
|
||||||
|
data->keepalive_pulse_ms, data->keepalive_interval_ms);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_WRN("HW keepalive unavailable (%d), fallback to SW backend", ret);
|
||||||
|
#else
|
||||||
|
LOG_WRN("HW keepalive requested but HW backend is not built, fallback to SW backend");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ip5306_keepalive_sw_start(dev);
|
||||||
|
LOG_INF("Keepalive backend=SW, pulse=%ums interval=%ums",
|
||||||
|
data->keepalive_pulse_ms, data->keepalive_interval_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ip5306_init(const struct device *dev)
|
||||||
|
{
|
||||||
|
const struct ip5306_config *cfg = dev->config;
|
||||||
|
struct ip5306_data *data = dev->data;
|
||||||
|
|
||||||
|
if (!i2c_is_ready_dt(&cfg->i2c)) {
|
||||||
|
return -ENODEV;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg->has_keepalive_gpio) {
|
||||||
|
if (!gpio_is_ready_dt(&cfg->keepalive_gpio)) {
|
||||||
|
return -ENODEV;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gpio_pin_configure_dt(&cfg->keepalive_gpio, GPIO_OUTPUT_INACTIVE) != 0) {
|
||||||
|
return -EIO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data->keepalive_high = false;
|
||||||
|
data->dev = dev;
|
||||||
|
data->backend = IP5306_KEEPALIVE_BACKEND_NONE;
|
||||||
|
data->keepalive_pulse_ms = (cfg->keepalive_pulse_ms != 0U) ?
|
||||||
|
cfg->keepalive_pulse_ms : IP5306_KEEPALIVE_DEFAULT_PULSE_MS;
|
||||||
|
data->keepalive_interval_ms = (cfg->keepalive_interval_ms != 0U) ?
|
||||||
|
cfg->keepalive_interval_ms : IP5306_KEEPALIVE_DEFAULT_INTERVAL_MS;
|
||||||
|
|
||||||
|
ip5306_keepalive_start(dev);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const struct ip5306_driver_api ip5306_api = {
|
||||||
|
.is_charging = ip5306_api_is_charging,
|
||||||
|
.is_charge_full = ip5306_api_is_charge_full,
|
||||||
|
};
|
||||||
|
|
||||||
|
#define IP5306_DEFINE(inst) \
|
||||||
|
static struct ip5306_data ip5306_data_##inst; \
|
||||||
|
static const struct ip5306_config ip5306_cfg_##inst = { \
|
||||||
|
.i2c = I2C_DT_SPEC_INST_GET(inst), \
|
||||||
|
.keepalive_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, keepalive_gpios, {0}), \
|
||||||
|
IP5306_CFG_KEEPALIVE_PSEL_INIT(inst) \
|
||||||
|
.keepalive_pulse_ms = DT_INST_PROP(inst, keepalive_pulse_ms), \
|
||||||
|
.keepalive_interval_ms = DT_INST_PROP(inst, keepalive_interval_ms), \
|
||||||
|
.has_keepalive_gpio = DT_INST_NODE_HAS_PROP(inst, keepalive_gpios), \
|
||||||
|
.keepalive_offload = DT_INST_PROP_OR(inst, keepalive_offload, false), \
|
||||||
|
}; \
|
||||||
|
DEVICE_DT_INST_DEFINE(inst, ip5306_init, NULL, &ip5306_data_##inst, \
|
||||||
|
&ip5306_cfg_##inst, POST_KERNEL, CONFIG_KERNEL_INIT_PRIORITY_DEVICE, \
|
||||||
|
&ip5306_api);
|
||||||
|
|
||||||
|
DT_INST_FOREACH_STATUS_OKAY(IP5306_DEFINE)
|
||||||
198
modules/ip5306/drivers/power/ip5306_keepalive_nrf.c
Normal file
198
modules/ip5306/drivers/power/ip5306_keepalive_nrf.c
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include <hal/nrf_rtc.h>
|
||||||
|
#include <nrfx_gpiote.h>
|
||||||
|
#include <nrfx_rtc.h>
|
||||||
|
|
||||||
|
#include <helpers/nrfx_gppi.h>
|
||||||
|
|
||||||
|
#include <zephyr/drivers/gpio.h>
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <zephyr/sys/util.h>
|
||||||
|
|
||||||
|
#include "ip5306_priv.h"
|
||||||
|
|
||||||
|
LOG_MODULE_DECLARE(ip5306, LOG_LEVEL_INF);
|
||||||
|
|
||||||
|
#define IP5306_KEEPALIVE_RTC_FREQUENCY_HZ 32768U
|
||||||
|
#define IP5306_KEEPALIVE_RTC_CC_HIGH 0U
|
||||||
|
#define IP5306_KEEPALIVE_RTC_CC_LOW 1U
|
||||||
|
#define IP5306_KEEPALIVE_RTC_CC_PERIOD 2U
|
||||||
|
#define IP5306_KEEPALIVE_MIN_PERIOD_TICKS 3U
|
||||||
|
|
||||||
|
/* nRF52 单核场景默认使用 GPIOTE0 + RTC2。 */
|
||||||
|
static nrfx_gpiote_t ip5306_keepalive_gpiote = NRFX_GPIOTE_INSTANCE(NRF_GPIOTE_INST_GET(0));
|
||||||
|
static const nrfx_rtc_t ip5306_keepalive_rtc = NRFX_RTC_INSTANCE(2);
|
||||||
|
static bool ip5306_keepalive_hw_claimed;
|
||||||
|
|
||||||
|
static void ip5306_keepalive_rtc_handler(nrfx_rtc_int_type_t int_type)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* 这里故意不处理任何中断逻辑:
|
||||||
|
* 本模块的保活脉冲由 RTC event -> GPPI/PPI -> GPIOTE task 的硬件链路完成,
|
||||||
|
* 不依赖线程或 ISR 参与,从而减少 CPU 唤醒次数、降低功耗。
|
||||||
|
*/
|
||||||
|
ARG_UNUSED(int_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t ip5306_keepalive_ms_to_rtc_ticks(uint32_t time_ms)
|
||||||
|
{
|
||||||
|
/* 四舍五入到最近 tick,降低长时间运行时累计漂移。 */
|
||||||
|
uint64_t ticks = ((uint64_t)time_ms * IP5306_KEEPALIVE_RTC_FREQUENCY_HZ + 500U) / 1000U;
|
||||||
|
|
||||||
|
return (uint32_t)MAX(ticks, 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ip5306_keepalive_hw_nrf_start(const struct device *dev)
|
||||||
|
{
|
||||||
|
struct ip5306_data *data = dev->data;
|
||||||
|
const struct ip5306_config *cfg = dev->config;
|
||||||
|
const bool active_high = ((cfg->keepalive_gpio.dt_flags & GPIO_ACTIVE_LOW) == 0U);
|
||||||
|
const uint32_t period_ticks = ip5306_keepalive_ms_to_rtc_ticks(data->keepalive_interval_ms);
|
||||||
|
uint32_t high_ticks = ip5306_keepalive_ms_to_rtc_ticks(data->keepalive_pulse_ms);
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
if (ip5306_keepalive_hw_claimed) {
|
||||||
|
return -EBUSY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period_ticks < IP5306_KEEPALIVE_MIN_PERIOD_TICKS) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (high_ticks >= (period_ticks - 1U)) {
|
||||||
|
high_ticks = period_ticks - 1U;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nrfx_gpiote_init_check(&ip5306_keepalive_gpiote)) {
|
||||||
|
ret = nrfx_gpiote_init(&ip5306_keepalive_gpiote, NRFX_GPIOTE_DEFAULT_CONFIG_IRQ_PRIORITY);
|
||||||
|
if ((ret != 0) && (ret != -EALREADY)) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = nrfx_gpiote_channel_alloc(&ip5306_keepalive_gpiote, &data->keepalive_gpiote_channel);
|
||||||
|
if (ret != 0) {
|
||||||
|
/* GPIOTE 通道不足通常意味着被其他驱动占用。 */
|
||||||
|
LOG_ERR("Failed to allocate GPIOTE channel");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nrfx_gpiote_output_config_t output_config = {
|
||||||
|
.drive = NRF_GPIO_PIN_S0S1,
|
||||||
|
.input_connect = NRF_GPIO_PIN_INPUT_DISCONNECT,
|
||||||
|
.pull = NRF_GPIO_PIN_NOPULL,
|
||||||
|
};
|
||||||
|
const nrfx_gpiote_task_config_t task_config = {
|
||||||
|
.task_ch = data->keepalive_gpiote_channel,
|
||||||
|
.polarity = NRF_GPIOTE_POLARITY_TOGGLE,
|
||||||
|
.init_val = active_high ? NRF_GPIOTE_INITIAL_VALUE_LOW : NRF_GPIOTE_INITIAL_VALUE_HIGH,
|
||||||
|
};
|
||||||
|
|
||||||
|
ret = nrfx_gpiote_output_configure(&ip5306_keepalive_gpiote, cfg->keepalive_psel,
|
||||||
|
&output_config, &task_config);
|
||||||
|
if (ret != 0) {
|
||||||
|
goto err_free_channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
nrfx_gpiote_out_task_enable(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
|
||||||
|
nrfx_gpiote_out_task_force(&ip5306_keepalive_gpiote, cfg->keepalive_psel, active_high ? 0U : 1U);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* RTC2 初始化前先检查是否已被外部占用:
|
||||||
|
* 若占用则直接返回 -EBUSY,让上层走软件保活回退。
|
||||||
|
*/
|
||||||
|
if (nrfx_rtc_init_check(&ip5306_keepalive_rtc)) {
|
||||||
|
ret = -EBUSY;
|
||||||
|
goto err_disable_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nrfx_rtc_config_t rtc_cfg = NRFX_RTC_DEFAULT_CONFIG;
|
||||||
|
|
||||||
|
ret = nrfx_rtc_init(&ip5306_keepalive_rtc, &rtc_cfg, ip5306_keepalive_rtc_handler);
|
||||||
|
if ((ret != 0) && (ret != -EALREADY)) {
|
||||||
|
goto err_disable_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
nrfx_rtc_disable(&ip5306_keepalive_rtc);
|
||||||
|
nrfx_rtc_counter_clear(&ip5306_keepalive_rtc);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* RTC 角色:时间基准,按 compare 点产生事件。
|
||||||
|
* - CC0 到点 -> 产生 COMPARE0 event(脉冲上升沿触发点)
|
||||||
|
* - CC1 到点 -> 产生 COMPARE1 event(脉冲下降沿触发点)
|
||||||
|
* - CC2 到点 -> 触发周期边界,配合 SHORT 实现自动循环
|
||||||
|
*/
|
||||||
|
ret = nrfx_rtc_cc_set(&ip5306_keepalive_rtc, IP5306_KEEPALIVE_RTC_CC_HIGH, period_ticks - high_ticks - 10U, false);
|
||||||
|
if (ret != 0) {
|
||||||
|
goto err_disable_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = nrfx_rtc_cc_set(&ip5306_keepalive_rtc, IP5306_KEEPALIVE_RTC_CC_LOW, period_ticks - 10U, false);
|
||||||
|
if (ret != 0) {
|
||||||
|
goto err_disable_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = nrfx_rtc_cc_set(&ip5306_keepalive_rtc, IP5306_KEEPALIVE_RTC_CC_PERIOD, period_ticks, false);
|
||||||
|
if (ret != 0) {
|
||||||
|
goto err_disable_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t set_task = active_high ?
|
||||||
|
nrfx_gpiote_set_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel) :
|
||||||
|
nrfx_gpiote_clr_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
|
||||||
|
const uint32_t clr_task = active_high ?
|
||||||
|
nrfx_gpiote_clr_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel) :
|
||||||
|
nrfx_gpiote_set_task_address_get(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
|
||||||
|
const uint32_t high_event = nrfx_rtc_event_address_get(&ip5306_keepalive_rtc, NRF_RTC_EVENT_COMPARE_0);
|
||||||
|
const uint32_t low_event = nrfx_rtc_event_address_get(&ip5306_keepalive_rtc, NRF_RTC_EVENT_COMPARE_1);
|
||||||
|
const uint32_t period_event = nrfx_rtc_event_address_get(&ip5306_keepalive_rtc, NRF_RTC_EVENT_COMPARE_2);
|
||||||
|
const uint32_t clear_task = nrfx_rtc_task_address_get(&ip5306_keepalive_rtc, NRF_RTC_TASK_CLEAR);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GPPI/PPI 角色:事件路由器。
|
||||||
|
* 将 RTC compare event 直接连接到 GPIOTE task,不经过线程/ISR。
|
||||||
|
*/
|
||||||
|
ret = nrfx_gppi_conn_alloc(high_event, set_task, &data->keepalive_set_handle);
|
||||||
|
if (ret != 0) {
|
||||||
|
goto err_disable_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = nrfx_gppi_conn_alloc(low_event, clr_task, &data->keepalive_clr_handle);
|
||||||
|
if (ret != 0) {
|
||||||
|
nrfx_gppi_conn_free(high_event, set_task, data->keepalive_set_handle);
|
||||||
|
goto err_disable_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 某些 SoC/SDK 组合下 RTC SHORT 宏不可用,因此这里用第三条 GPPI 链路实现“周期自动清零”。
|
||||||
|
* 这样仍保持纯硬件闭环:COMPARE2 event -> RTC CLEAR task,不需要 CPU/ISR 参与。
|
||||||
|
*/
|
||||||
|
ret = nrfx_gppi_conn_alloc(period_event, clear_task, &data->keepalive_period_handle);
|
||||||
|
if (ret != 0) {
|
||||||
|
nrfx_gppi_conn_free(low_event, clr_task, data->keepalive_clr_handle);
|
||||||
|
nrfx_gppi_conn_free(high_event, set_task, data->keepalive_set_handle);
|
||||||
|
goto err_disable_task;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GPIOTE 角色:GPIO 执行器。
|
||||||
|
* 收到 task 后立即对目标 pin 执行 SET/CLR,输出保活脉冲波形。
|
||||||
|
*/
|
||||||
|
nrfx_gppi_conn_enable(data->keepalive_set_handle);
|
||||||
|
nrfx_gppi_conn_enable(data->keepalive_clr_handle);
|
||||||
|
nrfx_gppi_conn_enable(data->keepalive_period_handle);
|
||||||
|
nrfx_rtc_enable(&ip5306_keepalive_rtc);
|
||||||
|
|
||||||
|
ip5306_keepalive_hw_claimed = true;
|
||||||
|
data->backend = IP5306_KEEPALIVE_BACKEND_HW_NRF;
|
||||||
|
data->dev = dev;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
err_disable_task:
|
||||||
|
nrfx_gpiote_out_task_disable(&ip5306_keepalive_gpiote, cfg->keepalive_psel);
|
||||||
|
err_free_channel:
|
||||||
|
(void)nrfx_gpiote_channel_free(&ip5306_keepalive_gpiote, data->keepalive_gpiote_channel);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
54
modules/ip5306/drivers/power/ip5306_keepalive_sw.c
Normal file
54
modules/ip5306/drivers/power/ip5306_keepalive_sw.c
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
#include <zephyr/sys/util.h>
|
||||||
|
|
||||||
|
#include "ip5306_priv.h"
|
||||||
|
|
||||||
|
LOG_MODULE_DECLARE(ip5306, LOG_LEVEL_INF);
|
||||||
|
|
||||||
|
void ip5306_keepalive_sw_work_handler(struct k_work *work)
|
||||||
|
{
|
||||||
|
struct k_work_delayable *dwork = k_work_delayable_from_work(work);
|
||||||
|
struct ip5306_data *data = CONTAINER_OF(dwork, struct ip5306_data, keepalive_work);
|
||||||
|
const struct ip5306_config *cfg = data->dev->config;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
if ((data->backend != IP5306_KEEPALIVE_BACKEND_SW) || (data->keepalive_interval_ms == 0U))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data->keepalive_high)
|
||||||
|
{
|
||||||
|
ret = gpio_pin_set_dt(&cfg->keepalive_gpio, GPIO_OUTPUT_ACTIVE);
|
||||||
|
if (ret != 0)
|
||||||
|
{
|
||||||
|
LOG_ERR("SW keepalive set high failed: %d", ret);
|
||||||
|
(void)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_interval_ms));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data->keepalive_high = true;
|
||||||
|
(void)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_pulse_ms));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ret = gpio_pin_set_dt(&cfg->keepalive_gpio, GPIO_OUTPUT_INACTIVE);
|
||||||
|
if (ret != 0)
|
||||||
|
{
|
||||||
|
LOG_ERR("SW keepalive set low failed: %d", ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
data->keepalive_high = false;
|
||||||
|
(void)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_interval_ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ip5306_keepalive_sw_start(const struct device *dev)
|
||||||
|
{
|
||||||
|
struct ip5306_data *data = dev->data;
|
||||||
|
|
||||||
|
data->backend = IP5306_KEEPALIVE_BACKEND_SW;
|
||||||
|
data->dev = dev;
|
||||||
|
k_work_init_delayable(&data->keepalive_work, ip5306_keepalive_sw_work_handler);
|
||||||
|
return (int)k_work_schedule(&data->keepalive_work, K_MSEC(data->keepalive_interval_ms));
|
||||||
|
}
|
||||||
58
modules/ip5306/drivers/power/ip5306_priv.h
Normal file
58
modules/ip5306/drivers/power/ip5306_priv.h
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#ifndef IP5306_KEEPALIVE_PRIV_H_
|
||||||
|
#define IP5306_KEEPALIVE_PRIV_H_
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <zephyr/device.h>
|
||||||
|
#include <zephyr/drivers/gpio.h>
|
||||||
|
#include <zephyr/drivers/i2c.h>
|
||||||
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/sys/util.h>
|
||||||
|
|
||||||
|
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
|
||||||
|
#include <helpers/nrfx_gppi.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define IP5306_KEEPALIVE_DEFAULT_PULSE_MS 100U
|
||||||
|
#define IP5306_KEEPALIVE_DEFAULT_INTERVAL_MS 20000U
|
||||||
|
|
||||||
|
enum ip5306_keepalive_backend {
|
||||||
|
IP5306_KEEPALIVE_BACKEND_NONE,
|
||||||
|
IP5306_KEEPALIVE_BACKEND_SW,
|
||||||
|
IP5306_KEEPALIVE_BACKEND_HW_NRF,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ip5306_config {
|
||||||
|
struct i2c_dt_spec i2c;
|
||||||
|
struct gpio_dt_spec keepalive_gpio;
|
||||||
|
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
|
||||||
|
uint32_t keepalive_psel;
|
||||||
|
#endif
|
||||||
|
uint32_t keepalive_pulse_ms;
|
||||||
|
uint32_t keepalive_interval_ms;
|
||||||
|
bool has_keepalive_gpio;
|
||||||
|
bool keepalive_offload;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ip5306_data {
|
||||||
|
struct k_work_delayable keepalive_work;
|
||||||
|
bool keepalive_high;
|
||||||
|
uint32_t keepalive_pulse_ms;
|
||||||
|
uint32_t keepalive_interval_ms;
|
||||||
|
enum ip5306_keepalive_backend backend;
|
||||||
|
const struct device *dev;
|
||||||
|
|
||||||
|
#if IS_ENABLED(CONFIG_IP5306_KEEPALIVE_HW_NRF)
|
||||||
|
uint8_t keepalive_gpiote_channel;
|
||||||
|
nrfx_gppi_handle_t keepalive_set_handle;
|
||||||
|
nrfx_gppi_handle_t keepalive_clr_handle;
|
||||||
|
nrfx_gppi_handle_t keepalive_period_handle;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
int ip5306_keepalive_sw_start(const struct device *dev);
|
||||||
|
|
||||||
|
int ip5306_keepalive_hw_nrf_start(const struct device *dev);
|
||||||
|
|
||||||
|
#endif /* IP5306_KEEPALIVE_PRIV_H_ */
|
||||||
33
modules/ip5306/dts/bindings/power/injoinic,ip5306.yaml
Normal file
33
modules/ip5306/dts/bindings/power/injoinic,ip5306.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
title: Injoinic IP5306 PMIC
|
||||||
|
|
||||||
|
description: |
|
||||||
|
Injoinic IP5306 power management IC accessed over I2C.
|
||||||
|
This binding exposes two status bits and an optional KEY pin keepalive
|
||||||
|
pulse output.
|
||||||
|
|
||||||
|
compatible: "injoinic,ip5306"
|
||||||
|
|
||||||
|
include: i2c-device.yaml
|
||||||
|
|
||||||
|
properties:
|
||||||
|
keepalive-gpios:
|
||||||
|
type: phandle-array
|
||||||
|
description: |
|
||||||
|
GPIO connected to KEY pin. The driver sends periodic pulses on this pin
|
||||||
|
to keep boost output alive.
|
||||||
|
|
||||||
|
keepalive-pulse-ms:
|
||||||
|
type: int
|
||||||
|
default: 100
|
||||||
|
description: Pulse width in milliseconds for each keepalive pulse.
|
||||||
|
|
||||||
|
keepalive-interval-ms:
|
||||||
|
type: int
|
||||||
|
default: 20000
|
||||||
|
description: Interval in milliseconds between keepalive pulses.
|
||||||
|
|
||||||
|
keepalive-offload:
|
||||||
|
type: boolean
|
||||||
|
description: |
|
||||||
|
Prefer hardware keepalive offload backend when available on this SoC.
|
||||||
|
If not set, driver always uses software keepalive backend.
|
||||||
1
modules/ip5306/dts/bindings/vendor-prefixes.txt
Normal file
1
modules/ip5306/dts/bindings/vendor-prefixes.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
injoinic Injoinic
|
||||||
48
modules/ip5306/include/zephyr/drivers/power/ip5306.h
Normal file
48
modules/ip5306/include/zephyr/drivers/power/ip5306.h
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#ifndef ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_
|
||||||
|
#define ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
#include <zephyr/device.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef int (*ip5306_get_flag_t)(const struct device *dev, bool *flag);
|
||||||
|
|
||||||
|
struct ip5306_driver_api {
|
||||||
|
ip5306_get_flag_t is_charging;
|
||||||
|
ip5306_get_flag_t is_charge_full;
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline int ip5306_is_charging(const struct device *dev, bool *charging)
|
||||||
|
{
|
||||||
|
const struct ip5306_driver_api *api =
|
||||||
|
(const struct ip5306_driver_api *)dev->api;
|
||||||
|
|
||||||
|
if (api == NULL || api->is_charging == NULL) {
|
||||||
|
return -ENOSYS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return api->is_charging(dev, charging);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int ip5306_is_charge_full(const struct device *dev, bool *full)
|
||||||
|
{
|
||||||
|
const struct ip5306_driver_api *api =
|
||||||
|
(const struct ip5306_driver_api *)dev->api;
|
||||||
|
|
||||||
|
if (api == NULL || api->is_charge_full == NULL) {
|
||||||
|
return -ENOSYS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return api->is_charge_full(dev, full);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* ZEPHYR_INCLUDE_DRIVERS_POWER_IP5306_H_ */
|
||||||
5
modules/ip5306/zephyr/module.yml
Normal file
5
modules/ip5306/zephyr/module.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
build:
|
||||||
|
cmake: .
|
||||||
|
kconfig: Kconfig
|
||||||
|
settings:
|
||||||
|
dts_root: .
|
||||||
@@ -1,37 +1,31 @@
|
|||||||
mcuboot:
|
mcuboot:
|
||||||
address: 0x0
|
address: 0x0
|
||||||
end_address: 0xc000
|
end_address: 0x10000
|
||||||
region: flash_primary
|
region: flash_primary
|
||||||
size: 0xc000
|
size: 0x10000
|
||||||
|
|
||||||
mcuboot_pad:
|
mcuboot_pad:
|
||||||
address: 0xc000
|
address: 0x10000
|
||||||
end_address: 0xc200
|
end_address: 0x10200
|
||||||
region: flash_primary
|
region: flash_primary
|
||||||
size: 0x200
|
size: 0x200
|
||||||
|
|
||||||
app:
|
app:
|
||||||
address: 0xc200
|
address: 0x10200
|
||||||
end_address: 0x82000
|
end_address: 0xf8000
|
||||||
region: flash_primary
|
region: flash_primary
|
||||||
size: 0x75e00
|
size: 0xe7e00
|
||||||
|
|
||||||
mcuboot_primary:
|
mcuboot_primary:
|
||||||
address: 0xc000
|
address: 0x10000
|
||||||
end_address: 0x82000
|
end_address: 0xf8000
|
||||||
orig_span: &id001
|
orig_span: &id001
|
||||||
- mcuboot_pad
|
- mcuboot_pad
|
||||||
- app
|
- app
|
||||||
region: flash_primary
|
region: flash_primary
|
||||||
size: 0x76000
|
size: 0xe8000
|
||||||
span: *id001
|
span: *id001
|
||||||
|
|
||||||
mcuboot_secondary:
|
|
||||||
address: 0x82000
|
|
||||||
end_address: 0xf8000
|
|
||||||
region: flash_primary
|
|
||||||
size: 0x76000
|
|
||||||
|
|
||||||
settings_storage:
|
settings_storage:
|
||||||
address: 0xf8000
|
address: 0xf8000
|
||||||
end_address: 0x100000
|
end_address: 0x100000
|
||||||
|
|||||||
16
prj.conf
16
prj.conf
@@ -16,6 +16,10 @@ CONFIG_MCUMGR_GRP_OS=n
|
|||||||
CONFIG_IMG_MANAGER=n
|
CONFIG_IMG_MANAGER=n
|
||||||
CONFIG_STREAM_FLASH=n
|
CONFIG_STREAM_FLASH=n
|
||||||
|
|
||||||
|
CONFIG_RETAINED_MEM=y
|
||||||
|
CONFIG_RETENTION=y
|
||||||
|
CONFIG_RETENTION_BOOT_MODE=y
|
||||||
|
|
||||||
CONFIG_BT=y
|
CONFIG_BT=y
|
||||||
CONFIG_BT_PERIPHERAL=y
|
CONFIG_BT_PERIPHERAL=y
|
||||||
CONFIG_BT_SMP=y
|
CONFIG_BT_SMP=y
|
||||||
@@ -52,9 +56,15 @@ CONFIG_BT_GATT_POOL=y
|
|||||||
CONFIG_BT_GATT_CHRC_POOL_SIZE=16
|
CONFIG_BT_GATT_CHRC_POOL_SIZE=16
|
||||||
CONFIG_BT_GATT_UUID16_POOL_SIZE=24
|
CONFIG_BT_GATT_UUID16_POOL_SIZE=24
|
||||||
CONFIG_BT_HIDS_ATTR_MAX=40
|
CONFIG_BT_HIDS_ATTR_MAX=40
|
||||||
CONFIG_BT_HIDS_INPUT_REP_MAX=3
|
CONFIG_BT_HIDS_INPUT_REP_MAX=4
|
||||||
CONFIG_BT_HIDS_OUTPUT_REP_MAX=2
|
CONFIG_BT_HIDS_OUTPUT_REP_MAX=3
|
||||||
CONFIG_BT_HIDS_FEATURE_REP_MAX=0
|
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_USB_DEVICE_STACK_NEXT=y
|
||||||
CONFIG_USBD_HID_SUPPORT=y
|
CONFIG_USBD_HID_SUPPORT=y
|
||||||
@@ -82,7 +92,7 @@ CONFIG_CAF_BUTTONS_DEBOUNCE_INTERVAL=10
|
|||||||
|
|
||||||
CONFIG_ADC=y
|
CONFIG_ADC=y
|
||||||
CONFIG_I2C=y
|
CONFIG_I2C=y
|
||||||
CONFIG_IP5305=y
|
CONFIG_IP5306=y
|
||||||
CONFIG_SENSOR=y
|
CONFIG_SENSOR=y
|
||||||
CONFIG_DISPLAY=y
|
CONFIG_DISPLAY=y
|
||||||
CONFIG_MIPI_DBI=y
|
CONFIG_MIPI_DBI=y
|
||||||
|
|||||||
31
src/events/display_theme_event.c
Normal file
31
src/events/display_theme_event.c
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#include "display_theme_event.h"
|
||||||
|
|
||||||
|
static void log_display_theme_event(const struct app_event_header *aeh)
|
||||||
|
{
|
||||||
|
const struct display_theme_event *event = cast_display_theme_event(aeh);
|
||||||
|
|
||||||
|
APP_EVENT_MANAGER_LOG(aeh, "rgb=(%u,%u,%u)",
|
||||||
|
event->red, event->green, event->blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void profile_display_theme_event(struct log_event_buf *buf,
|
||||||
|
const struct app_event_header *aeh)
|
||||||
|
{
|
||||||
|
const struct display_theme_event *event = cast_display_theme_event(aeh);
|
||||||
|
|
||||||
|
nrf_profiler_log_encode_uint8(buf, event->red);
|
||||||
|
nrf_profiler_log_encode_uint8(buf, event->green);
|
||||||
|
nrf_profiler_log_encode_uint8(buf, event->blue);
|
||||||
|
}
|
||||||
|
|
||||||
|
APP_EVENT_INFO_DEFINE(display_theme_event,
|
||||||
|
ENCODE(NRF_PROFILER_ARG_U8,
|
||||||
|
NRF_PROFILER_ARG_U8,
|
||||||
|
NRF_PROFILER_ARG_U8),
|
||||||
|
ENCODE("red", "green", "blue"),
|
||||||
|
profile_display_theme_event);
|
||||||
|
|
||||||
|
APP_EVENT_TYPE_DEFINE(display_theme_event,
|
||||||
|
log_display_theme_event,
|
||||||
|
&display_theme_event_info,
|
||||||
|
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));
|
||||||
30
src/events/display_theme_event.h
Normal file
30
src/events/display_theme_event.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#ifndef DISPLAY_THEME_EVENT_H__
|
||||||
|
#define DISPLAY_THEME_EVENT_H__
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <app_event_manager.h>
|
||||||
|
#include <app_event_manager_profiler_tracer.h>
|
||||||
|
|
||||||
|
struct display_theme_event {
|
||||||
|
struct app_event_header header;
|
||||||
|
uint8_t red;
|
||||||
|
uint8_t green;
|
||||||
|
uint8_t blue;
|
||||||
|
};
|
||||||
|
|
||||||
|
APP_EVENT_TYPE_DECLARE(display_theme_event);
|
||||||
|
|
||||||
|
static inline void display_theme_event_submit(uint8_t red,
|
||||||
|
uint8_t green,
|
||||||
|
uint8_t blue)
|
||||||
|
{
|
||||||
|
struct display_theme_event *event = new_display_theme_event();
|
||||||
|
|
||||||
|
event->red = red;
|
||||||
|
event->green = green;
|
||||||
|
event->blue = blue;
|
||||||
|
APP_EVENT_SUBMIT(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* DISPLAY_THEME_EVENT_H__ */
|
||||||
29
src/events/hid_host_ack_event.c
Normal file
29
src/events/hid_host_ack_event.c
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#include "hid_host_ack_event.h"
|
||||||
|
|
||||||
|
static void log_hid_host_ack_event(const struct app_event_header *aeh)
|
||||||
|
{
|
||||||
|
const struct hid_host_ack_event *event = cast_hid_host_ack_event(aeh);
|
||||||
|
|
||||||
|
APP_EVENT_MANAGER_LOG(aeh, "transport=%u cmd=0x%02x",
|
||||||
|
event->transport, event->cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void profile_hid_host_ack_event(struct log_event_buf *buf,
|
||||||
|
const struct app_event_header *aeh)
|
||||||
|
{
|
||||||
|
const struct hid_host_ack_event *event = cast_hid_host_ack_event(aeh);
|
||||||
|
|
||||||
|
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->transport);
|
||||||
|
nrf_profiler_log_encode_uint8(buf, event->cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
APP_EVENT_INFO_DEFINE(hid_host_ack_event,
|
||||||
|
ENCODE(NRF_PROFILER_ARG_U8,
|
||||||
|
NRF_PROFILER_ARG_U8),
|
||||||
|
ENCODE("transport", "cmd"),
|
||||||
|
profile_hid_host_ack_event);
|
||||||
|
|
||||||
|
APP_EVENT_TYPE_DEFINE(hid_host_ack_event,
|
||||||
|
log_hid_host_ack_event,
|
||||||
|
&hid_host_ack_event_info,
|
||||||
|
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));
|
||||||
29
src/events/hid_host_ack_event.h
Normal file
29
src/events/hid_host_ack_event.h
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
#ifndef HID_HOST_ACK_EVENT_H__
|
||||||
|
#define HID_HOST_ACK_EVENT_H__
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <app_event_manager.h>
|
||||||
|
#include <app_event_manager_profiler_tracer.h>
|
||||||
|
|
||||||
|
#include "hid_host_transport.h"
|
||||||
|
|
||||||
|
struct hid_host_ack_event {
|
||||||
|
struct app_event_header header;
|
||||||
|
enum hid_host_transport transport;
|
||||||
|
uint8_t cmd;
|
||||||
|
};
|
||||||
|
|
||||||
|
APP_EVENT_TYPE_DECLARE(hid_host_ack_event);
|
||||||
|
|
||||||
|
static inline void hid_host_ack_event_submit(enum hid_host_transport transport,
|
||||||
|
uint8_t cmd)
|
||||||
|
{
|
||||||
|
struct hid_host_ack_event *event = new_hid_host_ack_event();
|
||||||
|
|
||||||
|
event->transport = transport;
|
||||||
|
event->cmd = cmd;
|
||||||
|
APP_EVENT_SUBMIT(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* HID_HOST_ACK_EVENT_H__ */
|
||||||
36
src/events/hid_host_command_error_event.c
Normal file
36
src/events/hid_host_command_error_event.c
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#include "hid_host_command_error_event.h"
|
||||||
|
|
||||||
|
static void log_hid_host_command_error_event(const struct app_event_header *aeh)
|
||||||
|
{
|
||||||
|
const struct hid_host_command_error_event *event =
|
||||||
|
cast_hid_host_command_error_event(aeh);
|
||||||
|
|
||||||
|
APP_EVENT_MANAGER_LOG(aeh,
|
||||||
|
"transport=%u cmd=0x%02x reason=%u",
|
||||||
|
event->transport,
|
||||||
|
event->cmd,
|
||||||
|
event->reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void profile_hid_host_command_error_event(struct log_event_buf *buf,
|
||||||
|
const struct app_event_header *aeh)
|
||||||
|
{
|
||||||
|
const struct hid_host_command_error_event *event =
|
||||||
|
cast_hid_host_command_error_event(aeh);
|
||||||
|
|
||||||
|
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->transport);
|
||||||
|
nrf_profiler_log_encode_uint8(buf, event->cmd);
|
||||||
|
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
APP_EVENT_INFO_DEFINE(hid_host_command_error_event,
|
||||||
|
ENCODE(NRF_PROFILER_ARG_U8,
|
||||||
|
NRF_PROFILER_ARG_U8,
|
||||||
|
NRF_PROFILER_ARG_U8),
|
||||||
|
ENCODE("transport", "cmd", "reason"),
|
||||||
|
profile_hid_host_command_error_event);
|
||||||
|
|
||||||
|
APP_EVENT_TYPE_DEFINE(hid_host_command_error_event,
|
||||||
|
log_hid_host_command_error_event,
|
||||||
|
&hid_host_command_error_event_info,
|
||||||
|
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));
|
||||||
41
src/events/hid_host_command_error_event.h
Normal file
41
src/events/hid_host_command_error_event.h
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#ifndef HID_HOST_COMMAND_ERROR_EVENT_H__
|
||||||
|
#define HID_HOST_COMMAND_ERROR_EVENT_H__
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <app_event_manager.h>
|
||||||
|
#include <app_event_manager_profiler_tracer.h>
|
||||||
|
|
||||||
|
#include "hid_host_transport.h"
|
||||||
|
|
||||||
|
enum hid_host_command_error_reason {
|
||||||
|
HID_HOST_COMMAND_ERROR_UNKNOWN_CMD = 0,
|
||||||
|
HID_HOST_COMMAND_ERROR_INVALID_LENGTH,
|
||||||
|
HID_HOST_COMMAND_ERROR_INVALID_PARAM,
|
||||||
|
HID_HOST_COMMAND_ERROR_NOT_READY,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct hid_host_command_error_event {
|
||||||
|
struct app_event_header header;
|
||||||
|
enum hid_host_transport transport;
|
||||||
|
enum hid_host_command_error_reason reason;
|
||||||
|
uint8_t cmd;
|
||||||
|
};
|
||||||
|
|
||||||
|
APP_EVENT_TYPE_DECLARE(hid_host_command_error_event);
|
||||||
|
|
||||||
|
static inline void hid_host_command_error_event_submit(
|
||||||
|
enum hid_host_transport transport,
|
||||||
|
uint8_t cmd,
|
||||||
|
enum hid_host_command_error_reason reason)
|
||||||
|
{
|
||||||
|
struct hid_host_command_error_event *event =
|
||||||
|
new_hid_host_command_error_event();
|
||||||
|
|
||||||
|
event->transport = transport;
|
||||||
|
event->cmd = cmd;
|
||||||
|
event->reason = reason;
|
||||||
|
APP_EVENT_SUBMIT(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* HID_HOST_COMMAND_ERROR_EVENT_H__ */
|
||||||
47
src/events/hid_host_command_event.c
Normal file
47
src/events/hid_host_command_event.c
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#include "hid_host_command_event.h"
|
||||||
|
|
||||||
|
static const char *transport_name(enum hid_host_transport transport)
|
||||||
|
{
|
||||||
|
switch (transport) {
|
||||||
|
case HID_HOST_TRANSPORT_USB:
|
||||||
|
return "usb";
|
||||||
|
case HID_HOST_TRANSPORT_BLE:
|
||||||
|
return "ble";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_hid_host_command_event(const struct app_event_header *aeh)
|
||||||
|
{
|
||||||
|
const struct hid_host_command_event *event =
|
||||||
|
cast_hid_host_command_event(aeh);
|
||||||
|
|
||||||
|
APP_EVENT_MANAGER_LOG(aeh, "transport=%s cmd=0x%02x data_len=%u",
|
||||||
|
transport_name(event->transport),
|
||||||
|
event->cmd,
|
||||||
|
event->data_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void profile_hid_host_command_event(struct log_event_buf *buf,
|
||||||
|
const struct app_event_header *aeh)
|
||||||
|
{
|
||||||
|
const struct hid_host_command_event *event =
|
||||||
|
cast_hid_host_command_event(aeh);
|
||||||
|
|
||||||
|
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->transport);
|
||||||
|
nrf_profiler_log_encode_uint8(buf, event->cmd);
|
||||||
|
nrf_profiler_log_encode_uint8(buf, event->data_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
APP_EVENT_INFO_DEFINE(hid_host_command_event,
|
||||||
|
ENCODE(NRF_PROFILER_ARG_U8,
|
||||||
|
NRF_PROFILER_ARG_U8,
|
||||||
|
NRF_PROFILER_ARG_U8),
|
||||||
|
ENCODE("transport", "cmd", "data_len"),
|
||||||
|
profile_hid_host_command_event);
|
||||||
|
|
||||||
|
APP_EVENT_TYPE_DEFINE(hid_host_command_event,
|
||||||
|
log_hid_host_command_event,
|
||||||
|
&hid_host_command_event_info,
|
||||||
|
APP_EVENT_FLAGS_CREATE(APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE));
|
||||||
44
src/events/hid_host_command_event.h
Normal file
44
src/events/hid_host_command_event.h
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#ifndef HID_HOST_COMMAND_EVENT_H__
|
||||||
|
#define HID_HOST_COMMAND_EVENT_H__
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <app_event_manager.h>
|
||||||
|
#include <app_event_manager_profiler_tracer.h>
|
||||||
|
#include <zephyr/sys/util.h>
|
||||||
|
|
||||||
|
#include "hid_host_command_protocol.h"
|
||||||
|
#include "hid_host_transport.h"
|
||||||
|
|
||||||
|
struct hid_host_command_event {
|
||||||
|
struct app_event_header header;
|
||||||
|
enum hid_host_transport transport;
|
||||||
|
uint8_t cmd;
|
||||||
|
uint8_t data_len;
|
||||||
|
uint8_t data[HID_HOST_CMD_DATA_SIZE];
|
||||||
|
};
|
||||||
|
|
||||||
|
APP_EVENT_TYPE_DECLARE(hid_host_command_event);
|
||||||
|
|
||||||
|
static inline void hid_host_command_event_submit(enum hid_host_transport transport,
|
||||||
|
uint8_t cmd,
|
||||||
|
const uint8_t *data,
|
||||||
|
size_t data_len)
|
||||||
|
{
|
||||||
|
struct hid_host_command_event *event = new_hid_host_command_event();
|
||||||
|
size_t copy_len = MIN(data_len, (size_t)HID_HOST_CMD_DATA_SIZE);
|
||||||
|
|
||||||
|
event->transport = transport;
|
||||||
|
event->cmd = cmd;
|
||||||
|
event->data_len = (uint8_t)copy_len;
|
||||||
|
memset(event->data, 0, sizeof(event->data));
|
||||||
|
|
||||||
|
if ((copy_len > 0U) && (data != NULL)) {
|
||||||
|
memcpy(event->data, data, copy_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
APP_EVENT_SUBMIT(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* HID_HOST_COMMAND_EVENT_H__ */
|
||||||
@@ -11,8 +11,12 @@ static void log_hid_tx_event(const struct app_event_header *aeh)
|
|||||||
payload_len = event->dyndata.size - 1U;
|
payload_len = event->dyndata.size - 1U;
|
||||||
}
|
}
|
||||||
|
|
||||||
APP_EVENT_MANAGER_LOG(aeh, "kind=%u report_id=0x%02x payload_len=%u",
|
APP_EVENT_MANAGER_LOG(aeh,
|
||||||
event->kind, report_id, payload_len);
|
"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,
|
static void profile_hid_tx_event(struct log_event_buf *buf,
|
||||||
@@ -26,15 +30,17 @@ static void profile_hid_tx_event(struct log_event_buf *buf,
|
|||||||
}
|
}
|
||||||
|
|
||||||
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->kind);
|
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->kind);
|
||||||
|
nrf_profiler_log_encode_uint8(buf, (uint8_t)event->route);
|
||||||
nrf_profiler_log_encode_uint8(buf, report_id);
|
nrf_profiler_log_encode_uint8(buf, report_id);
|
||||||
nrf_profiler_log_encode_uint16(buf, event->dyndata.size);
|
nrf_profiler_log_encode_uint16(buf, event->dyndata.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
APP_EVENT_INFO_DEFINE(hid_tx_event,
|
APP_EVENT_INFO_DEFINE(hid_tx_event,
|
||||||
ENCODE(NRF_PROFILER_ARG_U8,
|
ENCODE(NRF_PROFILER_ARG_U8,
|
||||||
|
NRF_PROFILER_ARG_U8,
|
||||||
NRF_PROFILER_ARG_U8,
|
NRF_PROFILER_ARG_U8,
|
||||||
NRF_PROFILER_ARG_U16),
|
NRF_PROFILER_ARG_U16),
|
||||||
ENCODE("kind", "report_id", "len"),
|
ENCODE("kind", "route", "report_id", "len"),
|
||||||
profile_hid_tx_event);
|
profile_hid_tx_event);
|
||||||
|
|
||||||
APP_EVENT_TYPE_DEFINE(hid_tx_event,
|
APP_EVENT_TYPE_DEFINE(hid_tx_event,
|
||||||
|
|||||||
@@ -13,21 +13,30 @@ enum hid_tx_kind {
|
|||||||
HID_TX_KIND_REPORT,
|
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 hid_tx_event {
|
||||||
struct app_event_header header;
|
struct app_event_header header;
|
||||||
enum hid_tx_kind kind;
|
enum hid_tx_kind kind;
|
||||||
|
enum hid_tx_route route;
|
||||||
struct event_dyndata dyndata;
|
struct event_dyndata dyndata;
|
||||||
};
|
};
|
||||||
|
|
||||||
APP_EVENT_TYPE_DYNDATA_DECLARE(hid_tx_event);
|
APP_EVENT_TYPE_DYNDATA_DECLARE(hid_tx_event);
|
||||||
|
|
||||||
static inline void hid_tx_event_submit(enum hid_tx_kind kind,
|
static inline void hid_tx_event_submit_routed(enum hid_tx_kind kind,
|
||||||
|
enum hid_tx_route route,
|
||||||
const uint8_t *data,
|
const uint8_t *data,
|
||||||
size_t size)
|
size_t size)
|
||||||
{
|
{
|
||||||
struct hid_tx_event *event = new_hid_tx_event(size);
|
struct hid_tx_event *event = new_hid_tx_event(size);
|
||||||
|
|
||||||
event->kind = kind;
|
event->kind = kind;
|
||||||
|
event->route = route;
|
||||||
if ((size > 0U) && (data != NULL)) {
|
if ((size > 0U) && (data != NULL)) {
|
||||||
memcpy(event->dyndata.data, data, size);
|
memcpy(event->dyndata.data, data, size);
|
||||||
}
|
}
|
||||||
@@ -35,6 +44,13 @@ static inline void hid_tx_event_submit(enum hid_tx_kind kind,
|
|||||||
APP_EVENT_SUBMIT(event);
|
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)
|
static inline const uint8_t *hid_tx_event_get_data(const struct hid_tx_event *event)
|
||||||
{
|
{
|
||||||
return event->dyndata.data;
|
return event->dyndata.data;
|
||||||
@@ -45,4 +61,9 @@ static inline size_t hid_tx_event_get_size(const struct hid_tx_event *event)
|
|||||||
return event->dyndata.size;
|
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__ */
|
#endif /* HID_TX_EVENT_H__ */
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ static const char *time_sync_source_name(enum time_sync_source source)
|
|||||||
return "usb";
|
return "usb";
|
||||||
case TIME_SYNC_SOURCE_MANUAL:
|
case TIME_SYNC_SOURCE_MANUAL:
|
||||||
return "manual";
|
return "manual";
|
||||||
|
case TIME_SYNC_SOURCE_HID:
|
||||||
|
return "hid";
|
||||||
default:
|
default:
|
||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
#include <zephyr/kernel.h>
|
#include <zephyr/kernel.h>
|
||||||
#include <zephyr/device.h>
|
#include <zephyr/device.h>
|
||||||
#include <zephyr/drivers/adc.h>
|
#include <zephyr/drivers/power/ip5306.h>
|
||||||
#include <zephyr/drivers/gpio.h>
|
#include <zephyr/drivers/sensor.h>
|
||||||
#include <zephyr/drivers/power/ip5305.h>
|
#include <zephyr/pm/device.h>
|
||||||
#include <zephyr/sys/atomic.h>
|
#include <zephyr/sys/atomic.h>
|
||||||
#include <zephyr/sys/util.h>
|
#include <zephyr/sys/util.h>
|
||||||
|
|
||||||
@@ -20,11 +20,8 @@
|
|||||||
#include <zephyr/logging/log.h>
|
#include <zephyr/logging/log.h>
|
||||||
LOG_MODULE_REGISTER(MODULE);
|
LOG_MODULE_REGISTER(MODULE);
|
||||||
|
|
||||||
#define BATTERY_USER_NODE DT_PATH(zephyr_user)
|
#define BATTERY_SENSE_NODE DT_NODELABEL(battery_sense)
|
||||||
#define BATTERY_ADC_IO_CH_IDX 1
|
|
||||||
#define BATTERY_SAMPLE_INTERVAL_MS 1000
|
#define BATTERY_SAMPLE_INTERVAL_MS 1000
|
||||||
#define BATTERY_VDIV_NUM 2
|
|
||||||
#define BATTERY_VDIV_DEN 1
|
|
||||||
#define BATTERY_MV_WINDOW_SIZE 10
|
#define BATTERY_MV_WINDOW_SIZE 10
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -35,18 +32,26 @@ LOG_MODULE_REGISTER(MODULE);
|
|||||||
#define BATTERY_EMPTY_MV 3300
|
#define BATTERY_EMPTY_MV 3300
|
||||||
#define BATTERY_FULL_MV 4100
|
#define BATTERY_FULL_MV 4100
|
||||||
|
|
||||||
static const struct adc_dt_spec battery_adc =
|
static const struct device *const ip5306_dev = DEVICE_DT_GET(DT_NODELABEL(ip5306));
|
||||||
ADC_DT_SPEC_GET_BY_IDX(BATTERY_USER_NODE, BATTERY_ADC_IO_CH_IDX);
|
static const struct device *const battery_sensor_dev = DEVICE_DT_GET(BATTERY_SENSE_NODE);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 电池采样使能脚从 zephyr,user/vbat-en-gpios 读取,避免把引脚号硬编码在 C 里。
|
* 板级电源采样结果:
|
||||||
* 后续板级改脚位只需改 DTS,不需要改固件代码。
|
* - 由 board provider 负责给出“原始但可用”的充电状态与电压值;
|
||||||
|
* - 本模块基于这些采样结果做滤波、SOC 估算和事件发布。
|
||||||
*/
|
*/
|
||||||
static const struct gpio_dt_spec battery_en_gpio =
|
struct board_power_sample
|
||||||
GPIO_DT_SPEC_GET(BATTERY_USER_NODE, vbat_en_gpios);
|
{
|
||||||
|
int32_t voltage_mv;
|
||||||
static const struct device *const ip5305_dev = DEVICE_DT_GET(DT_NODELABEL(ip5305));
|
bool charging;
|
||||||
|
bool full;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 对外上报状态:
|
||||||
|
* - 保持现有 battery_status_event 语义不变;
|
||||||
|
* - 只承载业务层需要的 charging/full/soc 三元组。
|
||||||
|
*/
|
||||||
struct battery_status
|
struct battery_status
|
||||||
{
|
{
|
||||||
bool charging;
|
bool charging;
|
||||||
@@ -62,10 +67,16 @@ struct battery_filter_state
|
|||||||
size_t index;
|
size_t index;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 模块上下文:
|
||||||
|
* - sample_work 周期性拉取 board power sample;
|
||||||
|
* - filter 负责平滑电池电压;
|
||||||
|
* - last_status 用于抑制重复事件;
|
||||||
|
* - pm_restrict_level 跟踪当前对 power manager 的限制等级。
|
||||||
|
*/
|
||||||
struct battery_ctx
|
struct battery_ctx
|
||||||
{
|
{
|
||||||
struct k_work_delayable sample_work;
|
struct k_work_delayable sample_work;
|
||||||
int16_t adc_sample_buffer;
|
|
||||||
atomic_t active;
|
atomic_t active;
|
||||||
struct battery_status last_status;
|
struct battery_status last_status;
|
||||||
bool has_last_status;
|
bool has_last_status;
|
||||||
@@ -77,17 +88,14 @@ static struct battery_ctx battery = {
|
|||||||
.pm_restrict_level = POWER_MANAGER_LEVEL_MAX,
|
.pm_restrict_level = POWER_MANAGER_LEVEL_MAX,
|
||||||
};
|
};
|
||||||
|
|
||||||
static void battery_module_resume(void);
|
/* 线性 SOC 估算:把平滑后的电池电压映射到 0~100%。 */
|
||||||
|
|
||||||
static uint8_t soc_from_mv(int32_t mv)
|
static uint8_t soc_from_mv(int32_t mv)
|
||||||
{
|
{
|
||||||
if (mv <= BATTERY_EMPTY_MV)
|
if (mv <= BATTERY_EMPTY_MV) {
|
||||||
{
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mv >= BATTERY_FULL_MV)
|
if (mv >= BATTERY_FULL_MV) {
|
||||||
{
|
|
||||||
return 100;
|
return 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,109 +103,126 @@ static uint8_t soc_from_mv(int32_t mv)
|
|||||||
return (uint8_t)CLAMP(soc, 0, 100);
|
return (uint8_t)CLAMP(soc, 0, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int adc_sample_once_mv(int32_t *mv)
|
/* 初始化/恢复时清空滤波器,避免旧样本影响新一轮估算。 */
|
||||||
|
static void battery_filter_reset(void)
|
||||||
{
|
{
|
||||||
struct adc_sequence sequence = {0};
|
battery.filter.sum = 0;
|
||||||
int err = adc_sequence_init_dt(&battery_adc, &sequence);
|
battery.filter.count = 0;
|
||||||
if (err)
|
battery.filter.index = 0;
|
||||||
{
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
sequence.buffer = &battery.adc_sample_buffer;
|
|
||||||
sequence.buffer_size = sizeof(battery.adc_sample_buffer);
|
|
||||||
|
|
||||||
err = adc_read_dt(&battery_adc, &sequence);
|
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
LOG_WRN("adc_read_dt failed (err=%d)", err);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
*mv = battery.adc_sample_buffer;
|
|
||||||
err = adc_raw_to_millivolts_dt(&battery_adc, mv);
|
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
LOG_WRN("adc_raw_to_millivolts_dt failed (err=%d raw=%d)",
|
|
||||||
err, battery.adc_sample_buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int read_battery_mv(int32_t *mv)
|
|
||||||
{
|
|
||||||
int err;
|
|
||||||
int32_t sensed_mv;
|
|
||||||
|
|
||||||
err = adc_sample_once_mv(&sensed_mv);
|
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
return err;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 板级电池检测存在分压,ADC 读到的是分压后电压。
|
* 将最新电压样本写入固定窗口平均滤波器。
|
||||||
* 这里按分压比还原电池端真实电压,供 SOC 估算与事件上报使用。
|
* 返回值始终是“窗口平均后的电池电压”,供上层做 SOC 估算。
|
||||||
*/
|
*/
|
||||||
int32_t battery_mv = (sensed_mv * BATTERY_VDIV_NUM) / BATTERY_VDIV_DEN;
|
static int32_t battery_filter_apply(int32_t voltage_mv)
|
||||||
|
|
||||||
/*
|
|
||||||
* 使用固定窗口平均抑制采样抖动。
|
|
||||||
* 窗口未填满前按当前样本数求平均,填满后使用环形缓冲滚动更新。
|
|
||||||
*/
|
|
||||||
if (battery.filter.count < BATTERY_MV_WINDOW_SIZE)
|
|
||||||
{
|
{
|
||||||
battery.filter.window[battery.filter.index] = battery_mv;
|
if (battery.filter.count < BATTERY_MV_WINDOW_SIZE) {
|
||||||
battery.filter.sum += battery_mv;
|
battery.filter.window[battery.filter.index] = voltage_mv;
|
||||||
|
battery.filter.sum += voltage_mv;
|
||||||
battery.filter.count++;
|
battery.filter.count++;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
battery.filter.sum -= battery.filter.window[battery.filter.index];
|
battery.filter.sum -= battery.filter.window[battery.filter.index];
|
||||||
battery.filter.window[battery.filter.index] = battery_mv;
|
battery.filter.window[battery.filter.index] = voltage_mv;
|
||||||
battery.filter.sum += battery_mv;
|
battery.filter.sum += voltage_mv;
|
||||||
}
|
}
|
||||||
|
|
||||||
battery.filter.index = (battery.filter.index + 1U) % BATTERY_MV_WINDOW_SIZE;
|
battery.filter.index = (battery.filter.index + 1U) % BATTERY_MV_WINDOW_SIZE;
|
||||||
*mv = (int32_t)(battery.filter.sum / (int64_t)battery.filter.count);
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int read_battery_status(struct battery_status *status)
|
/*
|
||||||
|
* 聚合一次完整的 board power sample:
|
||||||
|
* 1) 先读 PMIC 状态;
|
||||||
|
* 2) 再读 battery_sense 电压;
|
||||||
|
* 3) 最后对电压做窗口平均,输出稳定值。
|
||||||
|
*/
|
||||||
|
static int board_power_monitor_collect_sample(struct board_power_sample *sample)
|
||||||
{
|
{
|
||||||
int err;
|
int32_t voltage_mv;
|
||||||
int32_t mv;
|
int err = board_power_monitor_read_charge_state(&sample->charging, &sample->full);
|
||||||
|
|
||||||
err = ip5305_is_charging(ip5305_dev, &status->charging);
|
if (err) {
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ip5305_is_charge_full(ip5305_dev, &status->full);
|
err = board_power_monitor_read_voltage_mv(&voltage_mv);
|
||||||
if (err)
|
if (err) {
|
||||||
{
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = read_battery_mv(&mv);
|
sample->voltage_mv = battery_filter_apply(voltage_mv);
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
LOG_WRN("read_battery_mv failed (err=%d)", err);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
status->soc = soc_from_mv(mv);
|
|
||||||
return 0;
|
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)
|
static void publish_battery_status_event(const struct battery_status *status)
|
||||||
{
|
{
|
||||||
battery_status_event_submit(status->charging, status->full, status->soc);
|
battery_status_event_submit(status->charging, status->full, status->soc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 判断本轮状态是否值得上报,避免重复事件淹没总线。 */
|
||||||
static bool battery_status_changed(const struct battery_status *lhs,
|
static bool battery_status_changed(const struct battery_status *lhs,
|
||||||
const struct battery_status *rhs)
|
const struct battery_status *rhs)
|
||||||
{
|
{
|
||||||
@@ -223,98 +248,79 @@ static void update_power_restrict_by_charging(bool charging)
|
|||||||
power_manager_restrict(MODULE_IDX(MODULE), 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)
|
static void battery_sampling_set_enabled(bool enable)
|
||||||
{
|
{
|
||||||
atomic_set(&battery.active, enable);
|
atomic_set(&battery.active, enable);
|
||||||
(void)gpio_pin_set_dt(&battery_en_gpio, enable ? GPIO_OUTPUT_ACTIVE : GPIO_OUTPUT_INACTIVE);
|
int err = board_power_monitor_set_voltage_sensor_enabled(enable);
|
||||||
|
|
||||||
if (enable)
|
if (err) {
|
||||||
{
|
LOG_WRN("board_power_monitor_set_voltage_sensor_enabled(%d) failed (err=%d)",
|
||||||
k_work_reschedule(&battery.sample_work, K_NO_WAIT);
|
enable, err);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
if (enable) {
|
||||||
|
/* 延迟开始采样,等待板上采样前端和分压网络稳定。 */
|
||||||
|
k_work_reschedule(&battery.sample_work, K_MSEC(2000));
|
||||||
|
} else {
|
||||||
(void)k_work_cancel_delayable(&battery.sample_work);
|
(void)k_work_cancel_delayable(&battery.sample_work);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 周期性读取 board power sample,并在需要时上报业务状态。 */
|
||||||
static void battery_sample_fn(struct k_work *work)
|
static void battery_sample_fn(struct k_work *work)
|
||||||
{
|
{
|
||||||
ARG_UNUSED(work);
|
ARG_UNUSED(work);
|
||||||
|
|
||||||
if (!atomic_get(&battery.active))
|
if (!atomic_get(&battery.active)) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct board_power_sample sample;
|
||||||
|
struct battery_status status;
|
||||||
|
int err = board_power_monitor_collect_sample(&sample);
|
||||||
|
|
||||||
struct battery_status sampled;
|
if (err) {
|
||||||
int err = read_battery_status(&sampled);
|
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
goto out_reschedule;
|
goto out_reschedule;
|
||||||
}
|
}
|
||||||
|
|
||||||
update_power_restrict_by_charging(sampled.charging);
|
battery_status_from_sample(&sample, &status);
|
||||||
|
update_power_restrict_by_charging(status.charging);
|
||||||
|
|
||||||
/*
|
|
||||||
* 仅在状态发生变化时上报,避免重复事件淹没总线。
|
|
||||||
* 变化条件:充电标志、满电标志、SOC 任意一个变化。
|
|
||||||
*/
|
|
||||||
if (!battery.has_last_status ||
|
if (!battery.has_last_status ||
|
||||||
battery_status_changed(&sampled, &battery.last_status))
|
battery_status_changed(&status, &battery.last_status)) {
|
||||||
{
|
battery.last_status = status;
|
||||||
battery.last_status = sampled;
|
|
||||||
battery.has_last_status = true;
|
battery.has_last_status = true;
|
||||||
publish_battery_status_event(&sampled);
|
publish_battery_status_event(&status);
|
||||||
}
|
}
|
||||||
|
|
||||||
out_reschedule:
|
out_reschedule:
|
||||||
k_work_reschedule(&battery.sample_work, K_MSEC(BATTERY_SAMPLE_INTERVAL_MS));
|
k_work_reschedule(&battery.sample_work, K_MSEC(BATTERY_SAMPLE_INTERVAL_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 初始化 board power monitor consumer,并拉起首轮采样。 */
|
||||||
static int battery_module_init(void)
|
static int battery_module_init(void)
|
||||||
{
|
{
|
||||||
if (!device_is_ready(ip5305_dev))
|
if (!device_is_ready(ip5306_dev)) {
|
||||||
{
|
LOG_ERR("IP5306 device not ready");
|
||||||
LOG_ERR("IP5305 device not ready");
|
|
||||||
return -ENODEV;
|
return -ENODEV;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!adc_is_ready_dt(&battery_adc))
|
if (!device_is_ready(battery_sensor_dev)) {
|
||||||
{
|
LOG_ERR("Battery sense device not ready");
|
||||||
LOG_ERR("Battery ADC device not ready");
|
|
||||||
return -ENODEV;
|
return -ENODEV;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!device_is_ready(battery_en_gpio.port))
|
|
||||||
{
|
|
||||||
LOG_ERR("Battery EN GPIO device not ready");
|
|
||||||
return -ENODEV;
|
|
||||||
}
|
|
||||||
|
|
||||||
int err = gpio_pin_configure_dt(&battery_en_gpio, GPIO_OUTPUT_INACTIVE);
|
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
LOG_ERR("Battery EN GPIO configure failed (err=%d)", err);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
err = adc_channel_setup_dt(&battery_adc);
|
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
LOG_ERR("Battery ADC channel setup failed (err=%d)", err);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 默认非充电态允许进入 SUSPENDED,但禁止进入 OFF。 */
|
/* 默认非充电态允许进入 SUSPENDED,但禁止进入 OFF。 */
|
||||||
update_power_restrict_by_charging(false);
|
update_power_restrict_by_charging(false);
|
||||||
|
|
||||||
k_work_init_delayable(&battery.sample_work, battery_sample_fn);
|
k_work_init_delayable(&battery.sample_work, battery_sample_fn);
|
||||||
battery.has_last_status = false;
|
battery.has_last_status = false;
|
||||||
battery.filter.sum = 0;
|
battery_filter_reset();
|
||||||
battery.filter.count = 0;
|
|
||||||
battery.filter.index = 0;
|
|
||||||
atomic_set(&battery.active, false);
|
atomic_set(&battery.active, false);
|
||||||
|
|
||||||
battery_sampling_set_enabled(true);
|
battery_sampling_set_enabled(true);
|
||||||
@@ -322,10 +328,10 @@ static int battery_module_init(void)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 响应系统挂起:停止采样,并把本模块切到 STANDBY。 */
|
||||||
static void battery_module_suspend(void)
|
static void battery_module_suspend(void)
|
||||||
{
|
{
|
||||||
if (!atomic_get(&battery.active))
|
if (!atomic_get(&battery.active)) {
|
||||||
{
|
|
||||||
/* 已经处于挂起态,避免重复上报 STANDBY 造成 power_down 循环。 */
|
/* 已经处于挂起态,避免重复上报 STANDBY 造成 power_down 循环。 */
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -334,10 +340,10 @@ static void battery_module_suspend(void)
|
|||||||
module_set_state(MODULE_STATE_STANDBY);
|
module_set_state(MODULE_STATE_STANDBY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 响应系统唤醒:恢复电压传感器并重启周期采样。 */
|
||||||
static void battery_module_resume(void)
|
static void battery_module_resume(void)
|
||||||
{
|
{
|
||||||
if (atomic_get(&battery.active))
|
if (atomic_get(&battery.active)) {
|
||||||
{
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,21 +351,18 @@ static void battery_module_resume(void)
|
|||||||
module_set_state(MODULE_STATE_READY);
|
module_set_state(MODULE_STATE_READY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 仅处理模块 ready 和系统电源状态事件,保持模块职责单一。 */
|
||||||
static bool app_event_handler(const struct app_event_header *aeh)
|
static bool app_event_handler(const struct app_event_header *aeh)
|
||||||
{
|
{
|
||||||
if (is_module_state_event(aeh))
|
if (is_module_state_event(aeh)) {
|
||||||
{
|
|
||||||
const struct module_state_event *event = cast_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 (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
|
||||||
{
|
|
||||||
int err = battery_module_init();
|
int err = battery_module_init();
|
||||||
if (err)
|
|
||||||
{
|
if (err) {
|
||||||
module_set_state(MODULE_STATE_ERROR);
|
module_set_state(MODULE_STATE_ERROR);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
module_set_state(MODULE_STATE_READY);
|
module_set_state(MODULE_STATE_READY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
#include <errno.h>
|
#include <zephyr/bluetooth/services/bas.h>
|
||||||
|
|
||||||
#include <zephyr/kernel.h>
|
|
||||||
#include <zephyr/bluetooth/gatt.h>
|
|
||||||
|
|
||||||
#include <app_event_manager.h>
|
#include <app_event_manager.h>
|
||||||
|
|
||||||
@@ -13,50 +10,13 @@
|
|||||||
#include <zephyr/logging/log.h>
|
#include <zephyr/logging/log.h>
|
||||||
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
|
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
|
||||||
|
|
||||||
static bool notify_enabled;
|
|
||||||
static uint8_t battery_level = 100U;
|
|
||||||
|
|
||||||
static void bas_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value)
|
|
||||||
{
|
|
||||||
ARG_UNUSED(attr);
|
|
||||||
notify_enabled = (value == BT_GATT_CCC_NOTIFY);
|
|
||||||
}
|
|
||||||
|
|
||||||
static ssize_t read_battery_level(struct bt_conn *conn,
|
|
||||||
const struct bt_gatt_attr *attr,
|
|
||||||
void *buf,
|
|
||||||
uint16_t len,
|
|
||||||
uint16_t offset)
|
|
||||||
{
|
|
||||||
const uint8_t *value = attr->user_data;
|
|
||||||
|
|
||||||
return bt_gatt_attr_read(conn, attr, buf, len, offset, value, sizeof(*value));
|
|
||||||
}
|
|
||||||
|
|
||||||
BT_GATT_SERVICE_DEFINE(ble_battery_svc,
|
|
||||||
BT_GATT_PRIMARY_SERVICE(BT_UUID_BAS),
|
|
||||||
BT_GATT_CHARACTERISTIC(BT_UUID_BAS_BATTERY_LEVEL,
|
|
||||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
|
|
||||||
BT_GATT_PERM_READ_ENCRYPT,
|
|
||||||
read_battery_level, NULL, &battery_level),
|
|
||||||
BT_GATT_CCC(bas_ccc_cfg_changed,
|
|
||||||
BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT),
|
|
||||||
);
|
|
||||||
|
|
||||||
static bool handle_battery_status_event(const struct battery_status_event *event)
|
static bool handle_battery_status_event(const struct battery_status_event *event)
|
||||||
{
|
{
|
||||||
battery_level = battery_status_event_get_soc(event);
|
uint8_t battery_level = battery_status_event_get_soc(event);
|
||||||
|
int err = bt_bas_set_battery_level(battery_level);
|
||||||
|
|
||||||
if (!notify_enabled) {
|
if (err) {
|
||||||
return false;
|
LOG_ERR("bt_bas_set_battery_level failed: %d", err);
|
||||||
}
|
|
||||||
|
|
||||||
int err = bt_gatt_notify(NULL, &ble_battery_svc.attrs[1], &battery_level, sizeof(battery_level));
|
|
||||||
|
|
||||||
if (err == -ENOTCONN) {
|
|
||||||
LOG_WRN("BAS notify skipped: peer disconnecting");
|
|
||||||
} else if (err) {
|
|
||||||
LOG_ERR("BAS notify failed: %d", err);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <caf/events/ble_common_event.h>
|
#include <caf/events/ble_common_event.h>
|
||||||
|
|
||||||
#include "hid_protocol_event.h"
|
#include "hid_protocol_event.h"
|
||||||
|
#include "hid_host_command_event.h"
|
||||||
#include "hid_report_descriptor.h"
|
#include "hid_report_descriptor.h"
|
||||||
#include "hid_tx_done_event.h"
|
#include "hid_tx_done_event.h"
|
||||||
#include "hid_tx_event.h"
|
#include "hid_tx_event.h"
|
||||||
@@ -18,8 +19,8 @@
|
|||||||
#include <zephyr/logging/log.h>
|
#include <zephyr/logging/log.h>
|
||||||
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
|
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
|
||||||
|
|
||||||
#define INPUT_REPORT_COUNT 3
|
#define INPUT_REPORT_COUNT 4
|
||||||
#define OUTPUT_REPORT_COUNT 2
|
#define OUTPUT_REPORT_COUNT 3
|
||||||
|
|
||||||
BT_HIDS_DEF(hids_obj, INPUT_REPORT_COUNT, OUTPUT_REPORT_COUNT, 0);
|
BT_HIDS_DEF(hids_obj, INPUT_REPORT_COUNT, OUTPUT_REPORT_COUNT, 0);
|
||||||
|
|
||||||
@@ -52,6 +53,19 @@ static bool ble_hid_is_connected(void)
|
|||||||
return ble_hid.link.conn != NULL;
|
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)
|
static bool ble_hid_is_boot_mode(void)
|
||||||
{
|
{
|
||||||
return ble_hid.link.protocol_mode == BT_HIDS_PM_BOOT;
|
return ble_hid.link.protocol_mode == BT_HIDS_PM_BOOT;
|
||||||
@@ -156,6 +170,25 @@ static void vendor_output_report_handler(struct bt_hids_rep *rep,
|
|||||||
LOG_INF("Vendor mask updated over BLE len=%u", rep->size);
|
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 int hids_service_init(void)
|
||||||
{
|
{
|
||||||
static const uint8_t report_map[] = HID_DESC_KEYBOARD_NKRO_CONSUMER();
|
static const uint8_t report_map[] = HID_DESC_KEYBOARD_NKRO_CONSUMER();
|
||||||
@@ -182,6 +215,10 @@ static int hids_service_init(void)
|
|||||||
input_report[2].size = HID_VENDOR_PAYLOAD_SIZE;
|
input_report[2].size = HID_VENDOR_PAYLOAD_SIZE;
|
||||||
input_report[2].handler = report_notify_handler;
|
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].id = REPORT_ID_KEYBOARD;
|
||||||
output_report[0].size = HID_KBD_LED_PAYLOAD_SIZE;
|
output_report[0].size = HID_KBD_LED_PAYLOAD_SIZE;
|
||||||
output_report[0].handler = keyboard_output_report_handler;
|
output_report[0].handler = keyboard_output_report_handler;
|
||||||
@@ -190,6 +227,10 @@ static int hids_service_init(void)
|
|||||||
output_report[1].size = HID_VENDOR_PAYLOAD_SIZE;
|
output_report[1].size = HID_VENDOR_PAYLOAD_SIZE;
|
||||||
output_report[1].handler = vendor_output_report_handler;
|
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.inp_rep_group_init.cnt = INPUT_REPORT_COUNT;
|
||||||
init_param.outp_rep_group_init.cnt = OUTPUT_REPORT_COUNT;
|
init_param.outp_rep_group_init.cnt = OUTPUT_REPORT_COUNT;
|
||||||
init_param.pm_evt_handler = pm_evt_handler;
|
init_param.pm_evt_handler = pm_evt_handler;
|
||||||
@@ -227,7 +268,7 @@ static void handle_ble_peer_event(const struct ble_peer_event *event)
|
|||||||
|
|
||||||
static bool handle_hid_tx_event(const struct hid_tx_event *event)
|
static bool handle_hid_tx_event(const struct hid_tx_event *event)
|
||||||
{
|
{
|
||||||
if (!ble_hid.policy.ble_mode_selected) {
|
if (!ble_hid_should_handle_tx_event(event)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +332,8 @@ static bool handle_hid_tx_event(const struct hid_tx_event *event)
|
|||||||
rep_index = 1U;
|
rep_index = 1U;
|
||||||
} else if (report_id == REPORT_ID_VENDOR) {
|
} else if (report_id == REPORT_ID_VENDOR) {
|
||||||
rep_index = 2U;
|
rep_index = 2U;
|
||||||
|
} else if (report_id == REPORT_ID_VENDOR_CMD) {
|
||||||
|
rep_index = 3U;
|
||||||
} else {
|
} else {
|
||||||
hid_tx_done_event_submit(HID_TX_KIND_REPORT, false);
|
hid_tx_done_event_submit(HID_TX_KIND_REPORT, false);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
#include <zephyr/device.h>
|
#include <zephyr/device.h>
|
||||||
@@ -7,6 +8,7 @@
|
|||||||
#include <zephyr/drivers/display.h>
|
#include <zephyr/drivers/display.h>
|
||||||
#include <zephyr/drivers/led.h>
|
#include <zephyr/drivers/led.h>
|
||||||
#include <zephyr/kernel.h>
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/settings/settings.h>
|
||||||
|
|
||||||
#include <app_event_manager.h>
|
#include <app_event_manager.h>
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
@@ -18,6 +20,8 @@
|
|||||||
#include <caf/events/power_event.h>
|
#include <caf/events/power_event.h>
|
||||||
|
|
||||||
#include "battery_status_event.h"
|
#include "battery_status_event.h"
|
||||||
|
#include "display_theme_event.h"
|
||||||
|
#include "display_ui.h"
|
||||||
#include "keyboard_led_event.h"
|
#include "keyboard_led_event.h"
|
||||||
#include "mode_event.h"
|
#include "mode_event.h"
|
||||||
#include "time_manager.h"
|
#include "time_manager.h"
|
||||||
@@ -28,25 +32,14 @@ LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
|
|||||||
#define DISPLAY_UPDATE_PERIOD_MS 1000
|
#define DISPLAY_UPDATE_PERIOD_MS 1000
|
||||||
#define DISPLAY_IDLE_TIMEOUT_MIN 1
|
#define DISPLAY_IDLE_TIMEOUT_MIN 1
|
||||||
#define DISPLAY_BACKLIGHT_BRIGHTNESS 100
|
#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_YEAR 2026
|
||||||
#define DISPLAY_DEMO_BASE_MONTH 3
|
#define DISPLAY_DEMO_BASE_MONTH 3
|
||||||
#define DISPLAY_DEMO_BASE_DAY 27
|
#define DISPLAY_DEMO_BASE_DAY 27
|
||||||
#define DISPLAY_DEMO_BASE_HOUR 14
|
#define DISPLAY_DEMO_BASE_HOUR 14
|
||||||
#define DISPLAY_DEMO_BASE_MIN 28
|
#define DISPLAY_DEMO_BASE_MIN 28
|
||||||
#define DISPLAY_DEMO_BASE_SEC 36
|
#define DISPLAY_DEMO_BASE_SEC 36
|
||||||
#define DISPLAY_SYMBOL_PLUG "\xEF\x87\xA6" /* U+F1E6, custom plug glyph in ui_font_keyboard_small_18 */
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum display_pm_state
|
enum display_pm_state
|
||||||
{
|
{
|
||||||
@@ -54,22 +47,11 @@ enum display_pm_state
|
|||||||
DISPLAY_PM_STATE_OFF,
|
DISPLAY_PM_STATE_OFF,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct display_ui_state
|
struct display_theme_storage {
|
||||||
{
|
uint8_t red;
|
||||||
lv_color_t theme_color;
|
uint8_t green;
|
||||||
lv_color_t inactive_border_color;
|
uint8_t blue;
|
||||||
uint8_t battery_level;
|
uint8_t valid_marker;
|
||||||
mode_type_t mode;
|
|
||||||
uint8_t led_mask;
|
|
||||||
uint8_t battery_flags;
|
|
||||||
bool status_enabled[DISPLAY_STATUS_COUNT];
|
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct display_ctx
|
struct display_ctx
|
||||||
@@ -78,10 +60,15 @@ struct display_ctx
|
|||||||
struct display_capabilities caps;
|
struct display_capabilities caps;
|
||||||
struct k_work_delayable update_work;
|
struct k_work_delayable update_work;
|
||||||
struct k_work_delayable idle_work;
|
struct k_work_delayable idle_work;
|
||||||
struct display_ui_state ui;
|
struct k_work_delayable theme_save_work;
|
||||||
|
struct display_ui_model ui;
|
||||||
uint32_t tick_count;
|
uint32_t tick_count;
|
||||||
enum display_pm_state pm_state;
|
enum display_pm_state pm_state;
|
||||||
bool initialized;
|
bool initialized;
|
||||||
|
bool theme_storage_dirty;
|
||||||
|
bool theme_storage_loaded;
|
||||||
|
char date_text[16];
|
||||||
|
char time_text[16];
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct display_ctx disp = {
|
static struct display_ctx disp = {
|
||||||
@@ -91,21 +78,102 @@ static struct display_ctx disp = {
|
|||||||
.ui.battery_level = 15U,
|
.ui.battery_level = 15U,
|
||||||
.ui.battery_flags = 0U,
|
.ui.battery_flags = 0U,
|
||||||
.ui.mode = MODE_TYPE_USB,
|
.ui.mode = MODE_TYPE_USB,
|
||||||
.ui.status_enabled = {true, true, false, true},
|
.ui.led_mask = 0U,
|
||||||
.pm_state = DISPLAY_PM_STATE_OFF,
|
.pm_state = DISPLAY_PM_STATE_OFF,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static struct display_theme_storage display_theme_storage;
|
||||||
|
|
||||||
static const struct led_dt_spec display_backlight =
|
static const struct led_dt_spec display_backlight =
|
||||||
LED_DT_SPEC_GET(DT_NODELABEL(backlight));
|
LED_DT_SPEC_GET(DT_NODELABEL(backlight));
|
||||||
|
|
||||||
static const char *const g_status_texts[DISPLAY_STATUS_COUNT] = {
|
static int display_theme_store(const struct display_theme_storage *storage)
|
||||||
LV_SYMBOL_USB,
|
{
|
||||||
LV_SYMBOL_BLUETOOTH,
|
char key[] = MODULE_NAME "/" DISPLAY_THEME_STORAGE_KEY;
|
||||||
"1",
|
int err = settings_save_one(key, storage, sizeof(*storage));
|
||||||
"A",
|
|
||||||
};
|
|
||||||
|
|
||||||
static void display_refresh_all_locked(void);
|
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)
|
static void display_schedule_update(k_timeout_t delay)
|
||||||
{
|
{
|
||||||
@@ -121,7 +189,6 @@ static void display_schedule_idle_timeout(k_timeout_t delay)
|
|||||||
k_work_reschedule(&disp.idle_work, delay);
|
k_work_reschedule(&disp.idle_work, delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 背光初始化独立处理,避免 UI 创建逻辑里混入硬件使能细节。 */
|
|
||||||
static int display_backlight_set(uint8_t brightness)
|
static int display_backlight_set(uint8_t brightness)
|
||||||
{
|
{
|
||||||
int err;
|
int err;
|
||||||
@@ -147,7 +214,59 @@ static bool display_is_active(void)
|
|||||||
return disp.pm_state == DISPLAY_PM_STATE_ACTIVE;
|
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)
|
static void display_kick_idle_timer(void)
|
||||||
{
|
{
|
||||||
if (!disp.initialized || !display_is_active())
|
if (!disp.initialized || !display_is_active())
|
||||||
@@ -156,7 +275,6 @@ static void display_kick_idle_timer(void)
|
|||||||
display_schedule_idle_timeout(K_MINUTES(DISPLAY_IDLE_TIMEOUT_MIN));
|
display_schedule_idle_timeout(K_MINUTES(DISPLAY_IDLE_TIMEOUT_MIN));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 熄屏时同时关闭刷新和背光,并将模块状态切到 OFF。 */
|
|
||||||
static void display_sleep(void)
|
static void display_sleep(void)
|
||||||
{
|
{
|
||||||
int err;
|
int err;
|
||||||
@@ -166,6 +284,12 @@ static void display_sleep(void)
|
|||||||
|
|
||||||
(void)k_work_cancel_delayable(&disp.update_work);
|
(void)k_work_cancel_delayable(&disp.update_work);
|
||||||
(void)k_work_cancel_delayable(&disp.idle_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);
|
err = display_blanking_on(disp.dev);
|
||||||
if (err)
|
if (err)
|
||||||
@@ -176,7 +300,6 @@ static void display_sleep(void)
|
|||||||
module_set_state(MODULE_STATE_OFF);
|
module_set_state(MODULE_STATE_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 唤醒屏幕后立刻刷新 UI,并重新启动定时刷新和空闲超时。 */
|
|
||||||
static void display_wake(void)
|
static void display_wake(void)
|
||||||
{
|
{
|
||||||
int err;
|
int err;
|
||||||
@@ -212,309 +335,6 @@ static void display_idle_timeout_fn(struct k_work *work)
|
|||||||
display_sleep();
|
display_sleep();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 电量颜色与 PC 原型保持一致,顶部状态区能快速表达健康度。 */
|
|
||||||
static lv_color_t display_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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 电池图标由精简图标字体提供,不再依赖 LVGL 内建字体资源。 */
|
|
||||||
static const char *display_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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 模式事件只需要驱动 USB/BLE 两个 badge,2.4G 模式两者都灭。 */
|
|
||||||
static void display_update_mode_state(mode_type_t mode)
|
|
||||||
{
|
|
||||||
disp.ui.mode = mode;
|
|
||||||
disp.ui.status_enabled[DISPLAY_STATUS_USB] = (mode == MODE_TYPE_USB);
|
|
||||||
disp.ui.status_enabled[DISPLAY_STATUS_BLE] = (mode == MODE_TYPE_BLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 最新原型只显示 NumLock 和 CapsLock,不再展示 ScrollLock。 */
|
|
||||||
static void display_update_keyboard_led_state(uint8_t led_mask)
|
|
||||||
{
|
|
||||||
disp.ui.led_mask = led_mask;
|
|
||||||
disp.ui.status_enabled[DISPLAY_STATUS_NUMLOCK] =
|
|
||||||
(led_mask & KEYBOARD_LED_MASK_NUM_LOCK) != 0U;
|
|
||||||
disp.ui.status_enabled[DISPLAY_STATUS_CAPSLOCK] =
|
|
||||||
(led_mask & KEYBOARD_LED_MASK_CAPS_LOCK) != 0U;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 底部状态条的亮灭与边框颜色联动更新,保持原型机视觉语言。 */
|
|
||||||
static void display_refresh_status_bar_locked(void)
|
|
||||||
{
|
|
||||||
for (uint32_t i = 0; i < DISPLAY_STATUS_COUNT; i++)
|
|
||||||
{
|
|
||||||
lv_obj_t *badge = disp.ui.status_badges[i];
|
|
||||||
lv_obj_t *label = disp.ui.status_labels[i];
|
|
||||||
bool active = disp.ui.status_enabled[i];
|
|
||||||
|
|
||||||
if (!badge || !label)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
lv_obj_set_style_border_width(badge, 4, 0);
|
|
||||||
lv_obj_set_style_border_color(badge,
|
|
||||||
active ? disp.ui.theme_color : disp.ui.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 电池图标、百分比和状态图标分开更新,便于独立配色。 */
|
|
||||||
static void display_refresh_battery_locked(void)
|
|
||||||
{
|
|
||||||
char battery_text[8];
|
|
||||||
lv_color_t battery_color;
|
|
||||||
const char *state_symbol = "";
|
|
||||||
lv_color_t state_color = lv_color_white();
|
|
||||||
|
|
||||||
if (!disp.ui.battery_icon || !disp.ui.battery_label || !disp.ui.battery_state_label)
|
|
||||||
return;
|
|
||||||
|
|
||||||
battery_color = display_get_battery_color(disp.ui.battery_level);
|
|
||||||
snprintk(battery_text, sizeof(battery_text), "%u%%", disp.ui.battery_level);
|
|
||||||
|
|
||||||
if ((disp.ui.battery_flags & BATTERY_STATUS_FLAG_FULL) != 0U)
|
|
||||||
{
|
|
||||||
state_symbol = DISPLAY_SYMBOL_PLUG;
|
|
||||||
state_color = lv_color_hex(0x4C9EF5);
|
|
||||||
}
|
|
||||||
else if ((disp.ui.battery_flags & BATTERY_STATUS_FLAG_CHARGING) != 0U)
|
|
||||||
{
|
|
||||||
state_symbol = LV_SYMBOL_CHARGE;
|
|
||||||
state_color = lv_color_hex(0xF4D35E);
|
|
||||||
}
|
|
||||||
|
|
||||||
lv_label_set_text(disp.ui.battery_icon,
|
|
||||||
display_get_battery_symbol(disp.ui.battery_level));
|
|
||||||
lv_obj_set_style_text_color(disp.ui.battery_icon, battery_color, 0);
|
|
||||||
lv_label_set_text(disp.ui.battery_label, battery_text);
|
|
||||||
lv_label_set_text(disp.ui.battery_state_label, state_symbol);
|
|
||||||
lv_obj_set_style_text_color(disp.ui.battery_state_label, state_color, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 时间优先显示 time_manager 的真实快照。
|
|
||||||
* 如果当前尚未同步,则退回到固定基准上的 demo 时间,保证 UI 结构始终可见。
|
|
||||||
*/
|
|
||||||
static void display_refresh_datetime_locked(void)
|
|
||||||
{
|
|
||||||
struct time_manager_snapshot snapshot;
|
|
||||||
char date_text[16];
|
|
||||||
char time_text[16];
|
|
||||||
int err = time_manager_get_snapshot(&snapshot);
|
|
||||||
|
|
||||||
if (!disp.ui.date_label || !disp.ui.time_label)
|
|
||||||
return;
|
|
||||||
|
|
||||||
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(date_text, sizeof(date_text), "%04u/%02u/%02u",
|
|
||||||
year, month, day);
|
|
||||||
snprintk(time_text, sizeof(time_text), "%02u:%02u:%02u",
|
|
||||||
hour, minute, second);
|
|
||||||
lv_label_set_text(disp.ui.date_label, date_text);
|
|
||||||
lv_label_set_text(disp.ui.time_label, time_text);
|
|
||||||
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(date_text, sizeof(date_text), "%04d/%02d/%02d",
|
|
||||||
DISPLAY_DEMO_BASE_YEAR,
|
|
||||||
DISPLAY_DEMO_BASE_MONTH,
|
|
||||||
DISPLAY_DEMO_BASE_DAY);
|
|
||||||
snprintk(time_text, sizeof(time_text), "%02u:%02u:%02u",
|
|
||||||
hour, minute, second);
|
|
||||||
lv_label_set_text(disp.ui.date_label, date_text);
|
|
||||||
lv_label_set_text(disp.ui.time_label, time_text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 一次性把缓存状态刷到 UI,避免控件创建与状态恢复互相耦合。 */
|
|
||||||
static void display_refresh_all_locked(void)
|
|
||||||
{
|
|
||||||
display_refresh_status_bar_locked();
|
|
||||||
display_refresh_battery_locked();
|
|
||||||
display_refresh_datetime_locked();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 状态 badge 保持原型尺寸和圆角,确保在 320x172 面板上视觉一致。 */
|
|
||||||
static void display_create_status_chip_locked(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);
|
|
||||||
|
|
||||||
disp.ui.status_badges[id] = badge;
|
|
||||||
disp.ui.status_labels[id] = label;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* UI 直接内联到 display_module,保留原型布局而不引入额外 ui 抽象层。 */
|
|
||||||
static void display_create_ui_locked(void)
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
disp.ui.date_label = lv_label_create(top_row);
|
|
||||||
lv_obj_set_style_text_font(disp.ui.date_label, &ui_font_keyboard_small_18, 0);
|
|
||||||
lv_obj_set_style_text_color(disp.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);
|
|
||||||
|
|
||||||
disp.ui.battery_icon = lv_label_create(battery_wrap);
|
|
||||||
lv_obj_set_style_text_font(disp.ui.battery_icon, &ui_font_keyboard_small_18, 0);
|
|
||||||
|
|
||||||
disp.ui.battery_label = lv_label_create(battery_wrap);
|
|
||||||
lv_obj_set_style_text_font(disp.ui.battery_label, &ui_font_keyboard_small_18, 0);
|
|
||||||
lv_obj_set_style_text_color(disp.ui.battery_label, lv_color_hex(0xD8DEE9), 0);
|
|
||||||
|
|
||||||
disp.ui.battery_state_label = lv_label_create(battery_wrap);
|
|
||||||
lv_obj_set_style_text_font(disp.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);
|
|
||||||
|
|
||||||
disp.ui.time_label = lv_label_create(middle_row);
|
|
||||||
lv_obj_set_style_text_font(disp.ui.time_label, &ui_font_keyboard_time_48, 0);
|
|
||||||
lv_obj_set_style_text_color(disp.ui.time_label, lv_color_white(), 0);
|
|
||||||
lv_obj_center(disp.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_create_status_chip_locked(bottom_row, (enum display_status_id)i);
|
|
||||||
|
|
||||||
display_refresh_all_locked();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 周期刷新只负责时间区域;状态图标改为事件驱动,避免无谓重绘。 */
|
|
||||||
static void display_update_work_fn(struct k_work *work)
|
static void display_update_work_fn(struct k_work *work)
|
||||||
{
|
{
|
||||||
ARG_UNUSED(work);
|
ARG_UNUSED(work);
|
||||||
@@ -526,15 +346,15 @@ static void display_update_work_fn(struct k_work *work)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
disp.tick_count++;
|
disp.tick_count++;
|
||||||
|
display_update_datetime_text();
|
||||||
|
|
||||||
lvgl_lock();
|
lvgl_lock();
|
||||||
display_refresh_datetime_locked();
|
display_ui_refresh_datetime(disp.date_text, disp.time_text);
|
||||||
lvgl_unlock();
|
lvgl_unlock();
|
||||||
|
|
||||||
display_schedule_update(K_MSEC(DISPLAY_UPDATE_PERIOD_MS));
|
display_schedule_update(K_MSEC(DISPLAY_UPDATE_PERIOD_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 显示初始化完成后,后续 UI 更新全部通过事件和定时刷新驱动。 */
|
|
||||||
static int display_init(void)
|
static int display_init(void)
|
||||||
{
|
{
|
||||||
int err;
|
int err;
|
||||||
@@ -553,7 +373,10 @@ static int display_init(void)
|
|||||||
|
|
||||||
k_work_init_delayable(&disp.update_work, display_update_work_fn);
|
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.idle_work, display_idle_timeout_fn);
|
||||||
|
k_work_init_delayable(&disp.theme_save_work, display_theme_save_work_fn);
|
||||||
disp.tick_count = 0U;
|
disp.tick_count = 0U;
|
||||||
|
display_theme_apply_loaded_storage();
|
||||||
|
display_update_datetime_text();
|
||||||
|
|
||||||
err = display_blanking_off(disp.dev);
|
err = display_blanking_off(disp.dev);
|
||||||
if (err)
|
if (err)
|
||||||
@@ -567,7 +390,7 @@ static int display_init(void)
|
|||||||
return err;
|
return err;
|
||||||
|
|
||||||
lvgl_lock();
|
lvgl_lock();
|
||||||
display_create_ui_locked();
|
display_ui_init(&disp.ui, disp.date_text, disp.time_text);
|
||||||
lvgl_unlock();
|
lvgl_unlock();
|
||||||
|
|
||||||
disp.initialized = true;
|
disp.initialized = true;
|
||||||
@@ -579,59 +402,63 @@ static int display_init(void)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 电池事件只缓存最新 SOC,UI 若已就绪则立即刷新顶部电池区域。 */
|
|
||||||
static bool handle_battery_status_event(const struct battery_status_event *event)
|
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_level = battery_status_event_get_soc(event);
|
||||||
disp.ui.battery_flags = battery_status_event_get_flags(event);
|
disp.ui.battery_flags = battery_status_event_get_flags(event);
|
||||||
|
|
||||||
if (!disp.initialized)
|
if (!disp.initialized || !display_is_active()) {
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!display_is_active())
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
lvgl_lock();
|
lvgl_lock();
|
||||||
display_refresh_battery_locked();
|
display_ui_refresh_battery(&disp.ui);
|
||||||
lvgl_unlock();
|
lvgl_unlock();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 模式事件只影响 USB/BLE 两个 badge 的亮灭。 */
|
|
||||||
static bool handle_mode_event(const struct mode_event *event)
|
static bool handle_mode_event(const struct mode_event *event)
|
||||||
{
|
{
|
||||||
display_update_mode_state(event->mode_type);
|
disp.ui.mode = event->mode_type;
|
||||||
|
|
||||||
if (!disp.initialized)
|
if (!disp.initialized || !display_is_active()) {
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!display_is_active())
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
lvgl_lock();
|
lvgl_lock();
|
||||||
display_refresh_status_bar_locked();
|
display_ui_refresh_status_bar(&disp.ui);
|
||||||
lvgl_unlock();
|
lvgl_unlock();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* NumLock/CapsLock/ScrollLock 变化后,底部三个状态 badge 立即更新。 */
|
|
||||||
static bool handle_keyboard_led_event(const struct keyboard_led_event *event)
|
static bool handle_keyboard_led_event(const struct keyboard_led_event *event)
|
||||||
{
|
{
|
||||||
display_update_keyboard_led_state(keyboard_led_event_get_mask(event));
|
disp.ui.led_mask = keyboard_led_event_get_mask(event);
|
||||||
|
|
||||||
if (!disp.initialized)
|
if (!disp.initialized || !display_is_active()) {
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!display_is_active())
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
lvgl_lock();
|
lvgl_lock();
|
||||||
display_refresh_status_bar_locked();
|
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();
|
lvgl_unlock();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 任意按钮事件都可点亮屏幕并重置 1 分钟空闲计时。 */
|
|
||||||
static bool handle_button_event(const struct button_event *event)
|
static bool handle_button_event(const struct button_event *event)
|
||||||
{
|
{
|
||||||
ARG_UNUSED(event);
|
ARG_UNUSED(event);
|
||||||
@@ -654,6 +481,18 @@ static bool handle_wake_up_event(void)
|
|||||||
|
|
||||||
static bool handle_module_state_event(const struct module_state_event *event)
|
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))
|
if (!check_state(event, MODULE_ID(main), MODULE_STATE_READY))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -683,6 +522,9 @@ static bool app_event_handler(const struct app_event_header *aeh)
|
|||||||
if (is_keyboard_led_event(aeh))
|
if (is_keyboard_led_event(aeh))
|
||||||
return handle_keyboard_led_event(cast_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))
|
if (is_button_event(aeh))
|
||||||
return handle_button_event(cast_button_event(aeh));
|
return handle_button_event(cast_button_event(aeh));
|
||||||
|
|
||||||
@@ -699,6 +541,7 @@ static bool app_event_handler(const struct app_event_header *aeh)
|
|||||||
APP_EVENT_LISTENER(MODULE, app_event_handler);
|
APP_EVENT_LISTENER(MODULE, app_event_handler);
|
||||||
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
|
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
|
||||||
APP_EVENT_SUBSCRIBE(MODULE, battery_status_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, mode_event);
|
||||||
APP_EVENT_SUBSCRIBE(MODULE, keyboard_led_event);
|
APP_EVENT_SUBSCRIBE(MODULE, keyboard_led_event);
|
||||||
APP_EVENT_SUBSCRIBE(MODULE, button_event);
|
APP_EVENT_SUBSCRIBE(MODULE, button_event);
|
||||||
|
|||||||
121
src/modules/hid_host_command_module.c
Normal file
121
src/modules/hid_host_command_module.c
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#include <zephyr/sys/byteorder.h>
|
||||||
|
|
||||||
|
#include <app_event_manager.h>
|
||||||
|
|
||||||
|
#define MODULE hid_host_command
|
||||||
|
#include <caf/events/module_state_event.h>
|
||||||
|
|
||||||
|
#include "display_theme_event.h"
|
||||||
|
#include "hid_host_ack_event.h"
|
||||||
|
#include "hid_host_command_error_event.h"
|
||||||
|
#include "hid_host_command_event.h"
|
||||||
|
#include "hid_host_command_protocol.h"
|
||||||
|
#include "time_manager.h"
|
||||||
|
#include "time_sync_event.h"
|
||||||
|
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
|
||||||
|
|
||||||
|
static bool module_ready;
|
||||||
|
|
||||||
|
static bool handle_theme_color_command(const struct hid_host_command_event *event)
|
||||||
|
{
|
||||||
|
if (event->data_len < HID_HOST_CMD_THEME_PARAM_SIZE) {
|
||||||
|
hid_host_command_error_event_submit(
|
||||||
|
event->transport,
|
||||||
|
event->cmd,
|
||||||
|
HID_HOST_COMMAND_ERROR_INVALID_LENGTH);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
display_theme_event_submit(event->data[0], event->data[1], event->data[2]);
|
||||||
|
hid_host_ack_event_submit(event->transport, event->cmd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool handle_time_sync_command(const struct hid_host_command_event *event)
|
||||||
|
{
|
||||||
|
struct time_sync_update update = {
|
||||||
|
.timezone_min = 0,
|
||||||
|
.accuracy_ms = 0,
|
||||||
|
.source = TIME_SYNC_SOURCE_HID,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event->data_len != HID_HOST_CMD_TIME_SYNC_PARAM_SIZE) {
|
||||||
|
hid_host_command_error_event_submit(
|
||||||
|
event->transport,
|
||||||
|
event->cmd,
|
||||||
|
HID_HOST_COMMAND_ERROR_INVALID_LENGTH);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!time_manager_is_ready()) {
|
||||||
|
hid_host_command_error_event_submit(
|
||||||
|
event->transport,
|
||||||
|
event->cmd,
|
||||||
|
HID_HOST_COMMAND_ERROR_NOT_READY);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
update.utc_ms = sys_get_le64(event->data);
|
||||||
|
if (update.utc_ms == 0U) {
|
||||||
|
hid_host_command_error_event_submit(
|
||||||
|
event->transport,
|
||||||
|
event->cmd,
|
||||||
|
HID_HOST_COMMAND_ERROR_INVALID_PARAM);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
time_sync_event_submit(&update);
|
||||||
|
hid_host_ack_event_submit(event->transport, event->cmd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool handle_hid_host_command_event(const struct hid_host_command_event *event)
|
||||||
|
{
|
||||||
|
if (!module_ready) {
|
||||||
|
hid_host_command_error_event_submit(
|
||||||
|
event->transport,
|
||||||
|
event->cmd,
|
||||||
|
HID_HOST_COMMAND_ERROR_NOT_READY);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event->cmd) {
|
||||||
|
case HID_HOST_CMD_ID_THEME_COLOR:
|
||||||
|
return handle_theme_color_command(event);
|
||||||
|
case HID_HOST_CMD_ID_TIME_SYNC:
|
||||||
|
return handle_time_sync_command(event);
|
||||||
|
default:
|
||||||
|
hid_host_command_error_event_submit(
|
||||||
|
event->transport,
|
||||||
|
event->cmd,
|
||||||
|
HID_HOST_COMMAND_ERROR_UNKNOWN_CMD);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool app_event_handler(const struct app_event_header *aeh)
|
||||||
|
{
|
||||||
|
if (is_module_state_event(aeh)) {
|
||||||
|
const struct module_state_event *event = cast_module_state_event(aeh);
|
||||||
|
|
||||||
|
if (check_state(event, MODULE_ID(main), MODULE_STATE_READY)) {
|
||||||
|
module_ready = true;
|
||||||
|
module_set_state(MODULE_STATE_READY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_hid_host_command_event(aeh)) {
|
||||||
|
return handle_hid_host_command_event(cast_hid_host_command_event(aeh));
|
||||||
|
}
|
||||||
|
|
||||||
|
__ASSERT_NO_MSG(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
APP_EVENT_LISTENER(MODULE, app_event_handler);
|
||||||
|
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
|
||||||
|
APP_EVENT_SUBSCRIBE(MODULE, hid_host_command_event);
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "hid_report_descriptor.h"
|
#include "hid_report_descriptor.h"
|
||||||
#include "hid_boot_event.h"
|
#include "hid_boot_event.h"
|
||||||
|
#include "hid_host_ack_event.h"
|
||||||
#include "hid_report_event.h"
|
#include "hid_report_event.h"
|
||||||
#include "hid_tx_done_event.h"
|
#include "hid_tx_done_event.h"
|
||||||
#include "hid_tx_event.h"
|
#include "hid_tx_event.h"
|
||||||
@@ -26,14 +27,15 @@ enum hid_tx_flag {
|
|||||||
HID_TX_FLAG_IN_FLIGHT,
|
HID_TX_FLAG_IN_FLIGHT,
|
||||||
HID_TX_FLAG_BOOT_VALID,
|
HID_TX_FLAG_BOOT_VALID,
|
||||||
HID_TX_FLAG_BOOT_DIRTY,
|
HID_TX_FLAG_BOOT_DIRTY,
|
||||||
HID_TX_FLAG_NKRO_VALID,
|
HID_TX_FLAG_KEYBOARD_VALID,
|
||||||
HID_TX_FLAG_NKRO_DIRTY,
|
HID_TX_FLAG_KEYBOARD_DIRTY,
|
||||||
HID_TX_FLAG_VENDOR_VALID,
|
HID_TX_FLAG_VENDOR_VALID,
|
||||||
HID_TX_FLAG_VENDOR_DIRTY,
|
HID_TX_FLAG_VENDOR_DIRTY,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct hid_tx_item {
|
struct hid_tx_item {
|
||||||
enum hid_tx_kind kind;
|
enum hid_tx_kind kind;
|
||||||
|
enum hid_tx_route route;
|
||||||
size_t len;
|
size_t len;
|
||||||
uint8_t data[HID_TX_MAX_DATA];
|
uint8_t data[HID_TX_MAX_DATA];
|
||||||
};
|
};
|
||||||
@@ -41,7 +43,7 @@ struct hid_tx_item {
|
|||||||
struct hid_tx_ctx {
|
struct hid_tx_ctx {
|
||||||
atomic_t flags;
|
atomic_t flags;
|
||||||
struct hid_tx_item boot_state;
|
struct hid_tx_item boot_state;
|
||||||
struct hid_tx_item nkro_state;
|
struct hid_tx_item keyboard_state;
|
||||||
struct hid_tx_item vendor_state;
|
struct hid_tx_item vendor_state;
|
||||||
struct hid_tx_item inflight_item;
|
struct hid_tx_item inflight_item;
|
||||||
mode_type_t active_mode;
|
mode_type_t active_mode;
|
||||||
@@ -51,10 +53,13 @@ static struct hid_tx_ctx tx = {
|
|||||||
.active_mode = MODE_TYPE_COUNT,
|
.active_mode = MODE_TYPE_COUNT,
|
||||||
};
|
};
|
||||||
|
|
||||||
K_MSGQ_DEFINE(hid_tx_queue_msgq, sizeof(struct hid_tx_item), HID_TX_QUEUE_SIZE, 4);
|
K_MSGQ_DEFINE(hid_tx_consumer_msgq, sizeof(struct hid_tx_item), HID_TX_QUEUE_SIZE, 4);
|
||||||
|
K_MSGQ_DEFINE(hid_tx_ack_msgq, sizeof(struct hid_tx_item), HID_TX_QUEUE_SIZE, 4);
|
||||||
|
K_MSGQ_DEFINE(hid_tx_misc_msgq, sizeof(struct hid_tx_item), HID_TX_QUEUE_SIZE, 4);
|
||||||
|
|
||||||
static bool hid_tx_item_store(struct hid_tx_item *item,
|
static bool hid_tx_item_store(struct hid_tx_item *item,
|
||||||
enum hid_tx_kind kind,
|
enum hid_tx_kind kind,
|
||||||
|
enum hid_tx_route route,
|
||||||
const uint8_t *data,
|
const uint8_t *data,
|
||||||
size_t len)
|
size_t len)
|
||||||
{
|
{
|
||||||
@@ -64,6 +69,7 @@ static bool hid_tx_item_store(struct hid_tx_item *item,
|
|||||||
}
|
}
|
||||||
|
|
||||||
item->kind = kind;
|
item->kind = kind;
|
||||||
|
item->route = route;
|
||||||
item->len = len;
|
item->len = len;
|
||||||
if ((len > 0U) && (data != NULL)) {
|
if ((len > 0U) && (data != NULL)) {
|
||||||
memcpy(item->data, data, len);
|
memcpy(item->data, data, len);
|
||||||
@@ -72,15 +78,19 @@ static bool hid_tx_item_store(struct hid_tx_item *item,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool hid_tx_queue_push(enum hid_tx_kind kind, const uint8_t *data, size_t len)
|
static bool hid_tx_queue_push(struct k_msgq *queue,
|
||||||
|
enum hid_tx_kind kind,
|
||||||
|
enum hid_tx_route route,
|
||||||
|
const uint8_t *data,
|
||||||
|
size_t len)
|
||||||
{
|
{
|
||||||
struct hid_tx_item item;
|
struct hid_tx_item item;
|
||||||
|
|
||||||
if (!hid_tx_item_store(&item, kind, data, len)) {
|
if (!hid_tx_item_store(&item, kind, route, data, len)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (k_msgq_put(&hid_tx_queue_msgq, &item, K_NO_WAIT)) {
|
if (k_msgq_put(queue, &item, K_NO_WAIT)) {
|
||||||
LOG_WRN("Drop HID tx kind=%u len=%u: queue full", kind, len);
|
LOG_WRN("Drop HID tx kind=%u len=%u: queue full", kind, len);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -88,11 +98,16 @@ static bool hid_tx_queue_push(enum hid_tx_kind kind, const uint8_t *data, size_t
|
|||||||
return true;
|
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)
|
static bool hid_tx_dispatch_item(const struct hid_tx_item *item)
|
||||||
{
|
{
|
||||||
tx.inflight_item = *item;
|
tx.inflight_item = *item;
|
||||||
atomic_set_bit(&tx.flags, HID_TX_FLAG_IN_FLIGHT);
|
atomic_set_bit(&tx.flags, HID_TX_FLAG_IN_FLIGHT);
|
||||||
hid_tx_event_submit(item->kind, item->data, item->len);
|
hid_tx_event_submit_routed(item->kind, item->route, item->data, item->len);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,33 +120,44 @@ static void dispatch_next_if_possible(void)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((tx.active_mode != MODE_TYPE_USB) && (tx.active_mode != MODE_TYPE_BLE)) {
|
if (hid_tx_auto_route_available() &&
|
||||||
|
atomic_test_bit(&tx.flags, HID_TX_FLAG_KEYBOARD_DIRTY) &&
|
||||||
|
atomic_test_bit(&tx.flags, HID_TX_FLAG_KEYBOARD_VALID)) {
|
||||||
|
atomic_clear_bit(&tx.flags, HID_TX_FLAG_KEYBOARD_DIRTY);
|
||||||
|
(void)hid_tx_dispatch_item(&tx.keyboard_state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (atomic_test_bit(&tx.flags, HID_TX_FLAG_NKRO_DIRTY) &&
|
if (hid_tx_auto_route_available() &&
|
||||||
atomic_test_bit(&tx.flags, HID_TX_FLAG_NKRO_VALID)) {
|
atomic_test_bit(&tx.flags, HID_TX_FLAG_BOOT_DIRTY) &&
|
||||||
atomic_clear_bit(&tx.flags, HID_TX_FLAG_NKRO_DIRTY);
|
|
||||||
(void)hid_tx_dispatch_item(&tx.nkro_state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (atomic_test_bit(&tx.flags, HID_TX_FLAG_BOOT_DIRTY) &&
|
|
||||||
atomic_test_bit(&tx.flags, HID_TX_FLAG_BOOT_VALID)) {
|
atomic_test_bit(&tx.flags, HID_TX_FLAG_BOOT_VALID)) {
|
||||||
atomic_clear_bit(&tx.flags, HID_TX_FLAG_BOOT_DIRTY);
|
atomic_clear_bit(&tx.flags, HID_TX_FLAG_BOOT_DIRTY);
|
||||||
(void)hid_tx_dispatch_item(&tx.boot_state);
|
(void)hid_tx_dispatch_item(&tx.boot_state);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!k_msgq_get(&hid_tx_queue_msgq, &item, K_NO_WAIT)) {
|
if (hid_tx_auto_route_available() &&
|
||||||
|
!k_msgq_get(&hid_tx_consumer_msgq, &item, K_NO_WAIT)) {
|
||||||
(void)hid_tx_dispatch_item(&item);
|
(void)hid_tx_dispatch_item(&item);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (atomic_test_bit(&tx.flags, HID_TX_FLAG_VENDOR_DIRTY) &&
|
if (hid_tx_auto_route_available() &&
|
||||||
|
atomic_test_bit(&tx.flags, HID_TX_FLAG_VENDOR_DIRTY) &&
|
||||||
atomic_test_bit(&tx.flags, HID_TX_FLAG_VENDOR_VALID)) {
|
atomic_test_bit(&tx.flags, HID_TX_FLAG_VENDOR_VALID)) {
|
||||||
atomic_clear_bit(&tx.flags, HID_TX_FLAG_VENDOR_DIRTY);
|
atomic_clear_bit(&tx.flags, HID_TX_FLAG_VENDOR_DIRTY);
|
||||||
(void)hid_tx_dispatch_item(&tx.vendor_state);
|
(void)hid_tx_dispatch_item(&tx.vendor_state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!k_msgq_get(&hid_tx_ack_msgq, &item, K_NO_WAIT)) {
|
||||||
|
(void)hid_tx_dispatch_item(&item);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hid_tx_auto_route_available() &&
|
||||||
|
!k_msgq_get(&hid_tx_misc_msgq, &item, K_NO_WAIT)) {
|
||||||
|
(void)hid_tx_dispatch_item(&item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,10 +182,23 @@ static bool handle_mode_event(const struct mode_event *event)
|
|||||||
return false;
|
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)
|
static bool handle_hid_boot_request_event(const struct hid_boot_event *event)
|
||||||
{
|
{
|
||||||
(void)hid_tx_item_store(&tx.boot_state,
|
(void)hid_tx_item_store(&tx.boot_state,
|
||||||
HID_TX_KIND_BOOT,
|
HID_TX_KIND_BOOT,
|
||||||
|
HID_TX_ROUTE_AUTO,
|
||||||
hid_boot_event_get_data(event),
|
hid_boot_event_get_data(event),
|
||||||
hid_boot_event_get_size(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_VALID);
|
||||||
@@ -174,21 +213,51 @@ static bool handle_hid_report_request_event(const struct hid_report_event *event
|
|||||||
size_t len = hid_report_event_get_size(event);
|
size_t len = hid_report_event_get_size(event);
|
||||||
|
|
||||||
if ((len > 0U) && (data[0] == REPORT_ID_KEYBOARD)) {
|
if ((len > 0U) && (data[0] == REPORT_ID_KEYBOARD)) {
|
||||||
(void)hid_tx_item_store(&tx.nkro_state, HID_TX_KIND_REPORT, data, len);
|
(void)hid_tx_item_store(&tx.keyboard_state,
|
||||||
atomic_set_bit(&tx.flags, HID_TX_FLAG_NKRO_VALID);
|
HID_TX_KIND_REPORT,
|
||||||
atomic_set_bit(&tx.flags, HID_TX_FLAG_NKRO_DIRTY);
|
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)) {
|
} else if ((len > 0U) && (data[0] == REPORT_ID_VENDOR)) {
|
||||||
(void)hid_tx_item_store(&tx.vendor_state, HID_TX_KIND_REPORT, data, len);
|
(void)hid_tx_item_store(&tx.vendor_state,
|
||||||
|
HID_TX_KIND_REPORT,
|
||||||
|
HID_TX_ROUTE_AUTO,
|
||||||
|
data, len);
|
||||||
atomic_set_bit(&tx.flags, HID_TX_FLAG_VENDOR_VALID);
|
atomic_set_bit(&tx.flags, HID_TX_FLAG_VENDOR_VALID);
|
||||||
atomic_set_bit(&tx.flags, HID_TX_FLAG_VENDOR_DIRTY);
|
atomic_set_bit(&tx.flags, HID_TX_FLAG_VENDOR_DIRTY);
|
||||||
} else {
|
} else {
|
||||||
(void)hid_tx_queue_push(HID_TX_KIND_REPORT, data, len);
|
(void)hid_tx_queue_push(&hid_tx_misc_msgq,
|
||||||
|
HID_TX_KIND_REPORT,
|
||||||
|
HID_TX_ROUTE_AUTO,
|
||||||
|
data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch_next_if_possible();
|
dispatch_next_if_possible();
|
||||||
return true;
|
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)
|
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)) {
|
if (!atomic_test_bit(&tx.flags, HID_TX_FLAG_IN_FLIGHT)) {
|
||||||
@@ -227,6 +296,10 @@ static bool app_event_handler(const struct app_event_header *aeh)
|
|||||||
return handle_hid_tx_done_event(cast_hid_tx_done_event(aeh));
|
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);
|
__ASSERT_NO_MSG(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -236,4 +309,5 @@ APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
|
|||||||
APP_EVENT_SUBSCRIBE(MODULE, mode_event);
|
APP_EVENT_SUBSCRIBE(MODULE, mode_event);
|
||||||
APP_EVENT_SUBSCRIBE_EARLY(MODULE, hid_boot_event);
|
APP_EVENT_SUBSCRIBE_EARLY(MODULE, hid_boot_event);
|
||||||
APP_EVENT_SUBSCRIBE_EARLY(MODULE, hid_report_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);
|
APP_EVENT_SUBSCRIBE(MODULE, hid_tx_done_event);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#include <zephyr/kernel.h>
|
#include <zephyr/kernel.h>
|
||||||
#include <zephyr/device.h>
|
#include <zephyr/device.h>
|
||||||
#include <zephyr/drivers/adc.h>
|
#include <zephyr/drivers/sensor.h>
|
||||||
#include <zephyr/drivers/gpio.h>
|
|
||||||
#include <zephyr/sys/atomic.h>
|
#include <zephyr/sys/atomic.h>
|
||||||
#include <zephyr/sys/util.h>
|
#include <zephyr/sys/util.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -18,35 +17,25 @@
|
|||||||
#include <zephyr/logging/log.h>
|
#include <zephyr/logging/log.h>
|
||||||
LOG_MODULE_REGISTER(MODULE);
|
LOG_MODULE_REGISTER(MODULE);
|
||||||
|
|
||||||
#define MODE_USER_NODE DT_PATH(zephyr_user)
|
#define MODE_SENSE_NODE DT_NODELABEL(mode_sense)
|
||||||
#define MODE_ADC_IO_CH_IDX 0
|
|
||||||
#define MODE_SAMPLE_INTERVAL_MS 50
|
#define MODE_SAMPLE_INTERVAL_MS 50
|
||||||
|
|
||||||
static const struct adc_dt_spec mode_adc =
|
static const struct device *const mode_sensor_dev = DEVICE_DT_GET(MODE_SENSE_NODE);
|
||||||
ADC_DT_SPEC_GET_BY_IDX(MODE_USER_NODE, MODE_ADC_IO_CH_IDX);
|
|
||||||
|
|
||||||
static struct k_work_delayable mode_sample_work;
|
static struct k_work_delayable mode_sample_work;
|
||||||
static int16_t adc_sample_buffer;
|
|
||||||
|
|
||||||
static atomic_t active;
|
static atomic_t active;
|
||||||
static mode_type_t current_mode;
|
static mode_type_t current_mode;
|
||||||
static uint8_t mode_stable_status;
|
static uint8_t mode_stable_status;
|
||||||
|
|
||||||
static int init_adc(void)
|
static int init_mode_sensor(void)
|
||||||
{
|
{
|
||||||
if (!adc_is_ready_dt(&mode_adc))
|
if (!device_is_ready(mode_sensor_dev))
|
||||||
{
|
{
|
||||||
LOG_ERR("ADC device not ready");
|
LOG_ERR("Mode sense device not ready");
|
||||||
return -ENODEV;
|
return -ENODEV;
|
||||||
}
|
}
|
||||||
|
|
||||||
int err = adc_channel_setup_dt(&mode_adc);
|
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
LOG_ERR("ADC channel setup failed (err=%d)", err);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,29 +70,22 @@ static mode_type_t classify_mode_from_mv(int32_t mv)
|
|||||||
|
|
||||||
static int read_mode(mode_type_t *mode)
|
static int read_mode(mode_type_t *mode)
|
||||||
{
|
{
|
||||||
struct adc_sequence sequence = {0};
|
struct sensor_value value;
|
||||||
int err = adc_sequence_init_dt(&mode_adc, &sequence);
|
int err = sensor_sample_fetch(mode_sensor_dev);
|
||||||
if (err)
|
if (err)
|
||||||
{
|
{
|
||||||
|
LOG_WRN("sensor_sample_fetch(mode) failed (err=%d)", err);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
sequence.buffer = &adc_sample_buffer;
|
err = sensor_channel_get(mode_sensor_dev, SENSOR_CHAN_VOLTAGE, &value);
|
||||||
sequence.buffer_size = sizeof(adc_sample_buffer);
|
|
||||||
|
|
||||||
err = adc_read_dt(&mode_adc, &sequence);
|
|
||||||
if (err)
|
|
||||||
{
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32_t v = adc_sample_buffer;
|
|
||||||
err = adc_raw_to_millivolts_dt(&mode_adc, &v);
|
|
||||||
if (err)
|
if (err)
|
||||||
{
|
{
|
||||||
|
LOG_WRN("sensor_channel_get(mode) failed (err=%d)", err);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int32_t v = (int32_t)sensor_value_to_milli(&value);
|
||||||
*mode = classify_mode_from_mv(v);
|
*mode = classify_mode_from_mv(v);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@@ -187,7 +169,7 @@ static void init_mode_switch(void)
|
|||||||
if (atomic_get(&active))
|
if (atomic_get(&active))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (init_adc())
|
if (init_mode_sensor())
|
||||||
{
|
{
|
||||||
module_set_state(MODULE_STATE_ERROR);
|
module_set_state(MODULE_STATE_ERROR);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
#include <zephyr/device.h>
|
#include <zephyr/device.h>
|
||||||
#include <zephyr/drivers/sensor.h>
|
#include <zephyr/drivers/sensor.h>
|
||||||
#include <zephyr/kernel.h>
|
#include <zephyr/kernel.h>
|
||||||
|
#include <zephyr/pm/device.h>
|
||||||
|
#include <zephyr/sys/atomic.h>
|
||||||
|
|
||||||
#include <app_event_manager.h>
|
#include <app_event_manager.h>
|
||||||
|
#include <caf/events/power_event.h>
|
||||||
|
|
||||||
#define MODULE qdec
|
#define MODULE qdec
|
||||||
#include <caf/events/module_state_event.h>
|
#include <caf/events/module_state_event.h>
|
||||||
@@ -24,6 +27,7 @@ struct qdec_ctx {
|
|||||||
const struct device *dev;
|
const struct device *dev;
|
||||||
struct sensor_trigger trigger;
|
struct sensor_trigger trigger;
|
||||||
struct k_work_delayable emit_work;
|
struct k_work_delayable emit_work;
|
||||||
|
atomic_t active;
|
||||||
int32_t acc_deg;
|
int32_t acc_deg;
|
||||||
bool emit_scheduled;
|
bool emit_scheduled;
|
||||||
};
|
};
|
||||||
@@ -36,8 +40,31 @@ static struct qdec_ctx qdec = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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)
|
static void schedule_emit_work(void)
|
||||||
{
|
{
|
||||||
|
if (!atomic_get(&qdec.active)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (qdec.emit_scheduled) {
|
if (qdec.emit_scheduled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -52,6 +79,11 @@ static void qdec_emit_work_handler(struct k_work *work)
|
|||||||
|
|
||||||
ARG_UNUSED(work);
|
ARG_UNUSED(work);
|
||||||
|
|
||||||
|
if (!atomic_get(&qdec.active)) {
|
||||||
|
qdec.emit_scheduled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
qdec.emit_scheduled = false;
|
qdec.emit_scheduled = false;
|
||||||
|
|
||||||
if ((qdec.acc_deg < QDEC_DEG_PER_STEP_EVENT) &&
|
if ((qdec.acc_deg < QDEC_DEG_PER_STEP_EVENT) &&
|
||||||
@@ -83,6 +115,10 @@ static void qdec_data_handler(const struct device *dev,
|
|||||||
|
|
||||||
ARG_UNUSED(trigger);
|
ARG_UNUSED(trigger);
|
||||||
|
|
||||||
|
if (!atomic_get(&qdec.active)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
err = sensor_sample_fetch_chan(dev, SENSOR_CHAN_ROTATION);
|
err = sensor_sample_fetch_chan(dev, SENSOR_CHAN_ROTATION);
|
||||||
if (err) {
|
if (err) {
|
||||||
LOG_ERR("QDEC sample fetch failed: %d", err);
|
LOG_ERR("QDEC sample fetch failed: %d", err);
|
||||||
@@ -113,8 +149,8 @@ static int qdec_init(void)
|
|||||||
return -ENODEV;
|
return -ENODEV;
|
||||||
}
|
}
|
||||||
|
|
||||||
qdec.acc_deg = 0;
|
qdec_reset_state();
|
||||||
qdec.emit_scheduled = false;
|
atomic_set(&qdec.active, false);
|
||||||
k_work_init_delayable(&qdec.emit_work, qdec_emit_work_handler);
|
k_work_init_delayable(&qdec.emit_work, qdec_emit_work_handler);
|
||||||
|
|
||||||
err = sensor_trigger_set(qdec.dev, &qdec.trigger, qdec_data_handler);
|
err = sensor_trigger_set(qdec.dev, &qdec.trigger, qdec_data_handler);
|
||||||
@@ -129,6 +165,46 @@ static int qdec_init(void)
|
|||||||
return 0;
|
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)
|
static bool app_event_handler(const struct app_event_header *aeh)
|
||||||
{
|
{
|
||||||
if (is_module_state_event(aeh)) {
|
if (is_module_state_event(aeh)) {
|
||||||
@@ -140,16 +216,28 @@ static bool app_event_handler(const struct app_event_header *aeh)
|
|||||||
if (err) {
|
if (err) {
|
||||||
module_set_state(MODULE_STATE_ERROR);
|
module_set_state(MODULE_STATE_ERROR);
|
||||||
} else {
|
} else {
|
||||||
module_set_state(MODULE_STATE_READY);
|
qdec_module_resume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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);
|
__ASSERT_NO_MSG(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
APP_EVENT_LISTENER(MODULE, app_event_handler);
|
APP_EVENT_LISTENER(MODULE, app_event_handler);
|
||||||
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
|
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
|
||||||
|
APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event);
|
||||||
|
APP_EVENT_SUBSCRIBE(MODULE, wake_up_event);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
#include <zephyr/logging/log.h>
|
#include <zephyr/logging/log.h>
|
||||||
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
|
LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF);
|
||||||
|
|
||||||
#define TIME_MANAGER_SAVE_DELAY_MS 1000
|
#define TIME_MANAGER_SAVE_DELAY K_HOURS(24)
|
||||||
#define TIME_MANAGER_STORAGE_KEY "state"
|
#define TIME_MANAGER_STORAGE_KEY "state"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -58,6 +58,7 @@ static bool time_manager_source_is_valid(enum time_sync_source source)
|
|||||||
case TIME_SYNC_SOURCE_BLE:
|
case TIME_SYNC_SOURCE_BLE:
|
||||||
case TIME_SYNC_SOURCE_USB:
|
case TIME_SYNC_SOURCE_USB:
|
||||||
case TIME_SYNC_SOURCE_MANUAL:
|
case TIME_SYNC_SOURCE_MANUAL:
|
||||||
|
case TIME_SYNC_SOURCE_HID:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
@@ -72,8 +73,8 @@ static bool time_manager_timezone_is_valid(int16_t timezone_min)
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* 保存工作在系统工作队列里执行:
|
* 保存工作在系统工作队列里执行:
|
||||||
* - 这样 GATT 写回调和事件派发路径都只做内存更新;
|
* - 这样同步入口只做内存更新;
|
||||||
* - 真正可能触发 flash 擦写的 settings_save_one 被挪到异步上下文。
|
* - flash 写入被节流到 24 小时窗口,降低频繁校时带来的磨损。
|
||||||
*/
|
*/
|
||||||
static int time_manager_store_state(const struct time_manager_storage_data *storage)
|
static int time_manager_store_state(const struct time_manager_storage_data *storage)
|
||||||
{
|
{
|
||||||
@@ -144,11 +145,13 @@ static void time_manager_apply_update(const struct time_sync_update *update)
|
|||||||
k_spin_unlock(&time_ctx.lock, key);
|
k_spin_unlock(&time_ctx.lock, key);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 保存延迟 1 秒:
|
* 时间同步允许立即生效,但 flash 落盘不要求同步完成后立刻发生。
|
||||||
* - 避免未来 BLE/USB 连续校时时反复触发 flash 写入;
|
* 这里将写入节流到 24 小时窗口:只要当前还没有待执行的保存工作,
|
||||||
* - 也避免在同步入口上下文里直接阻塞等待存储完成。
|
* 就挂一个延迟保存;后续新的校时只更新内存快照,不反复重置定时器。
|
||||||
*/
|
*/
|
||||||
k_work_reschedule(&time_ctx.save_work, K_MSEC(TIME_MANAGER_SAVE_DELAY_MS));
|
if (!k_work_delayable_is_pending(&time_ctx.save_work)) {
|
||||||
|
k_work_schedule(&time_ctx.save_work, TIME_MANAGER_SAVE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INF("Time synchronized src=%u tz=%d utc_ms=%llu acc=%u",
|
LOG_INF("Time synchronized src=%u tz=%d utc_ms=%llu acc=%u",
|
||||||
update->source,
|
update->source,
|
||||||
@@ -269,37 +272,6 @@ static bool handle_time_sync_event(const struct time_sync_event *event)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* 掉电前尽量把最后一次同步结果落盘:
|
|
||||||
* - 如果没有脏数据,立即返回;
|
|
||||||
* - 如果有待保存数据,则同步执行一次保存,降低突然掉电时的丢失概率。
|
|
||||||
*/
|
|
||||||
static bool handle_power_down_event(void)
|
|
||||||
{
|
|
||||||
struct time_manager_storage_data storage;
|
|
||||||
bool should_store;
|
|
||||||
k_spinlock_key_t key;
|
|
||||||
|
|
||||||
if (!time_manager_is_ready()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
(void)k_work_cancel_delayable(&time_ctx.save_work);
|
|
||||||
|
|
||||||
key = k_spin_lock(&time_ctx.lock);
|
|
||||||
should_store = time_ctx.storage_dirty && time_ctx.has_persisted_time;
|
|
||||||
storage = time_ctx.persisted;
|
|
||||||
time_ctx.storage_dirty = false;
|
|
||||||
k_spin_unlock(&time_ctx.lock, key);
|
|
||||||
|
|
||||||
if (!should_store) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
(void)time_manager_store_state(&storage);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 仅在 settings_loader 完成后宣布 READY,保证外部读取到的是稳定状态。 */
|
/* 仅在 settings_loader 完成后宣布 READY,保证外部读取到的是稳定状态。 */
|
||||||
static bool handle_module_state_event(const struct module_state_event *event)
|
static bool handle_module_state_event(const struct module_state_event *event)
|
||||||
{
|
{
|
||||||
@@ -393,10 +365,6 @@ static bool app_event_handler(const struct app_event_header *aeh)
|
|||||||
return handle_time_sync_event(cast_time_sync_event(aeh));
|
return handle_time_sync_event(cast_time_sync_event(aeh));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_power_down_event(aeh)) {
|
|
||||||
return handle_power_down_event();
|
|
||||||
}
|
|
||||||
|
|
||||||
__ASSERT_NO_MSG(false);
|
__ASSERT_NO_MSG(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -404,4 +372,3 @@ static bool app_event_handler(const struct app_event_header *aeh)
|
|||||||
APP_EVENT_LISTENER(MODULE, app_event_handler);
|
APP_EVENT_LISTENER(MODULE, app_event_handler);
|
||||||
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
|
APP_EVENT_SUBSCRIBE(MODULE, module_state_event);
|
||||||
APP_EVENT_SUBSCRIBE(MODULE, time_sync_event);
|
APP_EVENT_SUBSCRIBE(MODULE, time_sync_event);
|
||||||
APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event);
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include "hid_report_descriptor.h"
|
#include "hid_report_descriptor.h"
|
||||||
#include "hid_boot_event.h"
|
#include "hid_boot_event.h"
|
||||||
|
#include "hid_host_command_event.h"
|
||||||
#include "hid_protocol_event.h"
|
#include "hid_protocol_event.h"
|
||||||
#include "hid_tx_done_event.h"
|
#include "hid_tx_done_event.h"
|
||||||
#include "hid_tx_event.h"
|
#include "hid_tx_event.h"
|
||||||
@@ -106,6 +107,19 @@ static bool usb_hid_should_be_active(void)
|
|||||||
return g_usb_hid.policy.usb_mode_selected && !g_usb_hid.policy.pm_suspended;
|
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)
|
static struct usb_hid_iface *usb_hid_iface_from_dev(const struct device *dev)
|
||||||
{
|
{
|
||||||
if (dev == g_usb_hid.boot.dev)
|
if (dev == g_usb_hid.boot.dev)
|
||||||
@@ -205,6 +219,35 @@ static bool try_extract_vendor_mask(const struct device *dev,
|
|||||||
return true;
|
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,
|
static int hid_stub_get_report(const struct device *dev,
|
||||||
uint8_t type, uint8_t id,
|
uint8_t type, uint8_t id,
|
||||||
uint16_t len, uint8_t *buf)
|
uint16_t len, uint8_t *buf)
|
||||||
@@ -226,6 +269,9 @@ static int hid_stub_set_report(const struct device *dev,
|
|||||||
|
|
||||||
const uint8_t *mask_data;
|
const uint8_t *mask_data;
|
||||||
size_t mask_len;
|
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))
|
if (try_extract_vendor_mask(dev, len, buf, &mask_data, &mask_len))
|
||||||
{
|
{
|
||||||
@@ -234,6 +280,13 @@ static int hid_stub_set_report(const struct device *dev,
|
|||||||
return 0;
|
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))
|
if (!should_handle_led_input_from_dev(dev))
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
@@ -313,18 +366,30 @@ static void hid_stub_input_done(const struct device *dev, const uint8_t *report)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void hid_stub_output_report(const struct device *dev, uint16_t len, const uint8_t *buf)
|
static void hid_stub_output_report(const struct device *dev, uint16_t len, const uint8_t *buf)
|
||||||
{
|
|
||||||
if (!should_handle_led_input_from_dev(dev))
|
|
||||||
{
|
{
|
||||||
const uint8_t *mask_data;
|
const uint8_t *mask_data;
|
||||||
size_t mask_len;
|
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))
|
if (try_extract_vendor_mask(dev, len, buf, &mask_data, &mask_len))
|
||||||
{
|
{
|
||||||
LOG_INF("hid_stub_output_report vendor mask len=%u", mask_len);
|
LOG_INF("hid_stub_output_report vendor mask len=%u", mask_len);
|
||||||
hid_vendor_mask_event_submit(mask_data, 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,7 +776,7 @@ static bool handle_wake_up_event(void)
|
|||||||
|
|
||||||
static bool handle_hid_tx_event(const struct hid_tx_event *event)
|
static bool handle_hid_tx_event(const struct hid_tx_event *event)
|
||||||
{
|
{
|
||||||
if (!g_usb_hid.policy.usb_mode_selected)
|
if (!usb_hid_should_handle_tx_event(event))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -793,7 +858,8 @@ static bool handle_hid_tx_event(const struct hid_tx_event *event)
|
|||||||
|
|
||||||
if ((report_id != REPORT_ID_KEYBOARD) &&
|
if ((report_id != REPORT_ID_KEYBOARD) &&
|
||||||
(report_id != REPORT_ID_CONSUMER) &&
|
(report_id != REPORT_ID_CONSUMER) &&
|
||||||
(report_id != REPORT_ID_VENDOR))
|
(report_id != REPORT_ID_VENDOR) &&
|
||||||
|
(report_id != REPORT_ID_VENDOR_CMD))
|
||||||
{
|
{
|
||||||
submit_usb_tx_done(HID_TX_KIND_REPORT, false);
|
submit_usb_tx_done(HID_TX_KIND_REPORT, false);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
306
src/ui/display_ui.c
Normal file
306
src/ui/display_ui.c
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
#include <zephyr/sys/printk.h>
|
||||||
|
|
||||||
|
#include "battery_status_event.h"
|
||||||
|
#include "display_ui.h"
|
||||||
|
#include "keyboard_led_event.h"
|
||||||
|
|
||||||
|
#define DISPLAY_SYMBOL_PLUG "\xEF\x87\xA6"
|
||||||
|
|
||||||
|
LV_FONT_DECLARE(ui_font_keyboard_small_18);
|
||||||
|
LV_FONT_DECLARE(ui_font_keyboard_time_48);
|
||||||
|
|
||||||
|
enum display_status_id
|
||||||
|
{
|
||||||
|
DISPLAY_STATUS_USB = 0,
|
||||||
|
DISPLAY_STATUS_BLE,
|
||||||
|
DISPLAY_STATUS_NUMLOCK,
|
||||||
|
DISPLAY_STATUS_CAPSLOCK,
|
||||||
|
DISPLAY_STATUS_COUNT,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct display_ui_ctx
|
||||||
|
{
|
||||||
|
lv_obj_t *status_badges[DISPLAY_STATUS_COUNT];
|
||||||
|
lv_obj_t *status_labels[DISPLAY_STATUS_COUNT];
|
||||||
|
lv_obj_t *battery_icon;
|
||||||
|
lv_obj_t *battery_label;
|
||||||
|
lv_obj_t *battery_state_label;
|
||||||
|
lv_obj_t *date_label;
|
||||||
|
lv_obj_t *time_label;
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct display_ui_ctx g_display_ui;
|
||||||
|
|
||||||
|
static const char *const g_status_texts[DISPLAY_STATUS_COUNT] = {
|
||||||
|
LV_SYMBOL_USB,
|
||||||
|
LV_SYMBOL_BLUETOOTH,
|
||||||
|
"1",
|
||||||
|
"A",
|
||||||
|
};
|
||||||
|
|
||||||
|
static lv_color_t display_ui_get_battery_color(uint8_t battery_level)
|
||||||
|
{
|
||||||
|
if (battery_level > 70U) {
|
||||||
|
return lv_color_hex(0x8BD450);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (battery_level >= 20U) {
|
||||||
|
return lv_color_hex(0xF4D35E);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lv_color_hex(0xE63946);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char *display_ui_get_battery_symbol(uint8_t battery_level)
|
||||||
|
{
|
||||||
|
if (battery_level > 85U) {
|
||||||
|
return LV_SYMBOL_BATTERY_FULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (battery_level > 60U) {
|
||||||
|
return LV_SYMBOL_BATTERY_3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (battery_level > 35U) {
|
||||||
|
return LV_SYMBOL_BATTERY_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (battery_level >= 20U) {
|
||||||
|
return LV_SYMBOL_BATTERY_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LV_SYMBOL_BATTERY_EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool display_ui_status_is_active(enum display_status_id id,
|
||||||
|
const struct display_ui_model *model)
|
||||||
|
{
|
||||||
|
switch (id) {
|
||||||
|
case DISPLAY_STATUS_USB:
|
||||||
|
return model->mode == MODE_TYPE_USB;
|
||||||
|
|
||||||
|
case DISPLAY_STATUS_BLE:
|
||||||
|
return model->mode == MODE_TYPE_BLE;
|
||||||
|
|
||||||
|
case DISPLAY_STATUS_NUMLOCK:
|
||||||
|
return (model->led_mask & KEYBOARD_LED_MASK_NUM_LOCK) != 0U;
|
||||||
|
|
||||||
|
case DISPLAY_STATUS_CAPSLOCK:
|
||||||
|
return (model->led_mask & KEYBOARD_LED_MASK_CAPS_LOCK) != 0U;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void display_ui_create_status_chip(lv_obj_t *parent, enum display_status_id id)
|
||||||
|
{
|
||||||
|
lv_obj_t *badge = lv_obj_create(parent);
|
||||||
|
lv_obj_t *label;
|
||||||
|
|
||||||
|
lv_obj_remove_style_all(badge);
|
||||||
|
lv_obj_set_size(badge, 50, 32);
|
||||||
|
lv_obj_set_style_radius(badge, 10, 0);
|
||||||
|
lv_obj_set_style_bg_opa(badge, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_pad_all(badge, 0, 0);
|
||||||
|
|
||||||
|
label = lv_label_create(badge);
|
||||||
|
lv_label_set_text(label, g_status_texts[id]);
|
||||||
|
lv_obj_set_width(label, LV_PCT(100));
|
||||||
|
lv_obj_set_style_text_font(label, &ui_font_keyboard_small_18, 0);
|
||||||
|
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
|
||||||
|
lv_obj_center(label);
|
||||||
|
|
||||||
|
g_display_ui.status_badges[id] = badge;
|
||||||
|
g_display_ui.status_labels[id] = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
void display_ui_refresh_status_bar(const struct display_ui_model *model)
|
||||||
|
{
|
||||||
|
for (uint32_t i = 0; i < DISPLAY_STATUS_COUNT; i++) {
|
||||||
|
lv_obj_t *badge = g_display_ui.status_badges[i];
|
||||||
|
lv_obj_t *label = g_display_ui.status_labels[i];
|
||||||
|
bool active = display_ui_status_is_active((enum display_status_id)i, model);
|
||||||
|
|
||||||
|
if (!badge || !label) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_obj_set_style_border_width(badge, 4, 0);
|
||||||
|
lv_obj_set_style_border_color(badge,
|
||||||
|
active ? model->theme_color :
|
||||||
|
model->inactive_border_color,
|
||||||
|
0);
|
||||||
|
lv_obj_set_style_bg_color(badge,
|
||||||
|
active ? lv_color_hex(0x1D2735) :
|
||||||
|
lv_color_hex(0x161A20),
|
||||||
|
0);
|
||||||
|
lv_obj_set_style_text_color(label,
|
||||||
|
active ? lv_color_white() :
|
||||||
|
lv_color_hex(0x7C8798),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void display_ui_refresh_battery(const struct display_ui_model *model)
|
||||||
|
{
|
||||||
|
char battery_text[8];
|
||||||
|
lv_color_t battery_color;
|
||||||
|
const char *state_symbol = "";
|
||||||
|
lv_color_t state_color = lv_color_white();
|
||||||
|
|
||||||
|
if (!g_display_ui.battery_icon ||
|
||||||
|
!g_display_ui.battery_label ||
|
||||||
|
!g_display_ui.battery_state_label) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
battery_color = display_ui_get_battery_color(model->battery_level);
|
||||||
|
snprintk(battery_text, sizeof(battery_text), "%u%%", model->battery_level);
|
||||||
|
|
||||||
|
if ((model->battery_flags & BATTERY_STATUS_FLAG_FULL) != 0U) {
|
||||||
|
state_symbol = DISPLAY_SYMBOL_PLUG;
|
||||||
|
state_color = lv_color_hex(0x4C9EF5);
|
||||||
|
} else if ((model->battery_flags & BATTERY_STATUS_FLAG_CHARGING) != 0U) {
|
||||||
|
state_symbol = LV_SYMBOL_CHARGE;
|
||||||
|
state_color = lv_color_hex(0xF4D35E);
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_label_set_text(g_display_ui.battery_icon,
|
||||||
|
display_ui_get_battery_symbol(model->battery_level));
|
||||||
|
lv_obj_set_style_text_color(g_display_ui.battery_icon, battery_color, 0);
|
||||||
|
lv_label_set_text(g_display_ui.battery_label, battery_text);
|
||||||
|
lv_label_set_text(g_display_ui.battery_state_label, state_symbol);
|
||||||
|
lv_obj_set_style_text_color(g_display_ui.battery_state_label, state_color, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void display_ui_refresh_datetime(const char *date_text, const char *time_text)
|
||||||
|
{
|
||||||
|
if (!g_display_ui.date_label || !g_display_ui.time_label) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_label_set_text(g_display_ui.date_label, date_text);
|
||||||
|
lv_label_set_text(g_display_ui.time_label, time_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void display_ui_refresh_all(const struct display_ui_model *model,
|
||||||
|
const char *date_text,
|
||||||
|
const char *time_text)
|
||||||
|
{
|
||||||
|
display_ui_refresh_status_bar(model);
|
||||||
|
display_ui_refresh_battery(model);
|
||||||
|
display_ui_refresh_datetime(date_text, time_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void display_ui_init(const struct display_ui_model *model,
|
||||||
|
const char *date_text,
|
||||||
|
const char *time_text)
|
||||||
|
{
|
||||||
|
lv_obj_t *screen = lv_screen_active();
|
||||||
|
lv_obj_t *content;
|
||||||
|
lv_obj_t *top_row;
|
||||||
|
lv_obj_t *battery_wrap;
|
||||||
|
lv_obj_t *middle_row;
|
||||||
|
lv_obj_t *bottom_row;
|
||||||
|
|
||||||
|
memset(&g_display_ui, 0, sizeof(g_display_ui));
|
||||||
|
|
||||||
|
lv_obj_clean(screen);
|
||||||
|
lv_obj_set_style_bg_color(screen, lv_color_hex(0x0F1115), 0);
|
||||||
|
lv_obj_set_style_bg_grad_color(screen, lv_color_hex(0x1A1F29), 0);
|
||||||
|
lv_obj_set_style_bg_grad_dir(screen, LV_GRAD_DIR_VER, 0);
|
||||||
|
lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_text_color(screen, lv_color_white(), 0);
|
||||||
|
lv_obj_set_style_pad_all(screen, 0, 0);
|
||||||
|
lv_obj_set_scrollbar_mode(screen, LV_SCROLLBAR_MODE_OFF);
|
||||||
|
|
||||||
|
content = lv_obj_create(screen);
|
||||||
|
lv_obj_remove_style_all(content);
|
||||||
|
lv_obj_set_size(content, LV_PCT(100), LV_PCT(100));
|
||||||
|
lv_obj_set_style_bg_color(content, lv_color_hex(0x0F1115), 0);
|
||||||
|
lv_obj_set_style_bg_opa(content, LV_OPA_TRANSP, 0);
|
||||||
|
lv_obj_set_style_pad_left(content, 14, 0);
|
||||||
|
lv_obj_set_style_pad_right(content, 14, 0);
|
||||||
|
lv_obj_set_style_pad_top(content, 8, 0);
|
||||||
|
lv_obj_set_style_pad_bottom(content, 8, 0);
|
||||||
|
lv_obj_set_layout(content, LV_LAYOUT_FLEX);
|
||||||
|
lv_obj_set_flex_flow(content, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_flex_align(content,
|
||||||
|
LV_FLEX_ALIGN_START,
|
||||||
|
LV_FLEX_ALIGN_CENTER,
|
||||||
|
LV_FLEX_ALIGN_CENTER);
|
||||||
|
|
||||||
|
top_row = lv_obj_create(content);
|
||||||
|
lv_obj_remove_style_all(top_row);
|
||||||
|
lv_obj_set_width(top_row, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(top_row, 1);
|
||||||
|
lv_obj_set_style_bg_color(top_row, lv_color_hex(0x0F1115), 0);
|
||||||
|
lv_obj_set_style_bg_opa(top_row, LV_OPA_TRANSP, 0);
|
||||||
|
lv_obj_set_layout(top_row, LV_LAYOUT_FLEX);
|
||||||
|
lv_obj_set_flex_flow(top_row, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(top_row,
|
||||||
|
LV_FLEX_ALIGN_SPACE_BETWEEN,
|
||||||
|
LV_FLEX_ALIGN_CENTER,
|
||||||
|
LV_FLEX_ALIGN_CENTER);
|
||||||
|
|
||||||
|
g_display_ui.date_label = lv_label_create(top_row);
|
||||||
|
lv_obj_set_style_text_font(g_display_ui.date_label, &ui_font_keyboard_small_18, 0);
|
||||||
|
lv_obj_set_style_text_color(g_display_ui.date_label, lv_color_hex(0xD8DEE9), 0);
|
||||||
|
|
||||||
|
battery_wrap = lv_obj_create(top_row);
|
||||||
|
lv_obj_remove_style_all(battery_wrap);
|
||||||
|
lv_obj_set_width(battery_wrap, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_layout(battery_wrap, LV_LAYOUT_FLEX);
|
||||||
|
lv_obj_set_flex_flow(battery_wrap, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(battery_wrap,
|
||||||
|
LV_FLEX_ALIGN_CENTER,
|
||||||
|
LV_FLEX_ALIGN_CENTER,
|
||||||
|
LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_column(battery_wrap, 4, 0);
|
||||||
|
|
||||||
|
g_display_ui.battery_icon = lv_label_create(battery_wrap);
|
||||||
|
lv_obj_set_style_text_font(g_display_ui.battery_icon, &ui_font_keyboard_small_18, 0);
|
||||||
|
|
||||||
|
g_display_ui.battery_label = lv_label_create(battery_wrap);
|
||||||
|
lv_obj_set_style_text_font(g_display_ui.battery_label, &ui_font_keyboard_small_18, 0);
|
||||||
|
lv_obj_set_style_text_color(g_display_ui.battery_label, lv_color_hex(0xD8DEE9), 0);
|
||||||
|
|
||||||
|
g_display_ui.battery_state_label = lv_label_create(battery_wrap);
|
||||||
|
lv_obj_set_style_text_font(g_display_ui.battery_state_label, &ui_font_keyboard_small_18, 0);
|
||||||
|
|
||||||
|
middle_row = lv_obj_create(content);
|
||||||
|
lv_obj_remove_style_all(middle_row);
|
||||||
|
lv_obj_set_width(middle_row, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(middle_row, 2);
|
||||||
|
lv_obj_set_style_bg_color(middle_row, lv_color_hex(0x0F1115), 0);
|
||||||
|
lv_obj_set_style_bg_opa(middle_row, LV_OPA_TRANSP, 0);
|
||||||
|
|
||||||
|
g_display_ui.time_label = lv_label_create(middle_row);
|
||||||
|
lv_obj_set_style_text_font(g_display_ui.time_label, &ui_font_keyboard_time_48, 0);
|
||||||
|
lv_obj_set_style_text_color(g_display_ui.time_label, lv_color_white(), 0);
|
||||||
|
lv_obj_center(g_display_ui.time_label);
|
||||||
|
|
||||||
|
bottom_row = lv_obj_create(content);
|
||||||
|
lv_obj_remove_style_all(bottom_row);
|
||||||
|
lv_obj_set_width(bottom_row, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(bottom_row, 1);
|
||||||
|
lv_obj_set_style_bg_color(bottom_row, lv_color_hex(0x0F1115), 0);
|
||||||
|
lv_obj_set_style_bg_opa(bottom_row, LV_OPA_TRANSP, 0);
|
||||||
|
lv_obj_set_layout(bottom_row, LV_LAYOUT_FLEX);
|
||||||
|
lv_obj_set_flex_flow(bottom_row, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(bottom_row,
|
||||||
|
LV_FLEX_ALIGN_CENTER,
|
||||||
|
LV_FLEX_ALIGN_CENTER,
|
||||||
|
LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_column(bottom_row, 6, 0);
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < DISPLAY_STATUS_COUNT; i++) {
|
||||||
|
display_ui_create_status_chip(bottom_row, (enum display_status_id)i);
|
||||||
|
}
|
||||||
|
|
||||||
|
display_ui_refresh_all(model, date_text, time_text);
|
||||||
|
}
|
||||||
34
src/ui/display_ui.h
Normal file
34
src/ui/display_ui.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#ifndef DISPLAY_UI_H
|
||||||
|
#define DISPLAY_UI_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include <lvgl.h>
|
||||||
|
|
||||||
|
#include "mode_event.h"
|
||||||
|
|
||||||
|
struct display_ui_model
|
||||||
|
{
|
||||||
|
lv_color_t theme_color;
|
||||||
|
lv_color_t inactive_border_color;
|
||||||
|
uint8_t battery_level;
|
||||||
|
mode_type_t mode;
|
||||||
|
uint8_t led_mask;
|
||||||
|
uint8_t battery_flags;
|
||||||
|
};
|
||||||
|
|
||||||
|
void display_ui_init(const struct display_ui_model *model,
|
||||||
|
const char *date_text,
|
||||||
|
const char *time_text);
|
||||||
|
|
||||||
|
void display_ui_refresh_all(const struct display_ui_model *model,
|
||||||
|
const char *date_text,
|
||||||
|
const char *time_text);
|
||||||
|
|
||||||
|
void display_ui_refresh_status_bar(const struct display_ui_model *model);
|
||||||
|
|
||||||
|
void display_ui_refresh_battery(const struct display_ui_model *model);
|
||||||
|
|
||||||
|
void display_ui_refresh_datetime(const char *date_text, const char *time_text);
|
||||||
|
|
||||||
|
#endif /* DISPLAY_UI_H */
|
||||||
@@ -1 +1,2 @@
|
|||||||
SB_CONFIG_BOOTLOADER_MCUBOOT=y
|
SB_CONFIG_BOOTLOADER_MCUBOOT=y
|
||||||
|
SB_CONFIG_MCUBOOT_MODE_SINGLE_APP=y
|
||||||
|
|||||||
25
sysbuild/mcuboot.conf
Normal file
25
sysbuild/mcuboot.conf
Normal file
@@ -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
|
||||||
23
sysbuild/mcuboot.overlay
Normal file
23
sysbuild/mcuboot.overlay
Normal file
@@ -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>;
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user