diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d89e5a..d84b4f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,8 +14,10 @@ zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src/events) target_sources(app PRIVATE src/main.c src/events/battery_status_event.c + src/events/config_event.c src/events/mode_event.c src/modules/battery_module.c + src/modules/ble_bond_module.c src/modules/button_map_module.c src/modules/mode_switch_module.c src/modules/hids_module.c diff --git a/prj.conf b/prj.conf index 3076c5b..99787d9 100644 --- a/prj.conf +++ b/prj.conf @@ -9,6 +9,8 @@ CONFIG_BT_SMP=y CONFIG_BT_DEVICE_NAME="new_kbd" CONFIG_BT_DEVICE_APPEARANCE=961 CONFIG_BT_MAX_CONN=1 +CONFIG_BT_MAX_PAIRED=4 +CONFIG_BT_ID_MAX=4 CONFIG_SETTINGS=y CONFIG_SETTINGS_NVS=y CONFIG_NVS=y diff --git a/src/events/config_event.c b/src/events/config_event.c new file mode 100644 index 0000000..ca0d367 --- /dev/null +++ b/src/events/config_event.c @@ -0,0 +1,18 @@ +#include "config_event.h" + +static void log_config_event(const struct app_event_header *aeh) +{ + const struct config_event *event = cast_config_event(aeh); + + APP_EVENT_MANAGER_LOG(aeh, "status:%u %s rcpt:%02x id:%02x", + event->status, + event->is_request ? "req" : "rsp", + event->recipient, + event->event_id); +} + +APP_EVENT_TYPE_DEFINE(config_event, + log_config_event, + NULL, + APP_EVENT_FLAGS_CREATE( + APP_EVENT_TYPE_FLAGS_INIT_LOG_ENABLE)); diff --git a/src/events/config_event.h b/src/events/config_event.h new file mode 100644 index 0000000..06ed7ce --- /dev/null +++ b/src/events/config_event.h @@ -0,0 +1,54 @@ +/* + * Lightweight config event used for local module configuration. + */ + +#ifndef NEW_KBD_CONFIG_EVENT_H__ +#define NEW_KBD_CONFIG_EVENT_H__ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Keep the same local recipient as nRF Desktop config channel. */ +#define CFG_CHAN_RECIPIENT_LOCAL 0x00 + +/* Event ID field layout (compatible with nRF Desktop config_event encoding). */ +#define MOD_FIELD_POS 4 +#define MOD_FIELD_SIZE 4 +#define MOD_FIELD_MASK BIT_MASK(MOD_FIELD_SIZE) +#define MOD_FIELD_GET(id) (((id) >> MOD_FIELD_POS) & MOD_FIELD_MASK) + +#define OPT_FIELD_POS 0 +#define OPT_FIELD_SIZE 4 +#define OPT_FIELD_MASK BIT_MASK(OPT_FIELD_SIZE) +#define OPT_FIELD_GET(id) (((id) >> OPT_FIELD_POS) & OPT_FIELD_MASK) +#define OPT_ID_GET(opt) ((opt) - 1U) + +enum config_status { + CONFIG_STATUS_SET = 0, + CONFIG_STATUS_FETCH, + CONFIG_STATUS_SUCCESS, + CONFIG_STATUS_REJECT, +}; + +struct config_event { + struct app_event_header header; + + uint16_t transport_id; + bool is_request; + uint8_t event_id; + uint8_t recipient; + uint8_t status; + struct event_dyndata dyndata; +}; + +APP_EVENT_TYPE_DYNDATA_DECLARE(config_event); + +#ifdef __cplusplus +} +#endif + +#endif /* NEW_KBD_CONFIG_EVENT_H__ */ diff --git a/src/modules/ble_bond_module.c b/src/modules/ble_bond_module.c new file mode 100644 index 0000000..0741575 --- /dev/null +++ b/src/modules/ble_bond_module.c @@ -0,0 +1,326 @@ +#include +#include + +#include + +#define MODULE ble_bond +#include +#include +#include + +#include "config_event.h" + +#include +LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); + +/* Application-visible options carried by config_event. */ +enum ble_bond_cfg_opt { + BLE_BOND_CFG_PEER_SELECT = 0, + BLE_BOND_CFG_PEER_ERASE, + BLE_BOND_CFG_PEER_ERASE_ALL, + BLE_BOND_CFG_OPT_COUNT +}; + +/* Module ID in config_event event_id[7:4]. Keep stable for host side tooling. */ +#define BLE_BOND_CFG_MODULE_ID 0x01 + +#define PEER_ID_KEY "peer_id" +#define BT_LUT_KEY "bt_lut" + +enum state { + STATE_DISABLED, + STATE_IDLE, + STATE_STANDBY, +}; + +BUILD_ASSERT(CONFIG_BT_ID_MAX >= 2, "Need at least one resettable identity"); + +#define APP_PEER_COUNT (CONFIG_BT_ID_MAX - 1) + +static enum state state = STATE_DISABLED; + +/* app peer id -> bt stack id mapping (default identity 0 is not used). */ +static uint8_t bt_stack_id_lut[APP_PEER_COUNT]; +static bool bt_stack_id_lut_valid; + +static uint8_t cur_ble_peer_id; +static bool cur_peer_id_valid; + +static uint8_t get_bt_stack_peer_id(uint8_t app_id) +{ + __ASSERT_NO_MSG(app_id < APP_PEER_COUNT); + return bt_stack_id_lut[app_id]; +} + +static int store_peer_id(uint8_t peer_id) +{ + char key[] = MODULE_NAME "/" PEER_ID_KEY; + + return settings_save_one(key, &peer_id, sizeof(peer_id)); +} + +static int store_bt_stack_id_lut(void) +{ + char key[] = MODULE_NAME "/" BT_LUT_KEY; + + return settings_save_one(key, bt_stack_id_lut, sizeof(bt_stack_id_lut)); +} + +static void submit_peer_op_event(enum peer_operation op, uint8_t app_id) +{ + struct ble_peer_operation_event *event = new_ble_peer_operation_event(); + + event->op = op; + event->bt_app_id = app_id; + event->bt_stack_id = get_bt_stack_peer_id(app_id); + APP_EVENT_SUBMIT(event); +} + +static void init_bt_stack_id_lut(void) +{ + for (size_t i = 0; i < ARRAY_SIZE(bt_stack_id_lut); i++) { + /* Keep id 0 (BT_ID_DEFAULT) untouched for safe reset/unpair flow. */ + bt_stack_id_lut[i] = i + 1; + } +} + +static bool storage_data_is_valid(void) +{ + if (!cur_peer_id_valid || !bt_stack_id_lut_valid) { + return false; + } + + if (cur_ble_peer_id >= APP_PEER_COUNT) { + return false; + } + + for (size_t i = 0; i < ARRAY_SIZE(bt_stack_id_lut); i++) { + if ((bt_stack_id_lut[i] == BT_ID_DEFAULT) || + (bt_stack_id_lut[i] >= CONFIG_BT_ID_MAX)) { + return false; + } + } + + return true; +} + +static int settings_set(const char *key, size_t len_rd, + settings_read_cb read_cb, void *cb_arg) +{ + ssize_t rc; + + if (!strcmp(key, PEER_ID_KEY)) { + if (len_rd != sizeof(cur_ble_peer_id)) { + cur_peer_id_valid = false; + return 0; + } + + rc = read_cb(cb_arg, &cur_ble_peer_id, sizeof(cur_ble_peer_id)); + cur_peer_id_valid = (rc == sizeof(cur_ble_peer_id)); + } else if (!strcmp(key, BT_LUT_KEY)) { + if (len_rd != sizeof(bt_stack_id_lut)) { + bt_stack_id_lut_valid = false; + return 0; + } + + rc = read_cb(cb_arg, bt_stack_id_lut, sizeof(bt_stack_id_lut)); + bt_stack_id_lut_valid = (rc == sizeof(bt_stack_id_lut)); + } + + return 0; +} + +SETTINGS_STATIC_HANDLER_DEFINE(ble_bond, MODULE_NAME, NULL, settings_set, NULL, NULL); + +static void load_identities(void) +{ + bt_addr_le_t addrs[CONFIG_BT_ID_MAX]; + size_t count = ARRAY_SIZE(addrs); + + bt_id_get(addrs, &count); + + for (; count < CONFIG_BT_ID_MAX; count++) { + int err = bt_id_create(NULL, NULL); + if (err < 0) { + LOG_ERR("Cannot create identity (err:%d)", err); + module_set_state(MODULE_STATE_ERROR); + return; + } + } +} + +static int erase_peer(uint8_t app_id) +{ + uint8_t stack_id = get_bt_stack_peer_id(app_id); + int err; + + /* Tell ble_adv to restart advertising session for this identity. */ + submit_peer_op_event(PEER_OPERATION_ERASE_ADV, app_id); + + err = bt_unpair(stack_id, NULL); + if (err) { + LOG_ERR("Cannot unpair id %u (err:%d)", stack_id, err); + return err; + } + + err = bt_id_reset(stack_id, NULL, NULL); + if (err < 0) { + LOG_ERR("Cannot reset id %u (err:%d)", stack_id, err); + return err; + } + + submit_peer_op_event(PEER_OPERATION_ERASED, app_id); + return 0; +} + +static int erase_all_peers(void) +{ + for (uint8_t i = 0; i < APP_PEER_COUNT; i++) { + int err = erase_peer(i); + if (err) { + return err; + } + } + + return 0; +} + +static bool handle_config_event(const struct config_event *event) +{ + if (!event->is_request || (event->recipient != CFG_CHAN_RECIPIENT_LOCAL)) { + return false; + } + + if (MOD_FIELD_GET(event->event_id) != BLE_BOND_CFG_MODULE_ID) { + return false; + } + + struct config_event *rsp = new_config_event(0); + rsp->transport_id = event->transport_id; + rsp->is_request = false; + rsp->event_id = event->event_id; + rsp->recipient = event->recipient; + rsp->status = CONFIG_STATUS_REJECT; + + if (event->status == CONFIG_STATUS_SET) { + uint8_t opt_field = OPT_FIELD_GET(event->event_id); + uint8_t opt_id = (opt_field == 0) ? UINT8_MAX : OPT_ID_GET(opt_field); + + switch (opt_id) { + case BLE_BOND_CFG_PEER_SELECT: + if (event->dyndata.size >= 1) { + uint8_t peer_id = event->dyndata.data[0]; + if (peer_id < APP_PEER_COUNT) { + cur_ble_peer_id = peer_id; + if (!store_peer_id(cur_ble_peer_id)) { + submit_peer_op_event(PEER_OPERATION_SELECTED, cur_ble_peer_id); + rsp->status = CONFIG_STATUS_SUCCESS; + } + } + } + break; + + case BLE_BOND_CFG_PEER_ERASE: + if (!erase_peer(cur_ble_peer_id)) { + rsp->status = CONFIG_STATUS_SUCCESS; + } + break; + + case BLE_BOND_CFG_PEER_ERASE_ALL: + if (!erase_all_peers()) { + rsp->status = CONFIG_STATUS_SUCCESS; + } + break; + + default: + break; + } + } else if (event->status == CONFIG_STATUS_FETCH) { + uint8_t opt_field = OPT_FIELD_GET(event->event_id); + uint8_t opt_id = (opt_field == 0) ? UINT8_MAX : OPT_ID_GET(opt_field); + + if (opt_id == BLE_BOND_CFG_PEER_SELECT) { + struct config_event *rsp_data = new_config_event(1); + rsp_data->transport_id = event->transport_id; + rsp_data->is_request = false; + rsp_data->event_id = event->event_id; + rsp_data->recipient = event->recipient; + rsp_data->status = CONFIG_STATUS_SUCCESS; + rsp_data->dyndata.data[0] = cur_ble_peer_id; + APP_EVENT_SUBMIT(rsp_data); + return true; + } + } + + APP_EVENT_SUBMIT(rsp); + return true; +} + +static int init_after_settings_loaded(void) +{ + load_identities(); + if (state == STATE_DISABLED) { + return -EFAULT; + } + + if (!storage_data_is_valid()) { + cur_ble_peer_id = 0; + init_bt_stack_id_lut(); + + if (store_peer_id(cur_ble_peer_id) || store_bt_stack_id_lut()) { + return -EIO; + } + } + + state = STATE_IDLE; + submit_peer_op_event(PEER_OPERATION_SELECTED, cur_ble_peer_id); + module_set_state(MODULE_STATE_READY); + + return 0; +} + +static bool app_event_handler(const struct app_event_header *aeh) +{ + if (is_module_state_event(aeh)) { + const struct module_state_event *event = cast_module_state_event(aeh); + + if (check_state(event, MODULE_ID(settings_loader), MODULE_STATE_READY) && + (state == STATE_DISABLED)) { + if (init_after_settings_loaded()) { + module_set_state(MODULE_STATE_ERROR); + } + } + + return false; + } + + if (is_power_down_event(aeh)) { + if (state == STATE_IDLE) { + state = STATE_STANDBY; + module_set_state(MODULE_STATE_OFF); + } + return false; + } + + if (is_wake_up_event(aeh)) { + if (state == STATE_STANDBY) { + state = STATE_IDLE; + module_set_state(MODULE_STATE_READY); + submit_peer_op_event(PEER_OPERATION_SELECTED, cur_ble_peer_id); + } + return false; + } + + if (is_config_event(aeh)) { + return handle_config_event(cast_config_event(aeh)); + } + + __ASSERT_NO_MSG(false); + return false; +} + +APP_EVENT_LISTENER(MODULE, app_event_handler); +APP_EVENT_SUBSCRIBE(MODULE, module_state_event); +APP_EVENT_SUBSCRIBE(MODULE, power_down_event); +APP_EVENT_SUBSCRIBE(MODULE, wake_up_event); +APP_EVENT_SUBSCRIBE_EARLY(MODULE, config_event);