Skip to content
Open
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
3 changes: 3 additions & 0 deletions config/boards/shields/corney/corney.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
8 changes: 8 additions & 0 deletions docs/gatt-layer-exposition.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 4 additions & 4 deletions openspec/changes/add-gatt-layer-exposition/tasks.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions zephyr/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
zephyr_library()
zephyr_library_sources_ifdef(CONFIG_ZMK_GATT_LAYER_EXPOSITION src/gatt_layer_exposition.c)

8 changes: 8 additions & 0 deletions zephyr/Kconfig
Original file line number Diff line number Diff line change
@@ -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.

2 changes: 2 additions & 0 deletions zephyr/module.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
build:
cmake: zephyr
kconfig: zephyr/Kconfig
settings:
board_root: .
86 changes: 86 additions & 0 deletions zephyr/src/gatt_layer_exposition.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2026 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/

#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/kernel.h>

#include <stdint.h>
#include <zmk/event_manager.h>
#include <zmk/events/layer_state_changed.h>
#include <zmk/keymap.h>

#include <zephyr/logging/log.h>

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);