Skip to content

Commit

Permalink
feat(split): sync central & peripherals last activity timing
Browse files Browse the repository at this point in the history
Sync central last activity timings to all devices, by having the
central emit how long it's inactive for, and the peripheral(s) using it
to adjust the local last activity time.

Prior to this commit, key presses on a peripheral keeps the central
awake, but not vice versa. With this commit, key presses on the central
(or hypothetical peripheral B) can keep peripheral A awake.

This is done by:
1. Adding a new `SYNC_ACTIVITY` GATT characteristic for central to sync
it's inactive timer to the other peripheral(s).
2. Central's `activity.c` broadcasting the inactive time at a regular
`ZMK_SPLIT_SYNC_SLEEP_TIMERS_INTERVAL_MS` interval
3. Peripheral's `service.c` receiving the data and sending it to its
`activity.c`
4. Peripheral's `activity.c` determining the new `activity_last_uptime`
value using the inactive duration as a relative difference.

Additionally:
- `central.c` is updated to sync the timers upon a new BLE connection,
so that new devices don't need to wait the full interval to get sync-ed.
- If a peripheral is in IDLE mode when the central comes online, it'll
get sync-ed to ACTIVE as well.

Cons:
- This solution doesn't handle cases where the peripheral(s) is already
in deep sleep, since BLE is turned off. However, the sync-ing can
prevent peripheral(s) from going into deep sleep if the central is used.
- This is a time based sync, so it won't be as accurate as event based
where each key press is sync-ed to the peripheral(s), but that would
take up more of the channel bandwidth.
- If `ZMK_SPLIT_SYNC_SLEEP_TIMERS_INTERVAL_MS` > `ZMK_IDLE_TIMEOUT`,
there can be situations where the peripheral(s) goes into IDLE before
receiving the next sync from central. The original default interval was
5 minutes, primarily to prevent unnecessary deep sleeps, but it would
run into the above case.
  • Loading branch information
angweekiat committed Sep 5, 2024
1 parent 0f972f1 commit 09ddf9b
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 3 deletions.
1 change: 1 addition & 0 deletions app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ target_sources_ifdef(CONFIG_ZMK_BATTERY_REPORTING app PRIVATE src/battery.c)
target_sources_ifdef(CONFIG_ZMK_HID_INDICATORS app PRIVATE src/events/hid_indicators_changed.c)

target_sources_ifdef(CONFIG_ZMK_SPLIT app PRIVATE src/events/split_peripheral_status_changed.c)
target_sources_ifdef(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING app PRIVATE src/events/sync_activity_event.c)
add_subdirectory(src/split)

target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/usb.c)
Expand Down
16 changes: 16 additions & 0 deletions app/include/zmk/events/sync_activity_event.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#pragma once

#include <zmk/hid_indicators_types.h>
#include <zmk/event_manager.h>

struct zmk_sync_activity_event {
int32_t central_inactive_duration;
};

ZMK_EVENT_DECLARE(zmk_sync_activity_event);
8 changes: 7 additions & 1 deletion app/include/zmk/split/bluetooth/central.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,10 @@ int zmk_split_bt_update_hid_indicator(zmk_hid_indicators_t indicators);

int zmk_split_get_peripheral_battery_level(uint8_t source, uint8_t *level);

#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)

#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)

int zmk_split_bt_sync_activity(int32_t inactive_duration);

#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
1 change: 1 addition & 0 deletions app/include/zmk/split/bluetooth/uuid.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
#define ZMK_SPLIT_BT_CHAR_RUN_BEHAVIOR_UUID ZMK_BT_SPLIT_UUID(0x00000002)
#define ZMK_SPLIT_BT_CHAR_SENSOR_STATE_UUID ZMK_BT_SPLIT_UUID(0x00000003)
#define ZMK_SPLIT_BT_UPDATE_HID_INDICATORS_UUID ZMK_BT_SPLIT_UUID(0x00000004)
#define ZMK_SPLIT_BT_CHAR_SYNC_ACTIVITY_UUID ZMK_BT_SPLIT_UUID(0x00000005)
49 changes: 47 additions & 2 deletions app/src/activity.c
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/events/activity_state_changed.h>
#include <zmk/events/position_state_changed.h>
#include <zmk/events/sensor_event.h>
#include <zmk/events/sync_activity_event.h>

#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
#include <zmk/split/bluetooth/central.h>
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)

#include <zmk/pm.h>

Expand All @@ -37,6 +42,12 @@ bool is_usb_power_present(void) {
static enum zmk_activity_state activity_state;

static uint32_t activity_last_uptime;
#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING) && \
IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
static uint32_t last_sync_time;
#define SLEEP_TIMERS_SYNC_MS CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_INTERVAL_MS
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING) &&
// IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)

#define MAX_IDLE_MS CONFIG_ZMK_IDLE_TIMEOUT

Expand Down Expand Up @@ -68,6 +79,7 @@ int activity_event_listener(const zmk_event_t *eh) {
void activity_work_handler(struct k_work *work) {
int32_t current = k_uptime_get();
int32_t inactive_time = current - activity_last_uptime;

#if IS_ENABLED(CONFIG_ZMK_SLEEP)
if (inactive_time > MAX_SLEEP_MS && !is_usb_power_present()) {
// Put devices in suspend power mode before sleeping
Expand All @@ -83,8 +95,17 @@ void activity_work_handler(struct k_work *work) {
} else
#endif /* IS_ENABLED(CONFIG_ZMK_SLEEP) */
if (inactive_time > MAX_IDLE_MS) {
set_state(ZMK_ACTIVITY_IDLE);
}
set_state(ZMK_ACTIVITY_IDLE);
}

#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING) && \
IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
if (current - last_sync_time > SLEEP_TIMERS_SYNC_MS) {
last_sync_time = current;
zmk_split_bt_sync_activity(inactive_time);
}
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING) &&
// IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
}

K_WORK_DEFINE(activity_work, activity_work_handler);
Expand All @@ -104,4 +125,28 @@ ZMK_LISTENER(activity, activity_event_listener);
ZMK_SUBSCRIPTION(activity, zmk_position_state_changed);
ZMK_SUBSCRIPTION(activity, zmk_sensor_event);

#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING) && \
!IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
int sync_activity_event_listener(const zmk_event_t *eh) {
struct zmk_sync_activity_event *ev = as_zmk_sync_activity_event(eh);
if (ev == NULL) {
LOG_ERR("Invalid event type");
return -ENOTSUP;
}
int32_t central_activity_last_uptime = k_uptime_get() - ev->central_inactive_duration;
activity_last_uptime = central_activity_last_uptime;
int32_t new_inactive_time = k_uptime_get() - activity_last_uptime;

if (activity_state == ZMK_ACTIVITY_IDLE && new_inactive_time < MAX_IDLE_MS) {
LOG_DBG("Syncing state to active to match central device.");
return set_state(ZMK_ACTIVITY_ACTIVE);
}
return 0;
}

ZMK_LISTENER(sync_activity, sync_activity_event_listener);
ZMK_SUBSCRIPTION(sync_activity, zmk_sync_activity_event);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING) &&
// !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)

SYS_INIT(activity_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
10 changes: 10 additions & 0 deletions app/src/events/sync_activity_event.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#include <zephyr/kernel.h>
#include <zmk/events/sync_activity_event.h>

ZMK_EVENT_IMPL(zmk_sync_activity_event);
15 changes: 15 additions & 0 deletions app/src/split/Kconfig
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ config ZMK_SPLIT_PERIPHERAL_HID_INDICATORS
help
Enable propagating the HID (LED) Indicator state to the split peripheral(s).

config ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING
bool "Sync last activity timing across all devices"
default n
help
Sync central device last activity timing to the split peripheral(s).
Does not help to wake up peripheral devices that have gone to deep sleep.

if ZMK_SPLIT_ROLE_CENTRAL && ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING
config ZMK_SPLIT_SYNC_LAST_ACTIVITY_INTERVAL_MS
int "Last activity time sync interval in milliseconds"
default 30000

#ZMK_SPLIT_ROLE_CENTRAL && ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING
endif

#ZMK_SPLIT
endif

Expand Down
66 changes: 66 additions & 0 deletions app/src/split/bluetooth/central.c
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ struct peripheral_slot {
struct bt_gatt_subscribe_params sensor_subscribe_params;
struct bt_gatt_discover_params sub_discover_params;
uint16_t run_behavior_handle;
#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
uint16_t sync_activity_handle;
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
struct bt_gatt_subscribe_params batt_lvl_subscribe_params;
struct bt_gatt_read_params batt_lvl_read_params;
Expand All @@ -66,6 +69,11 @@ static bool is_scanning = false;

static const struct bt_uuid_128 split_service_uuid = BT_UUID_INIT_128(ZMK_SPLIT_BT_SERVICE_UUID);

#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
static int32_t activity_inactive_duration;
static void split_central_sync_activity_with_delay();
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)

K_MSGQ_DEFINE(peripheral_event_msgq, sizeof(struct zmk_position_state_changed),
CONFIG_ZMK_SPLIT_BLE_CENTRAL_POSITION_QUEUE_SIZE, 4);

Expand Down Expand Up @@ -144,6 +152,9 @@ int release_peripheral_slot(int index) {
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
slot->update_hid_indicators = 0;
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
slot->sync_activity_handle = 0;
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)

return 0;
}
Expand Down Expand Up @@ -465,6 +476,13 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,
slot->batt_lvl_read_params.single.offset = 0;
bt_gatt_read(conn, &slot->batt_lvl_read_params);
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */
#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
} else if (!bt_uuid_cmp(chrc_uuid, BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_SYNC_ACTIVITY_UUID))) {
LOG_DBG("Found sync activity handle");
slot->discover_params.uuid = NULL;
slot->discover_params.start_handle = attr->handle + 2;
slot->sync_activity_handle = bt_gatt_attr_value_handle(attr);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
}

bool subscribed = slot->run_behavior_handle && slot->subscribe_params.value_handle;
Expand All @@ -476,6 +494,9 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
subscribed = subscribed && slot->update_hid_indicators;
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
subscribed = subscribed && slot->sync_activity_handle;
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
subscribed = subscribed && slot->batt_lvl_subscribe_params.value_handle;
#endif /* IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) */
Expand Down Expand Up @@ -713,6 +734,10 @@ static void split_central_connected(struct bt_conn *conn, uint8_t conn_err) {

confirm_peripheral_slot_conn(conn);
split_central_process_connection(conn);

#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
split_central_sync_activity_with_delay();
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
}

static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) {
Expand Down Expand Up @@ -866,6 +891,47 @@ int zmk_split_bt_update_hid_indicator(zmk_hid_indicators_t indicators) {

#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)

#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)

static void split_central_sync_activity_callback(struct k_work *work) {
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
if (peripherals[i].state != PERIPHERAL_SLOT_STATE_CONNECTED ||
peripherals[i].sync_activity_handle == 0) {
continue;
}

int err = bt_gatt_write_without_response(
peripherals[i].conn, peripherals[i].sync_activity_handle, &activity_inactive_duration,
sizeof(activity_inactive_duration), true);

if (err) {
LOG_ERR("Failed to sync activity state (err %d)", err);
}
}
}

static K_WORK_DEFINE(split_central_sync_activity, split_central_sync_activity_callback);

void split_central_sync_activity_delay_timer_callback(struct k_timer *_timer) {
k_timer_stop(_timer);
k_work_submit_to_queue(&split_central_split_run_q, &split_central_sync_activity);
}
K_TIMER_DEFINE(split_central_sync_activity_delay_timer,
split_central_sync_activity_delay_timer_callback, NULL);

static void split_central_sync_activity_with_delay() {
// Bluetooth discovery is done only after connection, so a delay is added here to compensate
// for that, before syncing the sleep timers
k_timer_start(&split_central_sync_activity_delay_timer, K_SECONDS(1), K_SECONDS(1));
}

int zmk_split_bt_sync_activity(int32_t inactive_duration) {
activity_inactive_duration = inactive_duration;
return k_work_submit_to_queue(&split_central_split_run_q, &split_central_sync_activity);
}

#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)

static int finish_init() {
return IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) ? 0 : start_scanning();
}
Expand Down
32 changes: 32 additions & 0 deletions app/src/split/bluetooth/service.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#include <zmk/events/hid_indicators_changed.h>
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
#include <zmk/events/sync_activity_event.h>
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)

#include <zmk/events/sensor_event.h>
#include <zmk/sensors.h>
Expand Down Expand Up @@ -138,6 +141,30 @@ static ssize_t split_svc_update_indicators(struct bt_conn *conn, const struct bt

#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)

#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
static int32_t central_inactive_duration;

static void split_svc_sync_activity_callback(struct k_work *work) {
raise_zmk_sync_activity_event(
(struct zmk_sync_activity_event){.central_inactive_duration = central_inactive_duration});
}

static K_WORK_DEFINE(split_svc_sync_activity_work, split_svc_sync_activity_callback);

static ssize_t split_svc_sync_activity(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset,
uint8_t flags) {
if (offset + len > sizeof(int32_t)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}

memcpy((uint8_t *)&central_inactive_duration + offset, buf, len);
k_work_submit(&split_svc_sync_activity_work);

return len;
}
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)

BT_GATT_SERVICE_DEFINE(
split_svc, BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_SERVICE_UUID)),
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_POSITION_STATE_UUID),
Expand All @@ -160,6 +187,11 @@ BT_GATT_SERVICE_DEFINE(
BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE_ENCRYPT, NULL,
split_svc_update_indicators, NULL),
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_CHAR_SYNC_ACTIVITY_UUID),
BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE_ENCRYPT, NULL,
split_svc_sync_activity, NULL),
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING)
);

K_THREAD_STACK_DEFINE(service_q_stack, CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE);
Expand Down

0 comments on commit 09ddf9b

Please sign in to comment.