diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index fd4b7ab5519..162f2b59a4e 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -94,6 +94,8 @@ 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_PERIODIC app PRIVATE src/events/sync_activity_event.c) +target_sources_ifdef(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT app PRIVATE src/events/sync_activity_event.c) add_subdirectory(src/split) target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/usb.c) diff --git a/app/include/zmk/events/sync_activity_event.h b/app/include/zmk/events/sync_activity_event.h new file mode 100644 index 00000000000..18d48095828 --- /dev/null +++ b/app/include/zmk/events/sync_activity_event.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +struct zmk_sync_activity_event { + int32_t central_inactive_duration; +}; + +ZMK_EVENT_DECLARE(zmk_sync_activity_event); diff --git a/app/include/zmk/split/bluetooth/central.h b/app/include/zmk/split/bluetooth/central.h index 5e9e09ff6a1..addaa87677c 100644 --- a/app/include/zmk/split/bluetooth/central.h +++ b/app/include/zmk/split/bluetooth/central.h @@ -21,4 +21,12 @@ 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) \ No newline at end of file +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) || \ + IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) + +int zmk_split_bt_queue_sync_activity(int32_t inactive_duration); + +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) || + // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) \ No newline at end of file diff --git a/app/include/zmk/split/bluetooth/uuid.h b/app/include/zmk/split/bluetooth/uuid.h index dccdfc804c5..07da04df4a6 100644 --- a/app/include/zmk/split/bluetooth/uuid.h +++ b/app/include/zmk/split/bluetooth/uuid.h @@ -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) diff --git a/app/src/activity.c b/app/src/activity.c index 454e91e5da0..2087c0fb3fc 100644 --- a/app/src/activity.c +++ b/app/src/activity.c @@ -17,6 +17,13 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) || \ + IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) +#include +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) || + // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) #include @@ -38,6 +45,20 @@ static enum zmk_activity_state activity_state; static uint32_t activity_last_uptime; +#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) +static uint32_t last_periodic_sync_time; +#define PERIODIC_SYNC_INTERVAL_MS CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC_INTERVAL_MS +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) +static uint32_t last_event_sync_time; +#define EVENT_SYNC_MIN_INTERVAL_MS CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_EVENT_MIN_INTERVAL +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) + +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + #define MAX_IDLE_MS CONFIG_ZMK_IDLE_TIMEOUT #if IS_ENABLED(CONFIG_ZMK_SLEEP) @@ -62,6 +83,16 @@ enum zmk_activity_state zmk_activity_get_state(void) { return activity_state; } int activity_event_listener(const zmk_event_t *eh) { activity_last_uptime = k_uptime_get(); +#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) && \ + IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) + if (activity_last_uptime - last_event_sync_time > EVENT_SYNC_MIN_INTERVAL_MS) { + LOG_DBG("Refresh %d", activity_last_uptime - last_event_sync_time); + last_event_sync_time = activity_last_uptime; + zmk_split_bt_queue_sync_activity(0); + } +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) && + // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) + return set_state(ZMK_ACTIVITY_ACTIVE); } @@ -85,6 +116,15 @@ void activity_work_handler(struct k_work *work) { if (inactive_time > MAX_IDLE_MS) { set_state(ZMK_ACTIVITY_IDLE); } + +#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) && \ + IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) + if (current - last_periodic_sync_time > PERIODIC_SYNC_INTERVAL_MS) { + last_periodic_sync_time = current; + zmk_split_bt_queue_sync_activity(inactive_time); + } +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) && + // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) } K_WORK_DEFINE(activity_work, activity_work_handler); @@ -104,4 +144,32 @@ 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_ROLE_CENTRAL) && \ + (IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) || \ + IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT)) + +int sync_activity_event_listener(const zmk_event_t *eh) { + int32_t current = k_uptime_get(); + + struct zmk_sync_activity_event *ev = as_zmk_sync_activity_event(eh); + if (ev == NULL) { + LOG_ERR("Invalid event type"); + return -ENOTSUP; + } + + activity_last_uptime = current - ev->central_inactive_duration; + + if (activity_state == ZMK_ACTIVITY_IDLE && ev->central_inactive_duration < 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_ROLE_CENTRAL) && + // (IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) || + // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT)) + SYS_INIT(activity_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/src/events/sync_activity_event.c b/app/src/events/sync_activity_event.c new file mode 100644 index 00000000000..2acc80f5ea3 --- /dev/null +++ b/app/src/events/sync_activity_event.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_sync_activity_event); \ No newline at end of file diff --git a/app/src/split/Kconfig b/app/src/split/Kconfig index ce90037b1ea..e10aff306ef 100644 --- a/app/src/split/Kconfig +++ b/app/src/split/Kconfig @@ -26,6 +26,28 @@ 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_PERIODIC + bool "Sync last activity timing across all devices periodically" + default n + help + Sync central device last activity timing to the split peripheral(s) with a periodic interval. + Does not help to wake up peripheral devices that have gone to deep sleep. + +config ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC_INTERVAL_MS + int "Last activity time periodic sync interval in milliseconds" + default 30000 + +config ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT + bool "Sync last activity timing across all devices upon central activity event" + default n + help + Sync central device last activity timing to the split peripheral(s) when an event (key press/sensor) + is detected. Does not help to wake up peripheral devices that have gone to deep sleep. + +config ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_EVENT_MIN_INTERVAL + int "Sync timings on events (key/sensors presses) as well, 0 to disable. Represents minimum interval in milliseconds" + default 1000 + #ZMK_SPLIT endif diff --git a/app/src/split/bluetooth/central.c b/app/src/split/bluetooth/central.c index 0f4cd78b531..398fc7620a9 100644 --- a/app/src/split/bluetooth/central.c +++ b/app/src/split/bluetooth/central.c @@ -35,6 +35,12 @@ static int start_scanning(void); #define POSITION_STATE_DATA_LEN 16 +#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) || \ + IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) +#define SYNC_LAST_ACTIVITY_TIMING 1 +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) || + // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) + enum peripheral_slot_state { PERIPHERAL_SLOT_STATE_OPEN, PERIPHERAL_SLOT_STATE_CONNECTING, @@ -49,6 +55,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(SYNC_LAST_ACTIVITY_TIMING) + uint16_t sync_activity_handle; +#endif // IS_ENABLED(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; @@ -66,6 +75,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(SYNC_LAST_ACTIVITY_TIMING) +static int32_t activity_inactive_duration; +static void split_central_sync_activity_with_delay(); +#endif // IS_ENABLED(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); @@ -144,6 +158,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(SYNC_LAST_ACTIVITY_TIMING) + slot->sync_activity_handle = 0; +#endif // IS_ENABLED(SYNC_LAST_ACTIVITY_TIMING) return 0; } @@ -465,6 +482,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(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(SYNC_LAST_ACTIVITY_TIMING) } bool subscribed = slot->run_behavior_handle && slot->subscribe_params.value_handle; @@ -476,6 +500,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(SYNC_LAST_ACTIVITY_TIMING) + subscribed = subscribed && slot->sync_activity_handle; +#endif // IS_ENABLED(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) */ @@ -713,6 +740,12 @@ 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(SYNC_LAST_ACTIVITY_TIMING) + // Bluetooth discovery is done only after connection, so a delay is + /// added here to compensate for that before syncing the activity time + split_central_sync_activity_with_delay(); +#endif // IS_ENABLED(SYNC_LAST_ACTIVITY_TIMING) } static void split_central_disconnected(struct bt_conn *conn, uint8_t reason) { @@ -866,6 +899,45 @@ int zmk_split_bt_update_hid_indicator(zmk_hid_indicators_t indicators) { #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) +#if IS_ENABLED(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() { + k_timer_start(&split_central_sync_activity_delay_timer, K_SECONDS(1), K_SECONDS(1)); +} + +int zmk_split_bt_queue_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(SYNC_LAST_ACTIVITY_TIMING) + static int finish_init() { return IS_ENABLED(CONFIG_ZMK_BLE_CLEAR_BONDS_ON_START) ? 0 : start_scanning(); } diff --git a/app/src/split/bluetooth/service.c b/app/src/split/bluetooth/service.c index 505eb363cd8..4015dd791e3 100644 --- a/app/src/split/bluetooth/service.c +++ b/app/src/split/bluetooth/service.c @@ -26,6 +26,13 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) +#if IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) || \ + IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) +#include +#define SYNC_LAST_ACTIVITY_TIMING 1 +#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_PERIODIC) || + // IS_ENABLED(CONFIG_ZMK_SPLIT_SYNC_LAST_ACTIVITY_TIMING_ON_EVENT) + #include #include @@ -138,6 +145,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(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 *)¢ral_inactive_duration + offset, buf, len); + k_work_submit(&split_svc_sync_activity_work); + + return len; +} +#endif // IS_ENABLED(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), @@ -160,6 +191,13 @@ 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(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(SYNC_LAST_ACTIVITY_TIMING) + ); K_THREAD_STACK_DEFINE(service_q_stack, CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE);