From 7b84dc0db8dab0c68726d09dba8a89ea41058ce4 Mon Sep 17 00:00:00 2001 From: Max Starikov Date: Wed, 11 Feb 2026 20:09:04 +0100 Subject: [PATCH] add ble exposition --- config/boards/shields/corney/corney.conf | 3 + docs/gatt-layer-exposition.md | 8 ++ .../add-gatt-layer-exposition/tasks.md | 8 +- zephyr/CMakeLists.txt | 3 + zephyr/Kconfig | 8 ++ zephyr/module.yml | 2 + zephyr/src/gatt_layer_exposition.c | 86 +++++++++++++++++++ 7 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 zephyr/CMakeLists.txt create mode 100644 zephyr/Kconfig create mode 100644 zephyr/src/gatt_layer_exposition.c diff --git a/config/boards/shields/corney/corney.conf b/config/boards/shields/corney/corney.conf index 5e35c54..db03086 100644 --- a/config/boards/shields/corney/corney.conf +++ b/config/boards/shields/corney/corney.conf @@ -8,4 +8,7 @@ CONFIG_ZMK_MOUSE_TICK_DURATION=8 # Optional: Adjust mouse movement speed (default is 500) CONFIG_ZMK_MOUSE_DEFAULT_SPEED=500 +# Expose active layer over GATT +CONFIG_ZMK_GATT_LAYER_EXPOSITION=y + ZMK_KEYBOARD_NAME="Corney" diff --git a/docs/gatt-layer-exposition.md b/docs/gatt-layer-exposition.md index 6212267..976c5f9 100644 --- a/docs/gatt-layer-exposition.md +++ b/docs/gatt-layer-exposition.md @@ -14,3 +14,11 @@ This document defines the custom GATT service and characteristic used to expose ## Properties - Read - Notify + +## Configuration +- Kconfig: `CONFIG_ZMK_GATT_LAYER_EXPOSITION` +- This repo enables the feature in `config/boards/shields/corney/corney.conf`. + +## Validation +- Connect with a BLE GATT browser and read the Layer Number characteristic to confirm it returns the active layer index. +- Subscribe to notifications, switch layers on the keyboard, and verify notifications contain the updated layer index. diff --git a/openspec/changes/add-gatt-layer-exposition/tasks.md b/openspec/changes/add-gatt-layer-exposition/tasks.md index 97c6119..afb7690 100644 --- a/openspec/changes/add-gatt-layer-exposition/tasks.md +++ b/openspec/changes/add-gatt-layer-exposition/tasks.md @@ -1,6 +1,6 @@ ## 1. Implementation - [x] 1.1 Define the GATT service/characteristic UUIDs and data format for layer number exposure -- [ ] 1.2 Add a read characteristic that returns the current active layer number -- [ ] 1.3 Add notify support and emit updates on layer change when clients subscribe -- [ ] 1.4 Add configuration and documentation for enabling/disabling the feature -- [ ] 1.5 Add tests or validation steps for read/notify behavior +- [x] 1.2 Add a read characteristic that returns the current active layer number +- [x] 1.3 Add notify support and emit updates on layer change when clients subscribe +- [x] 1.4 Add configuration and documentation for enabling/disabling the feature +- [x] 1.5 Add tests or validation steps for read/notify behavior diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt new file mode 100644 index 0000000..2b62400 --- /dev/null +++ b/zephyr/CMakeLists.txt @@ -0,0 +1,3 @@ +zephyr_library() +zephyr_library_sources_ifdef(CONFIG_ZMK_GATT_LAYER_EXPOSITION src/gatt_layer_exposition.c) + diff --git a/zephyr/Kconfig b/zephyr/Kconfig new file mode 100644 index 0000000..8613c38 --- /dev/null +++ b/zephyr/Kconfig @@ -0,0 +1,8 @@ +menuconfig ZMK_GATT_LAYER_EXPOSITION + bool "Expose active layer number over GATT" + depends on ZMK_BLE && BT_GATT + help + Adds a custom GATT service/characteristic that exposes the currently + active layer number as a readable byte and optionally notifies on + layer changes. + diff --git a/zephyr/module.yml b/zephyr/module.yml index 1cc2b35..1a77206 100644 --- a/zephyr/module.yml +++ b/zephyr/module.yml @@ -1,3 +1,5 @@ build: + cmake: zephyr + kconfig: zephyr/Kconfig settings: board_root: . diff --git a/zephyr/src/gatt_layer_exposition.c b/zephyr/src/gatt_layer_exposition.c new file mode 100644 index 0000000..e1c2a6d --- /dev/null +++ b/zephyr/src/gatt_layer_exposition.c @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2026 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include +#include +#include +#include + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#define ZMK_GATT_LAYER_SERVICE_UUID \ + BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x715d81e1, 0x377d, 0x4f26, 0xa678, 0xa506675d99ec)) + +#define ZMK_GATT_LAYER_CHAR_UUID \ + BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0xe87c518a, 0xf323, 0x4dd7, 0x9dd5, 0x0991add1c01b)) + +enum { + LAYER_SVC_ATTR_PRIMARY, + LAYER_SVC_ATTR_CHAR_DECL, + LAYER_SVC_ATTR_CHAR_VALUE, + LAYER_SVC_ATTR_CHAR_CCC, + LAYER_SVC_ATTR_COUNT, +}; + +static bool layer_notify_enabled; +static uint8_t last_layer_index = UINT8_MAX; + +static uint8_t current_layer_index(void) { + return (uint8_t)zmk_keymap_highest_layer_active(); +} + +static ssize_t read_layer(struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, + uint16_t len, uint16_t offset) { + uint8_t layer = current_layer_index(); + return bt_gatt_attr_read(conn, attr, buf, len, offset, &layer, sizeof(layer)); +} + +static void layer_ccc_cfg_changed(const struct bt_gatt_attr *attr, uint16_t value) { + ARG_UNUSED(attr); + layer_notify_enabled = (value == BT_GATT_CCC_NOTIFY); +} + +BT_GATT_SERVICE_DEFINE(layer_svc, BT_GATT_PRIMARY_SERVICE(ZMK_GATT_LAYER_SERVICE_UUID), + BT_GATT_CHARACTERISTIC(ZMK_GATT_LAYER_CHAR_UUID, + BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, + BT_GATT_PERM_READ, read_layer, NULL, NULL), + BT_GATT_CCC(layer_ccc_cfg_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE)); + +static void notify_layer_change(uint8_t layer_index) { + if (!layer_notify_enabled) { + return; + } + + int err = bt_gatt_notify(NULL, &layer_svc.attrs[LAYER_SVC_ATTR_CHAR_VALUE], &layer_index, + sizeof(layer_index)); + if (err) { + LOG_DBG("Layer notify failed (%d)", err); + } +} + +static int layer_state_changed_listener(const zmk_event_t *eh) { + if (as_zmk_layer_state_changed(eh) == NULL) { + return -ENOTSUP; + } + + uint8_t layer_index = current_layer_index(); + if (layer_index == last_layer_index) { + return 0; + } + + last_layer_index = layer_index; + notify_layer_change(layer_index); + return 0; +} + +ZMK_LISTENER(layer_gatt, layer_state_changed_listener); +ZMK_SUBSCRIPTION(layer_gatt, zmk_layer_state_changed);