Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(split): allow central to connect to multiple peripherals #836

Merged
merged 1 commit into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions app/include/zmk/ble.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL))

#if ZMK_BLE_IS_CENTRAL
#define ZMK_BLE_PROFILE_COUNT (CONFIG_BT_MAX_PAIRED - 1)
#define ZMK_SPLIT_BLE_PERIPHERAL_COUNT 1
#define ZMK_BLE_PROFILE_COUNT (CONFIG_BT_MAX_PAIRED - CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS)
#define ZMK_SPLIT_BLE_PERIPHERAL_COUNT CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS
#else
#define ZMK_BLE_PROFILE_COUNT CONFIG_BT_MAX_PAIRED
#endif
Expand All @@ -34,5 +34,5 @@ char *zmk_ble_active_profile_name();
int zmk_ble_unpair_all();

#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
void zmk_ble_set_peripheral_addr(bt_addr_le_t *addr);
int zmk_ble_put_peripheral_addr(const bt_addr_le_t *addr);
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) */
54 changes: 39 additions & 15 deletions app/src/ble.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ static uint8_t passkey_digit = 0;

#endif /* IS_ENABLED(CONFIG_ZMK_BLE_PASSKEY_ENTRY) */

#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
#define PROFILE_COUNT (CONFIG_BT_MAX_PAIRED - 1)
#else
#define PROFILE_COUNT CONFIG_BT_MAX_PAIRED
#endif

enum advertising_type {
ZMK_ADV_NONE,
ZMK_ADV_DIR,
Expand Down Expand Up @@ -84,7 +78,7 @@ static const struct bt_data zmk_ble_ad[] = {

#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)

static bt_addr_le_t peripheral_addr;
static bt_addr_le_t peripheral_addrs[ZMK_SPLIT_BLE_PERIPHERAL_COUNT];

#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) */

Expand Down Expand Up @@ -283,9 +277,34 @@ char *zmk_ble_active_profile_name() { return profiles[active_profile].name; }

#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)

void zmk_ble_set_peripheral_addr(bt_addr_le_t *addr) {
memcpy(&peripheral_addr, addr, sizeof(bt_addr_le_t));
settings_save_one("ble/peripheral_address", addr, sizeof(bt_addr_le_t));
int zmk_ble_put_peripheral_addr(const bt_addr_le_t *addr) {
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
// If the address is recognized and already stored in settings, return
// index and no additional action is necessary.
if (!bt_addr_le_cmp(&peripheral_addrs[i], addr)) {
return i;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the recorded peripheral appears after slot i+1, and takes ith slot?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand what i+1 you are referring to.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the addr is actually recorded at some slot j, where j > i, but somehow the ith slot is open, so the addr takes away the ith slot as well.

This may be a rare case, and jth slot anyhow could be recycled for the addr should trigger disconnected function in normal cases.

Copy link
Contributor Author

@xudongzheng xudongzheng Sep 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slot 0 is filled, then slot 1 is filled, and so on. There is no code to delete just a single ble/peripheral_addresses/%d setting so it's not possible to have i open and j occupied where i < j.


// If the peripheral address slot is open, store new peripheral in the
// slot and return index. This compares against BT_ADDR_LE_ANY as that
// is the zero value.
if (!bt_addr_le_cmp(&peripheral_addrs[i], BT_ADDR_LE_ANY)) {
char addr_str[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));
LOG_DBG("Storing peripheral %s in slot %d", addr_str, i);
bt_addr_le_copy(&peripheral_addrs[i], addr);

char setting_name[32];
sprintf(setting_name, "ble/peripheral_addresses/%d", i);
settings_save_one(setting_name, addr, sizeof(bt_addr_le_t));

return i;
}
}

// The peripheral does not match a known peripheral and there is no
// available slot.
return -ENOMEM;
}

#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) */
Expand Down Expand Up @@ -340,15 +359,20 @@ static int ble_profiles_handle_set(const char *name, size_t len, settings_read_c
}
}
#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
else if (settings_name_steq(name, "peripheral_address", &next) && !next) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line will solve multiple problems i have currently with my split setup

else if (settings_name_steq(name, "peripheral_addresses", &next) && next) {
if (len != sizeof(bt_addr_le_t)) {
return -EINVAL;
}

int err = read_cb(cb_arg, &peripheral_addr, sizeof(bt_addr_le_t));
if (err <= 0) {
LOG_ERR("Failed to handle peripheral address from settings (err %d)", err);
return err;
int i = atoi(next);
if (i < 0 || i >= ZMK_SPLIT_BLE_PERIPHERAL_COUNT) {
LOG_ERR("Failed to store peripheral address in memory");
} else {
int err = read_cb(cb_arg, &peripheral_addrs[i], sizeof(bt_addr_le_t));
if (err <= 0) {
LOG_ERR("Failed to handle peripheral address from settings (err %d)", err);
return err;
}
}
}
#endif
Expand Down
4 changes: 4 additions & 0 deletions app/src/split/bluetooth/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ config ZMK_SPLIT_ROLE_CENTRAL

if ZMK_SPLIT_ROLE_CENTRAL

config ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS
int "Number of peripherals that will connect to the central."
default 1

config ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE
int "Max number of key position state events to queue when received from peripherals"
default 5
Expand Down
138 changes: 84 additions & 54 deletions app/src/split/bluetooth/central.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>

static int start_scan(void);
static int start_scanning(void);

#define POSITION_STATE_DATA_LEN 16

Expand All @@ -49,6 +49,8 @@ struct peripheral_slot {

static struct peripheral_slot peripherals[ZMK_SPLIT_BLE_PERIPHERAL_COUNT];

static bool is_scanning = false;

static const struct bt_uuid_128 split_service_uuid = BT_UUID_INIT_128(ZMK_SPLIT_BT_SERVICE_UUID);

K_MSGQ_DEFINE(peripheral_event_msgq, sizeof(struct zmk_position_state_changed),
Expand Down Expand Up @@ -130,8 +132,9 @@ int release_peripheral_slot(int index) {
return 0;
}

int reserve_peripheral_slot() {
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
int reserve_peripheral_slot(const bt_addr_le_t *addr) {
int i = zmk_ble_put_peripheral_addr(addr);
if (i >= 0) {
if (peripherals[i].state == PERIPHERAL_SLOT_STATE_OPEN) {
// Be sure the slot is fully reinitialized.
release_peripheral_slot(i);
Expand Down Expand Up @@ -344,9 +347,54 @@ static void split_central_process_connection(struct bt_conn *conn) {

LOG_DBG("New connection params: Interval: %d, Latency: %d, PHY: %d", info.le.interval,
info.le.latency, info.le.phy->rx_phy);

// Restart scanning if necessary.
start_scanning();
}

static int stop_scanning() {
LOG_DBG("Stopping peripheral scanning");
is_scanning = false;

int err = bt_le_scan_stop();
if (err < 0) {
LOG_ERR("Stop LE scan failed (err %d)", err);
return err;
}

return 0;
}

static bool split_central_eir_found(struct bt_data *data, void *user_data) {
static bool split_central_eir_found(const bt_addr_le_t *addr) {
LOG_DBG("Found the split service");

// Stop scanning so we can connect to the peripheral device.
int err = stop_scanning();
if (err < 0) {
return false;
}

int slot_idx = reserve_peripheral_slot(addr);
if (slot_idx < 0) {
LOG_ERR("Failed to reserve peripheral slot (err %d)", slot_idx);
return false;
}

struct peripheral_slot *slot = &peripherals[slot_idx];

LOG_DBG("Initiating new connnection");
struct bt_le_conn_param *param = BT_LE_CONN_PARAM(0x0006, 0x0006, 30, 400);
err = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN, param, &slot->conn);
if (err < 0) {
LOG_ERR("Create conn failed (err %d) (create conn? 0x%04x)", err, BT_HCI_OP_LE_CREATE_CONN);
release_peripheral_slot(slot_idx);
start_scanning();
}

return false;
}

static bool split_central_eir_parse(struct bt_data *data, void *user_data) {
bt_addr_le_t *addr = user_data;
int i;

Expand All @@ -361,9 +409,7 @@ static bool split_central_eir_found(struct bt_data *data, void *user_data) {
}

for (i = 0; i < data->data_len; i += 16) {
struct bt_le_conn_param *param;
struct bt_uuid_128 uuid;
int err;

if (!bt_uuid_create(&uuid.uuid, &data->data[i], 16)) {
LOG_ERR("Unable to load UUID");
Expand All @@ -381,46 +427,7 @@ static bool split_central_eir_found(struct bt_data *data, void *user_data) {
continue;
}

LOG_DBG("Found the split service");

zmk_ble_set_peripheral_addr(addr);

err = bt_le_scan_stop();
if (err) {
LOG_ERR("Stop LE scan failed (err %d)", err);
continue;
}

uint8_t slot_idx = reserve_peripheral_slot();
if (slot_idx < 0) {
LOG_ERR("Faild to reserve peripheral slot (err %d)", slot_idx);
continue;
}

struct peripheral_slot *slot = &peripherals[slot_idx];

slot->conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, addr);
if (slot->conn) {
LOG_DBG("Found existing connection");
split_central_process_connection(slot->conn);
err = bt_conn_le_phy_update(slot->conn, BT_CONN_LE_PHY_PARAM_2M);
if (err) {
LOG_ERR("Update phy conn failed (err %d)", err);
}
} else {
param = BT_LE_CONN_PARAM(0x0006, 0x0006, 30, 400);

LOG_DBG("Initiating new connnection");

err = bt_conn_le_create(addr, BT_CONN_LE_CREATE_CONN, param, &slot->conn);
if (err) {
LOG_ERR("Create conn failed (err %d) (create conn? 0x%04x)", err,
BT_HCI_OP_LE_CREATE_CONN);
start_scan();
}
}

return false;
return split_central_eir_found(addr);
}
}

Expand All @@ -436,15 +443,34 @@ static void split_central_device_found(const bt_addr_le_t *addr, int8_t rssi, ui

/* We're only interested in connectable events */
if (type == BT_GAP_ADV_TYPE_ADV_IND || type == BT_GAP_ADV_TYPE_ADV_DIRECT_IND) {
bt_data_parse(ad, split_central_eir_found, (void *)addr);
bt_data_parse(ad, split_central_eir_parse, (void *)addr);
}
}

static int start_scan(void) {
int err;
static int start_scanning(void) {
// No action is necessary if central is already scanning.
if (is_scanning) {
LOG_DBG("Scanning already running");
return 0;
}

err = bt_le_scan_start(BT_LE_SCAN_PASSIVE, split_central_device_found);
if (err) {
// If all the devices are connected, there is no need to scan.
bool has_unconnected = false;
for (int i = 0; i < CONFIG_ZMK_SPLIT_BLE_CENTRAL_PERIPHERALS; i++) {
if (peripherals[i].conn == NULL) {
has_unconnected = true;
break;
}
}
if (!has_unconnected) {
LOG_DBG("All devices are connected, scanning is unnecessary");
return 0;
}

// Start scanning otherwise.
is_scanning = true;
int err = bt_le_scan_start(BT_LE_SCAN_PASSIVE, split_central_device_found);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for now, but ideally a future enhancement will add a backoff on scanning, since multi-peripheral setups are more likely to have one of them only on some of the time.

Copy link

@bezmi bezmi Nov 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm exploring this,

bt_le_scan_start(&test_scan_params, split_central_device_found)

where,

static struct bt_le_scan_param test_scan_params = {
    .type = BT_LE_SCAN_TYPE_PASSIVE,
    .options = BT_LE_SCAN_OPT_FILTER_DUPLICATE,
    .interval = BT_GAP_SCAN_FAST_INTERVAL,
    .window = BT_GAP_SCAN_FAST_WINDOW,
    .timeout = 1500,
    .interval_coded = 0,
    .window_coded = 0,
};

Setting any timeout > 0 will cause zmk to hang. .timeout = 0 will work just fine,

[00:00:00.770,935] <dbg> zmk: zmk_usb_get_conn_state: state: 1
[00:00:00.770,965] <dbg> zmk: get_selected_endpoint: No endpoints are ready.
[00:00:01.081,146] <inf> usb_hid: Device configured
[00:00:01.081,207] <dbg> zmk: zmk_usb_get_conn_state: state: 3
[00:00:01.081,237] <dbg> zmk: zmk_usb_get_conn_state: state: 3
[00:00:01.081,237] <dbg> zmk: get_selected_endpoint: Only USB is ready.
[00:00:01.081,237] <dbg> zmk: zmk_endpoints_send_report: usage page 0x07
[00:00:01.081,268] <dbg> zmk: zmk_endpoints_send_report: usage page 0x0C
[00:00:01.081,298] <inf> zmk: Endpoint changed: 0
[00:00:01.081,359] <dbg> zmk: destination_connection: Address pointer 0x20008fdf
[00:00:01.081,359] <wrn> zmk: Not sending, no active address for current profile
[00:00:01.081,390] <dbg> zmk: destination_connection: Address pointer 0x20008fdf
[00:00:01.081,390] <wrn> zmk: Not sending, no active address for current profile

Any ideas? I was hoping to register scan callbacks where after the 15s timeout, scanning is restarted with new params with a longer interval and window and an indefinite timeout.

if (err < 0) {
LOG_ERR("Scanning failed to start (err %d)", err);
return err;
}
Expand All @@ -471,7 +497,7 @@ static void split_central_connected(struct bt_conn *conn, uint8_t conn_err) {

release_peripheral_slot_for_conn(conn);

start_scan();
start_scanning();
return;
}

Expand All @@ -495,7 +521,7 @@ static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) {
return;
}

start_scan();
start_scanning();
}

static struct bt_conn_cb conn_callbacks = {
Expand Down Expand Up @@ -527,6 +553,10 @@ void split_central_split_run_callback(struct k_work *work) {
LOG_ERR("Source not connected");
continue;
}
if (!peripherals[payload_wrapper.source].run_behavior_handle) {
LOG_ERR("Run behavior handle not found");
continue;
}

int err = bt_gatt_write_without_response(
peripherals[payload_wrapper.source].conn,
Expand Down Expand Up @@ -590,7 +620,7 @@ int zmk_split_bt_central_init(const struct device *_arg) {
CONFIG_ZMK_BLE_THREAD_PRIORITY, NULL);
bt_conn_cb_register(&conn_callbacks);

return start_scan();
return start_scanning();
}

SYS_INIT(zmk_split_bt_central_init, APPLICATION, CONFIG_ZMK_BLE_INIT_PRIORITY);