#include #include #include #include #include #define MODULE ble_bond #include #include #include #include "config_event.h" #include LOG_MODULE_REGISTER(MODULE, LOG_LEVEL_INF); /* Application-visible options carried by config_event. */ enum ble_bond_cfg_opt { BLE_BOND_CFG_PEER_SELECT = 0, BLE_BOND_CFG_PEER_ERASE, BLE_BOND_CFG_PEER_ERASE_ALL, BLE_BOND_CFG_OPT_COUNT }; /* Module ID in config_event event_id[7:4]. Keep stable for host side tooling. */ #define BLE_BOND_CFG_MODULE_ID 0x01 #define PEER_ID_KEY "peer_id" #define BT_LUT_KEY "bt_lut" enum ble_bond_state { BLE_BOND_STATE_DISABLED, BLE_BOND_STATE_IDLE, BLE_BOND_STATE_STANDBY, }; BUILD_ASSERT(CONFIG_BT_ID_MAX >= 2, "Need at least one resettable identity"); #define APP_PEER_COUNT (CONFIG_BT_ID_MAX - 1) #define BLE_BOND_SLOT_COUNT 3 BUILD_ASSERT(BLE_BOND_SLOT_COUNT <= APP_PEER_COUNT, "BLE slot count exceeds available Bluetooth identities"); struct ble_bond_storage { uint8_t bt_stack_id_lut[APP_PEER_COUNT]; bool bt_stack_id_lut_valid; uint8_t cur_peer_id; bool cur_peer_id_valid; }; struct ble_bond_ctx { enum ble_bond_state state; struct ble_bond_storage storage; bool auto_switch_in_progress; }; static struct ble_bond_ctx bond = { .state = BLE_BOND_STATE_DISABLED, }; static const char *state_name(enum ble_bond_state s) { switch (s) { case BLE_BOND_STATE_DISABLED: return "DISABLED"; case BLE_BOND_STATE_IDLE: return "IDLE"; case BLE_BOND_STATE_STANDBY: return "STANDBY"; default: return "UNKNOWN"; } } static uint8_t get_bt_stack_peer_id(uint8_t app_id) { __ASSERT_NO_MSG(app_id < BLE_BOND_SLOT_COUNT); return bond.storage.bt_stack_id_lut[app_id]; } static int store_peer_id(uint8_t peer_id) { char key[] = MODULE_NAME "/" PEER_ID_KEY; return settings_save_one(key, &peer_id, sizeof(peer_id)); } static int store_bt_stack_id_lut(void) { char key[] = MODULE_NAME "/" BT_LUT_KEY; return settings_save_one(key, bond.storage.bt_stack_id_lut, sizeof(bond.storage.bt_stack_id_lut)); } static void submit_peer_op_event(enum peer_operation op, uint8_t app_id) { struct ble_peer_operation_event *event = new_ble_peer_operation_event(); event->op = op; event->bt_app_id = app_id; event->bt_stack_id = get_bt_stack_peer_id(app_id); APP_EVENT_SUBMIT(event); } static void init_bt_stack_id_lut(void) { for (size_t i = 0; i < ARRAY_SIZE(bond.storage.bt_stack_id_lut); i++) { /* Keep id 0 (BT_ID_DEFAULT) untouched for safe reset/unpair flow. */ bond.storage.bt_stack_id_lut[i] = i + 1; } } static bool storage_data_is_valid(void) { if (!bond.storage.cur_peer_id_valid || !bond.storage.bt_stack_id_lut_valid) { LOG_WRN("Stored data invalid: peer_valid=%d lut_valid=%d", bond.storage.cur_peer_id_valid, bond.storage.bt_stack_id_lut_valid); return false; } if (bond.storage.cur_peer_id >= BLE_BOND_SLOT_COUNT) { LOG_WRN("Stored peer id out of range: peer_id=%u max=%u", bond.storage.cur_peer_id, BLE_BOND_SLOT_COUNT - 1); return false; } for (size_t i = 0; i < ARRAY_SIZE(bond.storage.bt_stack_id_lut); i++) { if ((bond.storage.bt_stack_id_lut[i] == BT_ID_DEFAULT) || (bond.storage.bt_stack_id_lut[i] >= CONFIG_BT_ID_MAX)) { LOG_WRN("Stored LUT invalid at idx=%u value=%u", (uint32_t)i, bond.storage.bt_stack_id_lut[i]); return false; } } return true; } static int settings_set(const char *key, size_t len_rd, settings_read_cb read_cb, void *cb_arg) { ssize_t rc; if (!strcmp(key, PEER_ID_KEY)) { if (len_rd != sizeof(bond.storage.cur_peer_id)) { LOG_WRN("Settings '%s' size mismatch: got=%u expect=%u", PEER_ID_KEY, (uint32_t)len_rd, sizeof(bond.storage.cur_peer_id)); bond.storage.cur_peer_id_valid = false; return 0; } rc = read_cb(cb_arg, &bond.storage.cur_peer_id, sizeof(bond.storage.cur_peer_id)); bond.storage.cur_peer_id_valid = (rc == sizeof(bond.storage.cur_peer_id)); if (!bond.storage.cur_peer_id_valid) { LOG_WRN("Settings '%s' read failed: rc=%d", PEER_ID_KEY, (int)rc); } } else if (!strcmp(key, BT_LUT_KEY)) { if (len_rd != sizeof(bond.storage.bt_stack_id_lut)) { LOG_WRN("Settings '%s' size mismatch: got=%u expect=%u", BT_LUT_KEY, (uint32_t)len_rd, sizeof(bond.storage.bt_stack_id_lut)); bond.storage.bt_stack_id_lut_valid = false; return 0; } rc = read_cb(cb_arg, bond.storage.bt_stack_id_lut, sizeof(bond.storage.bt_stack_id_lut)); bond.storage.bt_stack_id_lut_valid = (rc == sizeof(bond.storage.bt_stack_id_lut)); if (!bond.storage.bt_stack_id_lut_valid) { LOG_WRN("Settings '%s' read failed: rc=%d", BT_LUT_KEY, (int)rc); } } return 0; } SETTINGS_STATIC_HANDLER_DEFINE(ble_bond, MODULE_NAME, NULL, settings_set, NULL, NULL); static int load_identities(void) { bt_addr_le_t addrs[CONFIG_BT_ID_MAX]; size_t count = ARRAY_SIZE(addrs); bt_id_get(addrs, &count); LOG_INF("Identity count before ensure: %u / %u", (uint32_t)count, CONFIG_BT_ID_MAX); for (; count < CONFIG_BT_ID_MAX; count++) { int err = bt_id_create(NULL, NULL); if (err < 0) { LOG_ERR("Cannot create identity (err:%d)", err); return err; } LOG_INF("Created identity idx=%u", (uint32_t)count); } return 0; } static void disconnect_le_conn_cb(struct bt_conn *conn, void *user_data) { (void)user_data; int err = bt_conn_disconnect(conn, BT_HCI_ERR_REMOTE_USER_TERM_CONN); if (!err) { LOG_INF("Disconnect LE peer for slot switch"); } else if (err == -ENOTCONN) { LOG_INF("LE peer already disconnected during slot switch"); } else { LOG_WRN("Failed to disconnect LE peer for slot switch err=%d", err); } } struct peer_bond_lookup { const bt_addr_le_t *peer_addr; bool found; }; static void peer_bond_lookup_cb(const struct bt_bond_info *info, void *user_data) { struct peer_bond_lookup *lookup = user_data; if (!bt_addr_le_cmp(&info->addr, lookup->peer_addr)) { lookup->found = true; } } static bool slot_has_peer_bond(uint8_t app_id, const bt_addr_le_t *peer_addr) { struct peer_bond_lookup lookup = { .peer_addr = peer_addr, }; bt_foreach_bond(get_bt_stack_peer_id(app_id), peer_bond_lookup_cb, &lookup); return lookup.found; } static bool find_peer_owner_slot(const bt_addr_le_t *peer_addr, uint8_t *owner_slot) { for (uint8_t slot = 0; slot < BLE_BOND_SLOT_COUNT; slot++) { if (slot_has_peer_bond(slot, peer_addr)) { *owner_slot = slot; return true; } } return false; } static int select_peer(uint8_t peer_id) { if (peer_id >= BLE_BOND_SLOT_COUNT) { return -EINVAL; } uint8_t previous_peer_id = bond.storage.cur_peer_id; if (bond.storage.cur_peer_id_valid && (previous_peer_id == peer_id)) { LOG_INF("Peer slot already selected: slot=%u stack_id=%u", peer_id, get_bt_stack_peer_id(peer_id)); return 0; } bond.storage.cur_peer_id = peer_id; bond.storage.cur_peer_id_valid = true; int err = store_peer_id(bond.storage.cur_peer_id); if (err) { LOG_ERR("Failed to store peer_id=%u (err:%d)", bond.storage.cur_peer_id, err); return err; } submit_peer_op_event(PEER_OPERATION_SELECTED, bond.storage.cur_peer_id); bt_conn_foreach(BT_CONN_TYPE_LE, disconnect_le_conn_cb, NULL); return 0; } static bool handle_ble_peer_event(const struct ble_peer_event *event) { if (event->state == PEER_STATE_CONNECTED) { const bt_addr_le_t *peer_addr = bt_conn_get_dst(event->id); uint8_t owner_slot; if (!peer_addr) { return false; } if (find_peer_owner_slot(peer_addr, &owner_slot) && (owner_slot != bond.storage.cur_peer_id)) { char addr_str[BT_ADDR_LE_STR_LEN]; int err; bt_addr_le_to_str(peer_addr, addr_str, sizeof(addr_str)); LOG_INF("Peer %s belongs to slot=%u, auto-switch from slot=%u", addr_str, owner_slot, bond.storage.cur_peer_id); bond.auto_switch_in_progress = true; err = select_peer(owner_slot); if (err) { bond.auto_switch_in_progress = false; LOG_ERR("Auto-switch to slot=%u failed err=%d", owner_slot, err); module_set_state(MODULE_STATE_ERROR); } } return false; } if (event->state == PEER_STATE_DISCONNECTED) { if (bond.auto_switch_in_progress) { bond.auto_switch_in_progress = false; LOG_INF("Auto-switch disconnect complete, waiting for reconnect on slot=%u", bond.storage.cur_peer_id); } return false; } if (event->state != PEER_STATE_SECURED) { return false; } struct bt_conn_info info; int err = bt_conn_get_info(event->id, &info); if (err) { LOG_ERR("Cannot get conn info for secured peer err=%d", err); module_set_state(MODULE_STATE_ERROR); return false; } uint8_t expected_stack_id = get_bt_stack_peer_id(bond.storage.cur_peer_id); if (info.id == expected_stack_id) { LOG_INF("Secured peer matches selected slot=%u stack_id=%u", bond.storage.cur_peer_id, expected_stack_id); return false; } LOG_INF("Disconnect peer on old id=%u expected=%u selected_slot=%u", info.id, expected_stack_id, bond.storage.cur_peer_id); err = bt_conn_disconnect(event->id, BT_HCI_ERR_REMOTE_USER_TERM_CONN); if (err && (err != -ENOTCONN)) { LOG_ERR("Cannot disconnect peer on old id err=%d", err); module_set_state(MODULE_STATE_ERROR); } return false; } static int erase_peer(uint8_t app_id) { uint8_t stack_id = get_bt_stack_peer_id(app_id); int err; /* Tell ble_adv to restart advertising session for this identity. */ submit_peer_op_event(PEER_OPERATION_ERASE_ADV, app_id); err = bt_unpair(stack_id, NULL); if (err) { LOG_ERR("Cannot unpair id %u (err:%d)", stack_id, err); return err; } err = bt_id_reset(stack_id, NULL, NULL); if (err < 0) { LOG_ERR("Cannot reset id %u (err:%d)", stack_id, err); return err; } submit_peer_op_event(PEER_OPERATION_ERASED, app_id); return 0; } static int erase_all_peers(void) { for (uint8_t i = 0; i < BLE_BOND_SLOT_COUNT; i++) { int err = erase_peer(i); if (err) { return err; } } return 0; } static bool handle_config_event(const struct config_event *event) { if (!event->is_request || (event->recipient != CFG_CHAN_RECIPIENT_LOCAL)) { return false; } if (MOD_FIELD_GET(event->event_id) != BLE_BOND_CFG_MODULE_ID) { return false; } struct config_event *rsp = new_config_event(0); rsp->transport_id = event->transport_id; rsp->is_request = false; rsp->event_id = event->event_id; rsp->recipient = event->recipient; rsp->status = CONFIG_STATUS_REJECT; if (event->status == CONFIG_STATUS_SET) { uint8_t opt_field = OPT_FIELD_GET(event->event_id); uint8_t opt_id = (opt_field == 0) ? UINT8_MAX : OPT_ID_GET(opt_field); switch (opt_id) { case BLE_BOND_CFG_PEER_SELECT: if (event->dyndata.size >= 1) { uint8_t peer_id = event->dyndata.data[0]; if (!select_peer(peer_id)) { rsp->status = CONFIG_STATUS_SUCCESS; } } break; case BLE_BOND_CFG_PEER_ERASE: if (!erase_peer(bond.storage.cur_peer_id)) { rsp->status = CONFIG_STATUS_SUCCESS; } break; case BLE_BOND_CFG_PEER_ERASE_ALL: if (!erase_all_peers()) { rsp->status = CONFIG_STATUS_SUCCESS; } break; default: break; } } else if (event->status == CONFIG_STATUS_FETCH) { uint8_t opt_field = OPT_FIELD_GET(event->event_id); uint8_t opt_id = (opt_field == 0) ? UINT8_MAX : OPT_ID_GET(opt_field); if (opt_id == BLE_BOND_CFG_PEER_SELECT) { struct config_event *rsp_data = new_config_event(1); rsp_data->transport_id = event->transport_id; rsp_data->is_request = false; rsp_data->event_id = event->event_id; rsp_data->recipient = event->recipient; rsp_data->status = CONFIG_STATUS_SUCCESS; rsp_data->dyndata.data[0] = bond.storage.cur_peer_id; APP_EVENT_SUBMIT(rsp_data); return true; } } APP_EVENT_SUBMIT(rsp); return true; } static int init_after_settings_loaded(void) { int err = load_identities(); if (err) { LOG_ERR("Identity initialization failed: %d", err); return err; } if (!storage_data_is_valid()) { LOG_WRN("Stored BLE bond data invalid, reinitializing defaults"); bond.storage.cur_peer_id = 0; bond.storage.cur_peer_id_valid = true; init_bt_stack_id_lut(); bond.storage.bt_stack_id_lut_valid = true; err = store_peer_id(bond.storage.cur_peer_id); if (err) { LOG_ERR("Failed to store peer_id=%u (err:%d)", bond.storage.cur_peer_id, err); return -EIO; } err = store_bt_stack_id_lut(); if (err) { LOG_ERR("Failed to store bt_stack_id_lut (err:%d)", err); return -EIO; } } bond.state = BLE_BOND_STATE_IDLE; LOG_INF("ble_bond init done: state=%s peer_id=%u stack_id=%u", state_name(bond.state), bond.storage.cur_peer_id, get_bt_stack_peer_id(bond.storage.cur_peer_id)); submit_peer_op_event(PEER_OPERATION_SELECTED, bond.storage.cur_peer_id); module_set_state(MODULE_STATE_READY); return 0; } static bool app_event_handler(const struct app_event_header *aeh) { if (is_module_state_event(aeh)) { const struct module_state_event *event = cast_module_state_event(aeh); if (check_state(event, MODULE_ID(settings_loader), MODULE_STATE_READY) && (bond.state == BLE_BOND_STATE_DISABLED)) { LOG_INF("settings_loader ready, starting ble_bond init"); int err = init_after_settings_loaded(); if (err) { LOG_ERR("ble_bond init failed (err:%d), state=%s", err, state_name(bond.state)); module_set_state(MODULE_STATE_ERROR); } } return false; } if (is_power_down_event(aeh)) { if (bond.state == BLE_BOND_STATE_IDLE) { bond.state = BLE_BOND_STATE_STANDBY; module_set_state(MODULE_STATE_OFF); } return false; } if (is_wake_up_event(aeh)) { if (bond.state == BLE_BOND_STATE_STANDBY) { bond.state = BLE_BOND_STATE_IDLE; module_set_state(MODULE_STATE_READY); submit_peer_op_event(PEER_OPERATION_SELECTED, bond.storage.cur_peer_id); } return false; } if (is_ble_peer_event(aeh)) { return handle_ble_peer_event(cast_ble_peer_event(aeh)); } if (is_config_event(aeh)) { return handle_config_event(cast_config_event(aeh)); } __ASSERT_NO_MSG(false); return false; } APP_EVENT_LISTENER(MODULE, app_event_handler); APP_EVENT_SUBSCRIBE(MODULE, module_state_event); APP_EVENT_SUBSCRIBE(MODULE, power_down_event); APP_EVENT_SUBSCRIBE(MODULE, wake_up_event); APP_EVENT_SUBSCRIBE(MODULE, ble_peer_event); APP_EVENT_SUBSCRIBE_EARLY(MODULE, config_event);