diff --git a/CMakeLists.txt b/CMakeLists.txt index 59e035a..8482dec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,7 @@ zephyr_nanopb_sources(app target_sources(app PRIVATE src/main.c src/battery_module.c + src/ble_bond_multi_module.c src/ble_adv_uuid16.c src/ble_bas_module.c src/ble_hid_module.c @@ -48,6 +49,7 @@ target_sources(app PRIVATE src/usb_hid_consumer_module.c src/usb_hid_keyboard_module.c src/events/bat_state_event.c + src/events/ble_bond_multi_event.c src/events/datetime_event.c src/events/encoder_event.c src/events/function_bitmap_state_event.c diff --git a/Kconfig b/Kconfig index 9e33318..e7454e1 100644 --- a/Kconfig +++ b/Kconfig @@ -2,6 +2,21 @@ mainmenu "blinky" source "Kconfig.zephyr" +menu "Application" + +config BLINKY_BLE_BOND_MULTI + bool "Blinky BLE multi-slot bond support" + depends on BT_BONDABLE + depends on BT_SETTINGS + depends on CAF_SETTINGS_LOADER + depends on CAF_BLE_COMMON_EVENTS + select CAF_BLE_BOND_SUPPORTED + default y + help + Enable the application-specific Bluetooth LE multi-slot bond module. + +endmenu + menu "Application Drivers" rsource "drivers/Kconfig" endmenu diff --git a/docs/ble_multi_slot_design.md b/docs/ble_multi_slot_design.md new file mode 100644 index 0000000..cade653 --- /dev/null +++ b/docs/ble_multi_slot_design.md @@ -0,0 +1,582 @@ +# Blinky 多槽蓝牙设计方案 + +## 1. 目标与约束 + +为 `C:\projects\blinky` 增加多槽蓝牙能力,当前阶段只实现 BLE,多 dongle 方案先预留结构,不实现完整业务流程。 + +本方案已经按当前讨论结论收敛为以下硬约束: + +- 支持 3 个 BLE 槽位 `Slot 1~3` +- 每个槽位固定对应一个 Bluetooth local identity +- 槽位切换本质上是切换 `bt_identity_id` +- 不引入 `app_slot_id` +- 不使用临时 identity +- 不做 erase advertising +- 不做擦除确认 +- 不做擦除回滚 +- 擦除当前槽位时直接删除该 identity 上的 bond +- settings 只持久化 `current_slot` 和各槽位 `meta` +- `slot_meta` 中必须保留 `display_name`,用于 UI 显示 +- `24G / dongle` 采用方案 A,但本阶段只预留专用槽位 + +## 2. 本地参考基线 + +本方案主要参考本地 NCS 3.2.3 中以下实现和文档: + +- `c:\ncs\v3.2.3\nrf\applications\nrf_desktop\doc\ble_bond.rst` +- `c:\ncs\v3.2.3\nrf\applications\nrf_desktop\src\modules\ble_bond.c` +- `c:\ncs\v3.2.3\nrf\applications\nrf_desktop\src\modules\Kconfig.ble_bond` +- `c:\ncs\v3.2.3\nrf\include\caf\events\ble_common_event.h` +- `c:\ncs\v3.2.3\nrf\subsys\caf\modules\ble_adv.c` +- `c:\ncs\v3.2.3\nrf\subsys\caf\modules\Kconfig.ble_state` +- `c:\ncs\v3.2.3\nrf\doc\nrf\libraries\caf\ble_state.rst` + +参考方式如下: + +- 继承 `nrf_desktop` 的总体方向:应用自定义 bond 管理模块,而不是 CAF 默认 `ble_bond` +- 继续通过 `ble_peer_operation_event` 驱动 CAF `ble_adv` +- 保留 dongle 专用 identity 的规划方式 + +明确不采用 `nrf_desktop` 的部分: + +- 不使用 `app_slot_id <-> bt_identity_id` 的二级映射 +- 不使用临时 identity +- 不采用 `ERASE_ADV` +- 不采用“新配对成功后再替换旧槽位”的回滚保护机制 + +## 3. 对现有项目的判断 + +`blinky` 当前已经具备多槽蓝牙需要的大部分基础能力: + +- 已启用 `CONFIG_CAF_BLE_STATE` +- 已启用 `CONFIG_CAF_BLE_ADV` +- 已启用 `CONFIG_BT_SETTINGS` +- HID 和 NUS 都已基于 `ble_peer_event` 跟踪当前连接 +- UI 已经预留 3 个蓝牙槽位和擦除当前槽位入口 +- 模式策略层已经区分: + - `MODE_SWITCH_BLE -> BLE_PROFILE_POLICY_GENERAL` + - `MODE_SWITCH_24G -> BLE_PROFILE_POLICY_DONGLE` + +相关文件: + +- `C:\projects\blinky\src\ble_hid_module.c` +- `C:\projects\blinky\src\ble_nus_module.c` +- `C:\projects\blinky\src\ui\ui_settings_ble.c` +- `C:\projects\blinky\src\ui\ui_settings_controller.c` +- `C:\projects\blinky\src\mode_policy_module.c` +- `C:\projects\blinky\prj.conf` + +当前限制也很明确: + +- `CONFIG_BT_MAX_PAIRED=1`,全设备只能保存 1 个 bond +- 当前是单槽逻辑 +- 还在使用 CAF 默认 `CONFIG_CAF_BLE_BOND=y` +- UI 的槽位状态仍是内存假数据 + +## 4. 配置语义结论 + +基于本地 Kconfig 和 CAF 文档,关键配置的含义如下: + +- `CONFIG_BT_MAX_PAIRED` + 说明:整个设备允许保存的 bond 总数 +- `CONFIG_CAF_BLE_STATE_MAX_LOCAL_ID_BONDS` + 说明:每个 Bluetooth local identity 允许的 bond 数上限 +- `CONFIG_BT_ID_MAX` + 说明:设备允许使用的 local identity 总数 + +如果目标是: + +- 3 个 BLE 槽位 +- 每槽 1 个 bond +- 预留 1 个 dongle 槽位 + +那么推荐配置应为: + +```conf +CONFIG_BT_MAX_CONN=1 +CONFIG_BT_MAX_PAIRED=4 +CONFIG_CAF_BLE_STATE_MAX_LOCAL_ID_BONDS=1 +CONFIG_BT_ID_MAX=5 +``` + +语义如下: + +- `BT_MAX_CONN=1` + 说明:同时只保持 1 条 BLE 连接 +- `BT_MAX_PAIRED=4` + 说明:全设备最多保存 4 个 bond +- `CAF_BLE_STATE_MAX_LOCAL_ID_BONDS=1` + 说明:每个 identity 只允许绑定 1 个主机 +- `BT_ID_MAX=5` + 说明:可用 identity 为 `0..4` + +配合本方案的 identity 规划: + +- `0`:默认 identity,不参与槽位 +- `1`:Slot 1 +- `2`:Slot 2 +- `3`:Slot 3 +- `4`:Dongle Slot 预留 + +## 5. 总体架构 + +## 5.1 设计结论 + +建议采用: + +- 保留 `CAF_BLE_STATE` +- 保留 `CAF_BLE_ADV` +- 关闭 CAF 默认 `BLE_BOND` +- 新增应用自定义模块 `ble_bond_multi_module` + +即: + +- `CONFIG_CAF_BLE_BOND=n` +- 新增 `CONFIG_BLINKY_BLE_BOND_MULTI` +- 自定义模块 `select CAF_BLE_BOND_SUPPORTED` +- 由自定义模块向 CAF 提交 `ble_peer_operation_event` + +原因: + +- CAF 默认 `ble_bond` 不适合固定多 identity 槽位管理 +- 你的需求已经明确不需要 `nrf_desktop` 那种复杂映射和回滚流程 +- 自定义模块更适合和现有 UI、模式策略、settings 同步 + +## 5.2 核心模型 + +本方案直接把“槽位”和“identity”绑定: + +- `Slot 1` = `bt_identity_id 1` +- `Slot 2` = `bt_identity_id 2` +- `Slot 3` = `bt_identity_id 3` +- `Dongle Slot` = `bt_identity_id 4` + +所以: + +- `current_slot` 实际上就是当前使用的 `bt_identity_id` +- UI 显示“Slot 1/2/3”时,后端直接映射到固定 identity +- 不再存在可变的 `id_lut` + +这个设计的优点: + +- 逻辑简单 +- settings 简单 +- 调试简单 +- 后续接入 dongle 不需要迁移已有 BLE 槽位 + +## 6. identity 规划 + +建议固定如下: + +| identity | 角色 | 当前阶段 | +| --- | --- | --- | +| `0` | 默认 identity | 不使用 | +| `1` | BLE Slot 1 | 使用 | +| `2` | BLE Slot 2 | 使用 | +| `3` | BLE Slot 3 | 使用 | +| `4` | Dongle Slot | 预留 | + +设计原则: + +- 不使用 `BT_ID_DEFAULT` + 原因:默认 identity 的 reset/unpair 行为不适合作为固定槽位基线 +- 3 个 BLE 槽位 identity 固定不变 +- `identity 4` 只给 `BLE_PROFILE_POLICY_DONGLE` + +## 7. 模块设计 + +## 7.1 新增模块和文件 + +建议新增: + +- `src/ble_bond_multi_module.c` +- `src/events/ble_bond_multi_event.c` +- `inc/events/ble_bond_multi_event.h` + +不建议新增: + +- `inc/ble_bond_multi.h` + +原因: + +- 当前设计是事件驱动模块 +- 没有必要向外暴露一组直接调用 API +- 共享内容更适合放在事件头文件里 + +只有在后续出现明确的跨模块直接调用需求时,才需要再补 `inc/ble_bond_multi.h` + +## 7.2 模块职责边界 + +### `ble_bond_multi_module` + +负责: + +- 管理当前活动 `bt_identity_id` +- 启动时恢复 `current_slot` +- 维护各 identity 槽位的 `slot_meta` +- 处理“切换当前槽位” +- 处理“直接擦除当前槽位” +- 监听连接、加密、断链事件,更新 `slot_meta` +- 向 CAF 提交 `ble_peer_operation_event` +- 向 UI / 显示层广播槽位状态 + +不负责: + +- 发 HID/NUS 数据 +- 改 GATT 服务 +- 绘制 UI + +### `ble_hid_module` / `ble_nus_module` + +保持现有思路,不理解“多槽逻辑”,只跟随当前活动连接: + +- 连接上谁,就服务谁 +- 切槽或擦除造成断链时,内部复位连接状态 + +### `mode_policy_module` + +负责决定当前是: + +- `BLE_PROFILE_POLICY_GENERAL` +- `BLE_PROFILE_POLICY_DONGLE` + +但不负责槽位 bond 细节。 + +## 8. 状态机设计 + +第一阶段建议极简化,只保留: + +- `DISABLED` +- `IDLE` +- `SWITCHING` +- `ERASING` + +明确不引入: + +- `SELECT_SLOT` +- `ERASE_PENDING` +- `ERASE_ADV` +- `TEMP_IDENTITY` +- `ROLLBACK` + +## 8.1 切槽流程 + +1. 用户在 UI 选择 `Slot N` +2. 模块判断: + - 如果 `N == current_slot`,直接返回 + - 否则进入 `SWITCHING` +3. 提交 `PEER_OPERATION_SELECTED` +4. CAF `ble_adv` 切换当前 advertising identity +5. 当前连接断开 +6. 设备以新 identity 重新广播 +7. 目标主机重新连接 +8. 保存新的 `current_slot` +9. 返回 `IDLE` + +这里采用“直接切换”,不做预选确认。 + +## 8.2 擦除当前槽位流程 + +1. 用户选择“Erase Bond” +2. 模块进入 `ERASING` +3. 对当前 `bt_identity_id` 执行: + - `bt_unpair(identity, BT_ADDR_LE_ANY)` +4. 清除该槽位 `slot_meta` +5. 重新以当前 identity 进入可配对广播 +6. 返回 `IDLE` + +这里采用产品级简化语义: + +- 不确认 +- 不保护旧配对 +- 不回滚 +- 擦除后旧主机立即失效 + +## 9. Settings 设计 + +## 9.1 持久化范围 + +只持久化: + +- `current_slot` +- 各槽位 `slot_meta` + +不持久化: + +- `id_lut` +- 临时 identity 状态 +- 擦除中间态 + +## 9.2 settings namespace + +建议采用: + +- `ble_multi/current_slot` +- `ble_multi/meta/1` +- `ble_multi/meta/2` +- `ble_multi/meta/3` +- `ble_multi/meta/4` + +其中: + +- `1/2/3` 是 BLE 槽位 +- `4` 是 dongle 预留槽位 + +## 9.3 slot_meta 结构 + +`slot_meta` 建议固定包含以下字段: + +- `occupied` +- `last_peer_addr` +- `display_name` + +字段说明: + +- `occupied` + 说明:该槽位当前是否有有效 bond +- `last_peer_addr` + 说明:最近一次绑定主机的 BLE 地址,用于识别和调试 +- `display_name` + 说明:UI 显示名,不做简化,作为正式字段保留 + +这里 `display_name` 是明确保留项,不是可选优化项。 +设计目的就是让 UI 能直接显示: + +- `MacBook Pro` +- `iPad Mini` +- `Windows-PC` + +而不是只能显示 `Bonded` / `Empty`。 + +## 9.4 display_name 来源 + +需要注意一点: + +- Zephyr bond 本身不会自动提供“友好主机名” + +因此 `display_name` 的来源需要在实现阶段明确,一般有两个方向: + +1. 配对后由应用层通过协议或自定义方式写入 +2. 若当前阶段无法拿到真实主机名,则先允许 UI/上位机为该槽位写入名称 + +也就是说: + +- `display_name` 必须作为正式持久化字段存在 +- 但其填充机制可以分阶段实现 + +第一阶段如果没有自动名称来源,建议默认值如下: + +- 有 bond 但无名称:`"Bonded Device"` +- 无 bond:空串或 `"Empty"` + +## 10. UI 设计 + +现有 UI 结构已经适合复用,不建议重做。 + +现有文件: + +- `C:\projects\blinky\src\ui\ui_settings_ble.c` +- `C:\projects\blinky\src\ui\ui_settings_controller.c` + +建议交互规则如下: + +- 进入 BLE 设置页后显示 `Slot 1~3` +- 每个槽位 value 直接显示 `display_name` +- 若该槽位无 bond,则显示 `Empty` +- 当前槽位显示为 `Slot N` +- 擦除项显示为 `Erase Slot N` + +操作方式: + +- 选中某个槽位后直接切换 +- 选中擦除项后直接擦除 +- 不加确认页 + +UI 需要的数据来源: + +- `current_slot` +- `slot_meta[1]` +- `slot_meta[2]` +- `slot_meta[3]` + +所以建议 UI 不再维护本地假数据,而改为订阅 `ble_bond_multi_event` 或读取统一状态缓存。 + +## 11. 与模式切换的兼容设计 + +## 11.1 BLE 档 + +`MODE_SWITCH_BLE` 下: + +- 只允许使用 `identity 1/2/3` +- UI 允许切槽和擦除 +- `current_slot` 必须属于 `1..3` + +## 11.2 24G / dongle 档 + +采用方案 A: + +- `MODE_SWITCH_24G` 绑定专用 `identity 4` +- `identity 4` 不与普通 BLE 槽位混用 +- 第一阶段只预留,不实现完整 dongle bond 生命周期 + +这样做的好处: + +- 保持 `mode_policy_module.c` 现有设计语义 +- 普通 BLE 槽位和 dongle 配对状态不会互相污染 +- 后续接入 dongle 时无需改动 BLE 三槽数据模型 + +## 12. 事件设计 + +建议新增应用事件: + +- `ble_bond_multi_event` + +放在: + +- `inc/events/ble_bond_multi_event.h` +- `src/events/ble_bond_multi_event.c` + +字段建议: + +- `current_slot` +- `active_identity_id` +- `slot_occupied_bitmap` +- `slot_meta_summary` +- `op` + +至少要能支持: + +- UI 刷新槽位名称 +- 显示层提示当前槽位 +- 设置层同步当前 active slot + +继续复用 CAF 事件: + +- `ble_peer_event` +- `ble_peer_operation_event` + +第一阶段不需要依赖: + +- `ble_peer_search_event` + +## 13. 对现有代码的影响评估 + +## 13.1 必改 + +- `prj.conf` +- `CMakeLists.txt` +- 新增 `src/ble_bond_multi_module.c` +- 新增 `inc/events/ble_bond_multi_event.h` +- 新增 `src/events/ble_bond_multi_event.c` +- `ui_settings_controller.c` + +## 13.2 小改 + +- `mode_policy_module.c` + 说明:把 `BLE_PROFILE_POLICY_GENERAL` 约束到 `identity 1/2/3`,把 `BLE_PROFILE_POLICY_DONGLE` 预留到 `identity 4` +- `display_module.c` + 说明:如果要显示当前槽位或名称,需要订阅新事件 + +## 13.3 原则上不应大改 + +- `ble_hid_module.c` +- `ble_nus_module.c` +- `keyboard_core_module.c` + +这些模块只应该感知当前连接状态,不承担槽位管理职责。 + +## 14. 实施分阶段建议 + +## 阶段 1:固定 identity 多槽骨架 + +- 替换 CAF 默认 `ble_bond` +- 新增 `ble_bond_multi_module` +- 固定 `identity 1/2/3` 为 BLE 槽位 +- 预留 `identity 4` 为 dongle 槽位 +- 恢复和保存 `current_slot` +- 打通直接切槽 + +交付标准: + +- `Slot 1~3` 可以切换 +- 每个槽位 bond 独立 +- 重启后当前槽位可恢复 + +## 阶段 2:slot_meta 与 UI 打通 + +- 维护 `occupied` +- 维护 `last_peer_addr` +- 正式持久化 `display_name` +- UI 改为显示真实槽位数据 + +交付标准: + +- 屏幕上显示真实槽位名 +- `display_name` 可稳定持久化 + +## 阶段 3:直接擦除与 dongle 预留对齐 + +- 实现当前槽位直接 `unpair` +- 擦除后刷新 `slot_meta` +- 擦除后重新进入可配对状态 +- 明确 `identity 4` 的模式入口 + +交付标准: + +- 擦除后旧主机不能再连接 +- 当前槽位可重新配对 +- `24G` 路径已有干净预留点 + +## 15. 风险与注意事项 + +## 15.1 直接擦除的用户体验 + +本方案放弃回滚能力。 +一旦擦除: + +- 旧 bond 立即失效 +- 用户必须重新配对 + +这是当前明确接受的产品行为。 + +## 15.2 display_name 获取机制 + +`display_name` 虽然是正式字段,但自动来源未必现成。 +实现阶段必须明确: + +- 名称是自动采集 +- 还是通过 UI / 上位机写入 + +这不是字段是否保留的问题,而是字段填充路径的问题。 + +## 15.3 `BT_ID_MAX=5` 可用性 + +必须在目标板上验证: + +- identity 创建正常 +- 切换 advertising identity 正常 +- settings 恢复正常 + +## 15.4 默认 identity 不参与槽位 + +本方案建议 `BT_ID_DEFAULT` 不参与槽位管理,避免后续行为不一致。 + +## 16. 当前版本结论 + +按目前所有已确认信息,最终设计结论如下: + +1. 只做 BLE 三槽,dongle 只预留专用槽位 +2. 三个 BLE 槽位直接固定到 `bt_identity_id 1/2/3` +3. `bt_identity_id 4` 预留给 dongle +4. 不使用 `app_slot_id` +5. 不使用临时 identity +6. 不做 erase advertising +7. 不做擦除确认和回滚 +8. `slot_meta` 固定包含: + - `occupied` + - `last_peer_addr` + - `display_name` +9. `display_name` 作为正式持久化字段保留,用于 UI 显示 +10. 不新增 `inc/ble_bond_multi.h`,共享接口放到事件头文件 + +如果你认可这个版本,下一步就可以按这份方案进入实现阶段。 diff --git a/inc/events/ble_bond_multi_event.h b/inc/events/ble_bond_multi_event.h new file mode 100644 index 0000000..8bb9aac --- /dev/null +++ b/inc/events/ble_bond_multi_event.h @@ -0,0 +1,46 @@ +#ifndef BLINKY_BLE_BOND_MULTI_EVENT_H_ +#define BLINKY_BLE_BOND_MULTI_EVENT_H_ + +#include + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define BLE_BOND_MULTI_DISPLAY_NAME_MAX_LEN 32U +#define BLE_BOND_MULTI_BLE_SLOT_COUNT 3U +#define BLE_BOND_MULTI_DONGLE_SLOT_ID 4U + +enum ble_bond_multi_op { + BLE_BOND_MULTI_OP_NONE = 0, + BLE_BOND_MULTI_OP_REFRESH, + BLE_BOND_MULTI_OP_SWITCH, + BLE_BOND_MULTI_OP_ERASE, +}; + +struct ble_bond_multi_slot_meta { + bool occupied; + bt_addr_le_t last_peer_addr; + char display_name[BLE_BOND_MULTI_DISPLAY_NAME_MAX_LEN]; +}; + +struct ble_bond_multi_event { + struct app_event_header header; + uint8_t current_slot; + uint8_t active_identity_id; + enum ble_bond_multi_op op; + uint8_t slot_occupied_bitmap; + struct ble_bond_multi_slot_meta slots[BLE_BOND_MULTI_BLE_SLOT_COUNT]; +}; + +APP_EVENT_TYPE_DECLARE(ble_bond_multi_event); + +#ifdef __cplusplus +} +#endif + +#endif /* BLINKY_BLE_BOND_MULTI_EVENT_H_ */ diff --git a/inc/ui/ui_settings_controller.h b/inc/ui/ui_settings_controller.h index d9fac87..74e1987 100644 --- a/inc/ui/ui_settings_controller.h +++ b/inc/ui/ui_settings_controller.h @@ -12,6 +12,8 @@ extern "C" { #endif +struct ble_bond_multi_slot_meta; + void ui_settings_controller_open(void); void ui_settings_controller_close(void); bool ui_settings_controller_back(void); @@ -27,6 +29,9 @@ const char *ui_settings_ble_current_label(void); const char *ui_settings_ble_slot_label(uint8_t slot); void ui_settings_ble_select_slot(uint8_t slot); void ui_settings_ble_erase_current(void); +void ui_settings_ble_set_current_slot(uint8_t slot); +void ui_settings_ble_set_slot_meta(uint8_t slot, + const struct ble_bond_multi_slot_meta *meta); void ui_settings_theme_set_current(struct theme_rgb theme); const char *ui_settings_theme_current_name(void); uint8_t ui_settings_theme_current_index(void); diff --git a/prj.conf b/prj.conf index e0acd89..843c0a7 100644 --- a/prj.conf +++ b/prj.conf @@ -60,7 +60,8 @@ CONFIG_BT_SMP=y CONFIG_BT_BONDABLE=y CONFIG_BT_SETTINGS=y CONFIG_BT_MAX_CONN=1 -CONFIG_BT_MAX_PAIRED=1 +CONFIG_BT_MAX_PAIRED=4 +CONFIG_BT_ID_MAX=5 CONFIG_BT_ATT_TX_COUNT=5 CONFIG_BT_L2CAP_TX_MTU=65 CONFIG_BT_BUF_ACL_RX_SIZE=69 @@ -110,7 +111,7 @@ CONFIG_CAF_BLE_ADV_SUSPEND_ON_READY=y CONFIG_CAF_BLE_ADV_FAST_ADV=y CONFIG_CAF_BLE_ADV_FILTER_ACCEPT_LIST=y CONFIG_CAF_BLE_ADV_MODULE_SUSPEND_EVENTS=y -CONFIG_CAF_BLE_BOND=y +CONFIG_CAF_BLE_BOND=n CONFIG_CAF_MODULE_SUSPEND_EVENTS=y CONFIG_BT_ADV_PROV_FLAGS=y CONFIG_BT_ADV_PROV_GAP_APPEARANCE=y diff --git a/src/ble_bond_multi_module.c b/src/ble_bond_multi_module.c new file mode 100644 index 0000000..57c8bdc --- /dev/null +++ b/src/ble_bond_multi_module.c @@ -0,0 +1,527 @@ +#include +#include +#include +#include +#include + +#include + +#define MODULE ble_bond +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ble_bond_multi_event.h" +#include "module_lifecycle.h" + +LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); + +#define BLE_SLOT_MIN 1U +#define BLE_SLOT_MAX BLE_BOND_MULTI_BLE_SLOT_COUNT +#define IDENTITY_DONGLE BLE_BOND_MULTI_DONGLE_SLOT_ID +#define SETTINGS_KEY_CURRENT_SLOT "current_slot" +#define SETTINGS_KEY_META_PREFIX "meta/" +#define DEFAULT_DISPLAY_NAME_BONDED "Bonded Device" +#define DEFAULT_DISPLAY_NAME_EMPTY "Empty" + +struct ble_bond_multi_slot_meta_storage { + uint8_t occupied; + bt_addr_le_t last_peer_addr; + char display_name[BLE_BOND_MULTI_DISPLAY_NAME_MAX_LEN]; +}; + +struct ble_bond_multi_ctx { + struct module_lifecycle_ctx lc; + uint8_t current_slot; + uint8_t pending_slot; + bool current_slot_valid; + bool identities_ready; + struct ble_bond_multi_slot_meta slot_meta[CONFIG_BT_ID_MAX]; + struct bt_conn *active_conn; +}; + +static int do_init(void); +static int do_start(void); +static int do_stop(void); +static int identity_ensure_exists(uint8_t identity); +static void slot_bond_cnt_cb(const struct bt_bond_info *info, void *user_data); +int ble_bond_multi_select_slot(uint8_t slot); +int ble_bond_multi_erase_current_slot(void); +const struct ble_bond_multi_slot_meta *ble_bond_multi_get_slot_meta(uint8_t slot); +uint8_t ble_bond_multi_get_current_slot(void); + +static const struct module_lifecycle_cfg lifecycle_cfg = { + .mode = ML_MODE_NONE, + .stopped_state = MODULE_STATE_OFF, +}; + +static const struct module_lifecycle_ops lifecycle_ops = { + .do_init = do_init, + .do_start = do_start, + .do_stop = do_stop, +}; + +static struct ble_bond_multi_ctx ctx = { + .lc = { + .state = LC_UNINIT, + .cfg = &lifecycle_cfg, + .ops = &lifecycle_ops, + }, + .current_slot = BLE_SLOT_MIN, + .pending_slot = BLE_SLOT_MIN, +}; + +BUILD_ASSERT(CONFIG_BT_ID_MAX > IDENTITY_DONGLE, + "BT_ID_MAX must include BLE slots and dongle slot"); +BUILD_ASSERT(CONFIG_BT_MAX_PAIRED >= 4, + "BT_MAX_PAIRED must allow three BLE slots and dongle slot"); + +static bool is_ble_slot(uint8_t slot) +{ + return (slot >= BLE_SLOT_MIN) && (slot <= BLE_SLOT_MAX); +} + +static void display_name_set_default(uint8_t slot) +{ + struct ble_bond_multi_slot_meta *meta = &ctx.slot_meta[slot]; + const char *name = meta->occupied ? DEFAULT_DISPLAY_NAME_BONDED : + DEFAULT_DISPLAY_NAME_EMPTY; + + strncpy(meta->display_name, name, sizeof(meta->display_name)); + meta->display_name[sizeof(meta->display_name) - 1U] = '\0'; +} + +static void slot_meta_clear(uint8_t slot) +{ + struct ble_bond_multi_slot_meta *meta = &ctx.slot_meta[slot]; + + memset(meta, 0, sizeof(*meta)); + bt_addr_le_copy(&meta->last_peer_addr, BT_ADDR_LE_ANY); + display_name_set_default(slot); +} + +static void slot_meta_ensure_name(uint8_t slot) +{ + if (ctx.slot_meta[slot].display_name[0] == '\0') { + display_name_set_default(slot); + } +} + +static void slot_meta_from_storage(uint8_t slot, + const struct ble_bond_multi_slot_meta_storage *storage) +{ + struct ble_bond_multi_slot_meta *meta = &ctx.slot_meta[slot]; + + meta->occupied = (storage->occupied != 0U); + bt_addr_le_copy(&meta->last_peer_addr, &storage->last_peer_addr); + memcpy(meta->display_name, storage->display_name, sizeof(meta->display_name)); + meta->display_name[sizeof(meta->display_name) - 1U] = '\0'; + slot_meta_ensure_name(slot); +} + +static void slot_meta_to_storage(uint8_t slot, + struct ble_bond_multi_slot_meta_storage *storage) +{ + const struct ble_bond_multi_slot_meta *meta = &ctx.slot_meta[slot]; + + memset(storage, 0, sizeof(*storage)); + storage->occupied = meta->occupied ? 1U : 0U; + bt_addr_le_copy(&storage->last_peer_addr, &meta->last_peer_addr); + memcpy(storage->display_name, meta->display_name, + sizeof(storage->display_name)); +} + +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, SETTINGS_KEY_CURRENT_SLOT)) { + uint8_t stored_slot; + + if (len_rd != sizeof(stored_slot)) { + return 0; + } + + rc = read_cb(cb_arg, &stored_slot, sizeof(stored_slot)); + if (rc == sizeof(stored_slot) && is_ble_slot(stored_slot)) { + ctx.current_slot = stored_slot; + ctx.pending_slot = stored_slot; + ctx.current_slot_valid = true; + } + + return 0; + } + + if (!strncmp(key, SETTINGS_KEY_META_PREFIX, + sizeof(SETTINGS_KEY_META_PREFIX) - 1)) { + const char *slot_str = key + (sizeof(SETTINGS_KEY_META_PREFIX) - 1); + long slot = strtol(slot_str, NULL, 10); + struct ble_bond_multi_slot_meta_storage storage; + + if ((slot < BLE_SLOT_MIN) || (slot > IDENTITY_DONGLE) || + (len_rd != sizeof(storage))) { + return 0; + } + + rc = read_cb(cb_arg, &storage, sizeof(storage)); + if (rc == sizeof(storage)) { + slot_meta_from_storage((uint8_t)slot, &storage); + } + + return 0; + } + + return 0; +} + +SETTINGS_STATIC_HANDLER_DEFINE(ble_bond_multi, "ble_multi", NULL, settings_set, + NULL, NULL); + +static int settings_save_current_slot(void) +{ + int err; + + err = settings_save_one("ble_multi/" SETTINGS_KEY_CURRENT_SLOT, + &ctx.current_slot, + sizeof(ctx.current_slot)); + if (err) { + LOG_ERR("Save current slot failed (%d)", err); + } + + return err; +} + +static int settings_save_slot_meta(uint8_t slot) +{ + int err; + char key[24]; + struct ble_bond_multi_slot_meta_storage storage; + + slot_meta_to_storage(slot, &storage); + snprintk(key, sizeof(key), "ble_multi/" SETTINGS_KEY_META_PREFIX "%u", slot); + err = settings_save_one(key, &storage, sizeof(storage)); + if (err) { + LOG_ERR("Save slot %u meta failed (%d)", slot, err); + } + + return err; +} + +static void active_conn_clear(void) +{ + ctx.active_conn = NULL; +} + +static void bond_addr_get_cb(const struct bt_bond_info *info, void *user_data) +{ + bt_addr_le_t *addr = user_data; + + if (!bt_addr_le_cmp(addr, BT_ADDR_LE_ANY)) { + bt_addr_le_copy(addr, &info->addr); + } +} + +static void publish_state(enum ble_bond_multi_op op) +{ + struct ble_bond_multi_event *event = new_ble_bond_multi_event(); + uint8_t occ = 0U; + + event->current_slot = ctx.current_slot; + event->active_identity_id = ctx.current_slot; + event->op = op; + + for (uint8_t slot = BLE_SLOT_MIN; slot <= BLE_SLOT_MAX; slot++) { + if (ctx.slot_meta[slot].occupied) { + occ |= BIT(slot - BLE_SLOT_MIN); + } + + event->slots[slot - BLE_SLOT_MIN] = ctx.slot_meta[slot]; + } + + event->slot_occupied_bitmap = occ; + APP_EVENT_SUBMIT(event); +} + +static int identity_ensure_exists(uint8_t identity) +{ + size_t count = CONFIG_BT_ID_MAX; + bt_addr_le_t addrs[CONFIG_BT_ID_MAX]; + + (void)bt_id_get(addrs, &count); + + while (count <= identity) { + int id = bt_id_create(NULL, NULL); + + if (id < 0) { + LOG_ERR("bt_id_create failed (%d)", id); + return id; + } + + count++; + } + + return 0; +} + +static bool slot_has_bond(uint8_t slot) +{ + size_t cnt = 0; + + bt_foreach_bond(slot, slot_bond_cnt_cb, &cnt); + + return cnt > 0U; +} + +static void slot_bond_cnt_cb(const struct bt_bond_info *info, void *user_data) +{ + size_t *count = user_data; + + ARG_UNUSED(info); + (*count)++; +} + +static void slot_update_from_bonds(uint8_t slot) +{ + struct ble_bond_multi_slot_meta *meta = &ctx.slot_meta[slot]; + + meta->occupied = slot_has_bond(slot); + if (!meta->occupied) { + bt_addr_le_copy(&meta->last_peer_addr, BT_ADDR_LE_ANY); + } else if (!bt_addr_le_cmp(&meta->last_peer_addr, BT_ADDR_LE_ANY)) { + bt_foreach_bond(slot, bond_addr_get_cb, &meta->last_peer_addr); + } + slot_meta_ensure_name(slot); +} + +static void all_slots_refresh_from_bonds(void) +{ + for (uint8_t slot = BLE_SLOT_MIN; slot <= IDENTITY_DONGLE; slot++) { + slot_update_from_bonds(slot); + } +} + +static void submit_selected_event(uint8_t identity) +{ + struct ble_peer_operation_event *event = new_ble_peer_operation_event(); + + event->op = PEER_OPERATION_SELECTED; + event->bt_app_id = identity; + event->bt_stack_id = identity; + APP_EVENT_SUBMIT(event); +} + +static void submit_erased_event(uint8_t identity) +{ + struct ble_peer_operation_event *event = new_ble_peer_operation_event(); + + event->op = PEER_OPERATION_ERASED; + event->bt_app_id = identity; + event->bt_stack_id = identity; + APP_EVENT_SUBMIT(event); +} + +static int switch_slot(uint8_t slot) +{ + if (!is_ble_slot(slot) || (slot == ctx.current_slot)) { + return 0; + } + + ctx.pending_slot = slot; + ctx.current_slot = slot; + submit_selected_event(slot); + return settings_save_current_slot(); +} + +static int erase_slot(uint8_t slot) +{ + int err; + + err = bt_unpair(slot, BT_ADDR_LE_ANY); + if (err) { + LOG_ERR("bt_unpair slot %u failed (%d)", slot, err); + return err; + } + + slot_meta_clear(slot); + err = settings_save_slot_meta(slot); + if (err) { + return err; + } + + submit_erased_event(slot); + submit_selected_event(slot); + return 0; +} + +static int do_init(void) +{ + for (uint8_t slot = BLE_SLOT_MIN; slot <= IDENTITY_DONGLE; slot++) { + slot_meta_clear(slot); + } + + if (!ctx.current_slot_valid) { + ctx.current_slot = BLE_SLOT_MIN; + ctx.pending_slot = BLE_SLOT_MIN; + } + + active_conn_clear(); + + return 0; +} + +static int do_start(void) +{ + int err; + + for (uint8_t identity = BLE_SLOT_MIN; identity <= IDENTITY_DONGLE; + identity++) { + err = identity_ensure_exists(identity); + if (err) { + return err; + } + } + ctx.identities_ready = true; + + all_slots_refresh_from_bonds(); + for (uint8_t slot = BLE_SLOT_MIN; slot <= IDENTITY_DONGLE; slot++) { + (void)settings_save_slot_meta(slot); + } + + submit_selected_event(ctx.current_slot); + publish_state(BLE_BOND_MULTI_OP_REFRESH); + + return 0; +} + +static int do_stop(void) +{ + active_conn_clear(); + return 0; +} + +static bool handle_ble_peer_event(const struct ble_peer_event *event) +{ + switch (event->state) { + case PEER_STATE_CONNECTED: + ctx.active_conn = event->id; + return false; + + case PEER_STATE_SECURED: + if (ctx.active_conn != event->id) { + return false; + } + + ctx.slot_meta[ctx.current_slot].occupied = true; + bt_addr_le_copy(&ctx.slot_meta[ctx.current_slot].last_peer_addr, + bt_conn_get_dst(ctx.active_conn)); + if ((ctx.slot_meta[ctx.current_slot].display_name[0] == '\0') || + !strcmp(ctx.slot_meta[ctx.current_slot].display_name, + DEFAULT_DISPLAY_NAME_EMPTY)) { + display_name_set_default(ctx.current_slot); + } + + (void)settings_save_slot_meta(ctx.current_slot); + publish_state(BLE_BOND_MULTI_OP_REFRESH); + return false; + + case PEER_STATE_DISCONNECTED: + case PEER_STATE_CONN_FAILED: + if (ctx.active_conn == event->id) { + active_conn_clear(); + } + return false; + + default: + 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(settings_loader), MODULE_STATE_READY)) { + (void)module_set_lifecycle(&ctx.lc, LC_RUNNING); + return false; + } + + return false; + } + + if (is_ble_peer_event(aeh)) { + return handle_ble_peer_event(cast_ble_peer_event(aeh)); + } + + if (is_ble_bond_multi_event(aeh)) { + return false; + } + + return false; +} + +int ble_bond_multi_select_slot(uint8_t slot) +{ + int err; + + if (!module_lifecycle_is_running(&ctx.lc)) { + return -EAGAIN; + } + + if (!ctx.identities_ready) { + return -EAGAIN; + } + + err = switch_slot(slot); + if (!err) { + publish_state(BLE_BOND_MULTI_OP_SWITCH); + } + + return err; +} + +int ble_bond_multi_erase_current_slot(void) +{ + int err; + + if (!module_lifecycle_is_running(&ctx.lc)) { + return -EAGAIN; + } + + if (!ctx.identities_ready) { + return -EAGAIN; + } + + err = erase_slot(ctx.current_slot); + if (!err) { + publish_state(BLE_BOND_MULTI_OP_ERASE); + } + + return err; +} + +const struct ble_bond_multi_slot_meta *ble_bond_multi_get_slot_meta(uint8_t slot) +{ + if (!is_ble_slot(slot)) { + return NULL; + } + + return &ctx.slot_meta[slot]; +} + +uint8_t ble_bond_multi_get_current_slot(void) +{ + return ctx.current_slot; +} + +APP_EVENT_LISTENER(MODULE, app_event_handler); +APP_EVENT_SUBSCRIBE(MODULE, module_state_event); +APP_EVENT_SUBSCRIBE_EARLY(MODULE, ble_peer_event); diff --git a/src/display_module.c b/src/display_module.c index 12dbdb5..0ab8692 100644 --- a/src/display_module.c +++ b/src/display_module.c @@ -14,6 +14,7 @@ #include #include "bat_state_event.h" +#include "ble_bond_multi_event.h" #include "datetime_event.h" #include "hid_led_event.h" #include "module_lifecycle.h" @@ -213,6 +214,11 @@ static bool app_event_handler(const struct app_event_header *aeh) return false; } + if (is_ble_bond_multi_event(aeh)) { + refresh_ui(); + return false; + } + if (is_hid_led_event(aeh)) { const struct hid_led_event *event = cast_hid_led_event(aeh); @@ -316,6 +322,7 @@ static bool app_event_handler(const struct app_event_header *aeh) APP_EVENT_LISTENER(MODULE, app_event_handler); APP_EVENT_SUBSCRIBE(MODULE, bat_state_event); +APP_EVENT_SUBSCRIBE(MODULE, ble_bond_multi_event); APP_EVENT_SUBSCRIBE(MODULE, datetime_event); APP_EVENT_SUBSCRIBE(MODULE, hid_led_event); APP_EVENT_SUBSCRIBE(MODULE, module_state_event); diff --git a/src/events/ble_bond_multi_event.c b/src/events/ble_bond_multi_event.c new file mode 100644 index 0000000..5047f55 --- /dev/null +++ b/src/events/ble_bond_multi_event.c @@ -0,0 +1,38 @@ +#include "ble_bond_multi_event.h" + +static void log_ble_bond_multi_event(const struct app_event_header *aeh) +{ + const struct ble_bond_multi_event *event = + cast_ble_bond_multi_event(aeh); + + APP_EVENT_MANAGER_LOG(aeh, "slot:%u identity:%u op:%u occ:0x%02x", + event->current_slot, event->active_identity_id, + event->op, event->slot_occupied_bitmap); +} + +static void profile_ble_bond_multi_event(struct log_event_buf *buf, + const struct app_event_header *aeh) +{ + const struct ble_bond_multi_event *event = + cast_ble_bond_multi_event(aeh); + + nrf_profiler_log_encode_uint8(buf, event->current_slot); + nrf_profiler_log_encode_uint8(buf, event->active_identity_id); + nrf_profiler_log_encode_uint8(buf, event->op); + nrf_profiler_log_encode_uint8(buf, event->slot_occupied_bitmap); +} + +APP_EVENT_INFO_DEFINE(ble_bond_multi_event, + ENCODE(NRF_PROFILER_ARG_U8, + NRF_PROFILER_ARG_U8, + NRF_PROFILER_ARG_U8, + NRF_PROFILER_ARG_U8), + ENCODE("current_slot", "active_identity_id", "op", + "slot_occupied_bitmap"), + profile_ble_bond_multi_event); + +APP_EVENT_TYPE_DEFINE(ble_bond_multi_event, + log_ble_bond_multi_event, + &ble_bond_multi_event_info, + APP_EVENT_FLAGS_CREATE( + APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE)); diff --git a/src/keyboard_core_module.c b/src/keyboard_core_module.c index 8b15dc9..cf8512c 100644 --- a/src/keyboard_core_module.c +++ b/src/keyboard_core_module.c @@ -94,7 +94,6 @@ struct keyboard_core_module_ctx { uint8_t function_usage_mask[KEYBOARD_PROTOCOL_BITMAP_BYTES]; enum keyboard_protocol_mode transport_protocol_modes[HID_TRANSPORT_COUNT]; enum hid_transport_policy current_transport; - bool mode_valid; bool settings_active; }; @@ -146,8 +145,7 @@ static enum keyboard_protocol_mode active_protocol_mode_get(void) { enum hid_transport transport; - if (ctx.mode_valid && - policy_to_transport(ctx.current_transport, &transport)) { + if (policy_to_transport(ctx.current_transport, &transport)) { return ctx.transport_protocol_modes[transport]; } @@ -358,7 +356,7 @@ static void submit_consumer_fifo_frame(uint16_t usage_id) enum keyboard_protocol_mode protocol_mode = active_protocol_mode_get(); enum mode_switch_mode mode; - if (!module_lifecycle_is_running(&ctx.lc) || !ctx.mode_valid || + if (!module_lifecycle_is_running(&ctx.lc) || ctx.settings_active || !transport_policy_to_mode(ctx.current_transport, &mode) || (protocol_mode == KEYBOARD_PROTOCOL_MODE_BOOT)) { @@ -406,8 +404,7 @@ static void emit_keys_report(bool force) enum keyboard_protocol_mode protocol_mode = active_protocol_mode_get(); enum mode_switch_mode mode; - if (!ctx.mode_valid || - !transport_policy_to_mode(ctx.current_transport, &mode)) { + if (!transport_policy_to_mode(ctx.current_transport, &mode)) { return; } @@ -445,8 +442,7 @@ static void emit_consumer_report(bool force) enum keyboard_protocol_mode protocol_mode = active_protocol_mode_get(); enum mode_switch_mode mode; - if (!ctx.mode_valid || - !transport_policy_to_mode(ctx.current_transport, &mode) || + if (!transport_policy_to_mode(ctx.current_transport, &mode) || ctx.settings_active || (protocol_mode == KEYBOARD_PROTOCOL_MODE_BOOT)) { return; @@ -522,8 +518,8 @@ static int do_init(void) keyboard_state_clear(); reports_cache_invalidate(); function_usage_mask_clear(); - ctx.mode_valid = false; ctx.settings_active = false; + ctx.current_transport = HID_TRANSPORT_POLICY_USB; ctx.transport_protocol_modes[HID_TRANSPORT_USB] = KEYBOARD_PROTOCOL_MODE_REPORT; ctx.transport_protocol_modes[HID_TRANSPORT_BLE] = @@ -547,14 +543,13 @@ static int do_stop(void) return 0; } - if (ctx.mode_valid) { + if (ctx.current_transport != HID_TRANSPORT_POLICY_NONE) { emit_release_reports(ctx.current_transport); } emit_function_state_event(); keyboard_state_clear(); reports_cache_invalidate(); - ctx.mode_valid = false; return 0; } @@ -625,8 +620,8 @@ static bool handle_transport_policy_event( return false; } - transport_changed = - ctx.mode_valid && (ctx.current_transport != event->hid_transport); + transport_changed = (ctx.current_transport != HID_TRANSPORT_POLICY_NONE) && + (ctx.current_transport != event->hid_transport); if (transport_changed) { emit_release_reports(ctx.current_transport); emit_function_state_event(); @@ -635,9 +630,8 @@ static bool handle_transport_policy_event( } ctx.current_transport = event->hid_transport; - ctx.mode_valid = (ctx.current_transport != HID_TRANSPORT_POLICY_NONE); - if (ctx.mode_valid) { + if (ctx.current_transport != HID_TRANSPORT_POLICY_NONE) { emit_all_reports(true); } @@ -646,7 +640,8 @@ static bool handle_transport_policy_event( static bool handle_encoder_event(const struct encoder_event *event) { - if (!module_lifecycle_is_running(&ctx.lc) || !ctx.mode_valid) { + if (!module_lifecycle_is_running(&ctx.lc) || + (ctx.current_transport == HID_TRANSPORT_POLICY_NONE)) { return false; } @@ -701,7 +696,7 @@ static bool app_event_handler(const struct app_event_header *aeh) ctx.transport_protocol_modes[event->transport] = event->protocol_mode; - if (module_lifecycle_is_running(&ctx.lc) && ctx.mode_valid && + if (module_lifecycle_is_running(&ctx.lc) && policy_to_transport(ctx.current_transport, &active_transport) && (active_transport == event->transport)) { reports_cache_invalidate(); @@ -731,7 +726,7 @@ static bool app_event_handler(const struct app_event_header *aeh) ctx.settings_active = event->active; if (ctx.settings_active) { - if (ctx.mode_valid) { + if (ctx.current_transport != HID_TRANSPORT_POLICY_NONE) { emit_release_reports(ctx.current_transport); } emit_function_state_event(); diff --git a/src/settings_module.c b/src/settings_module.c index 22c8273..3515310 100644 --- a/src/settings_module.c +++ b/src/settings_module.c @@ -10,7 +10,9 @@ #include #include +#include +#include "ble_bond_multi_event.h" #include "encoder_event.h" #include "module_lifecycle.h" #include "settings_mode_event.h" @@ -157,6 +159,22 @@ static bool handle_theme_rgb_update_event( return false; } +static bool handle_ble_bond_multi_event( + const struct ble_bond_multi_event *event) +{ + ui_settings_ble_set_current_slot(event->current_slot); + + for (uint8_t i = 0U; i < ARRAY_SIZE(event->slots); i++) { + ui_settings_ble_set_slot_meta(i + 1U, &event->slots[i]); + } + + if (ctx.active) { + ui_settings_controller_refresh(false); + } + + return false; +} + static bool app_event_handler(const struct app_event_header *aeh) { if (is_click_event(aeh)) { @@ -172,6 +190,11 @@ static bool app_event_handler(const struct app_event_header *aeh) cast_theme_rgb_update_event(aeh)); } + if (is_ble_bond_multi_event(aeh)) { + return handle_ble_bond_multi_event( + cast_ble_bond_multi_event(aeh)); + } + if (is_module_state_event(aeh)) { const struct module_state_event *event = cast_module_state_event(aeh); @@ -206,5 +229,6 @@ APP_EVENT_SUBSCRIBE(MODULE, click_event); APP_EVENT_SUBSCRIBE(MODULE, encoder_event); APP_EVENT_SUBSCRIBE(MODULE, module_state_event); APP_EVENT_SUBSCRIBE(MODULE, theme_rgb_update_event); +APP_EVENT_SUBSCRIBE(MODULE, ble_bond_multi_event); APP_EVENT_SUBSCRIBE_EARLY(MODULE, power_down_event); APP_EVENT_SUBSCRIBE(MODULE, wake_up_event); diff --git a/src/ui/ui_settings_controller.c b/src/ui/ui_settings_controller.c index 0c00c17..4d4bc1a 100644 --- a/src/ui/ui_settings_controller.c +++ b/src/ui/ui_settings_controller.c @@ -5,25 +5,29 @@ #include +#include "ble_bond_multi_event.h" #include "ui/ui_settings_controller.h" #include "settings_view_event.h" #include "ui_settings_pages.h" #define BLE_SLOT_COUNT 3U +int ble_bond_multi_select_slot(uint8_t slot); +int ble_bond_multi_erase_current_slot(void); + struct controller_ctx { struct ui_settings_page *current; bool active; uint8_t active_ble_slot; - char ble_labels[BLE_SLOT_COUNT][16]; + char ble_labels[BLE_SLOT_COUNT][BLE_BOND_MULTI_DISPLAY_NAME_MAX_LEN]; struct theme_rgb theme; }; static struct controller_ctx ctx = { .active_ble_slot = 0U, .ble_labels = { - "Host A", - "Host B", + "Empty", + "Empty", "Empty", }, .theme = { @@ -164,16 +168,13 @@ const char *ui_settings_ble_current_label(void) void ui_settings_ble_select_slot(uint8_t slot) { if (slot < BLE_SLOT_COUNT) { - ctx.active_ble_slot = slot; + (void)ble_bond_multi_select_slot(slot + 1U); } } void ui_settings_ble_erase_current(void) { - strncpy(ctx.ble_labels[ctx.active_ble_slot], "Empty", - sizeof(ctx.ble_labels[ctx.active_ble_slot])); - ctx.ble_labels[ctx.active_ble_slot] - [sizeof(ctx.ble_labels[ctx.active_ble_slot]) - 1U] = '\0'; + (void)ble_bond_multi_erase_current_slot(); } const char *ui_settings_ble_slot_label(uint8_t slot) @@ -185,6 +186,26 @@ const char *ui_settings_ble_slot_label(uint8_t slot) return ctx.ble_labels[slot]; } +void ui_settings_ble_set_current_slot(uint8_t slot) +{ + if ((slot >= 1U) && (slot <= BLE_SLOT_COUNT)) { + ctx.active_ble_slot = slot - 1U; + } +} + +void ui_settings_ble_set_slot_meta(uint8_t slot, + const struct ble_bond_multi_slot_meta *meta) +{ + if ((meta == NULL) || (slot < 1U) || (slot > BLE_SLOT_COUNT)) { + return; + } + + strncpy(ctx.ble_labels[slot - 1U], + meta->display_name[0] ? meta->display_name : "Empty", + sizeof(ctx.ble_labels[slot - 1U])); + ctx.ble_labels[slot - 1U][sizeof(ctx.ble_labels[slot - 1U]) - 1U] = '\0'; +} + void ui_settings_theme_set_current(struct theme_rgb theme) { ctx.theme = theme;