From 9813105d1d9741312e6fc6d3040fd4506bfa869b Mon Sep 17 00:00:00 2001 From: darknao Date: Thu, 15 Jan 2026 20:42:55 +0100 Subject: [PATCH 1/6] feat(split): Send layer state to peripherals --- app/CMakeLists.txt | 1 + .../events/split_peripheral_layer_changed.h | 16 +++++++++ app/include/zmk/split/bluetooth/uuid.h | 1 + app/include/zmk/split/central.h | 2 ++ app/include/zmk/split/peripheral_layers.h | 5 +++ app/include/zmk/split/transport/types.h | 5 +++ .../events/split_peripheral_layer_changed.c | 10 ++++++ app/src/split/CMakeLists.txt | 1 + app/src/split/bluetooth/central.c | 28 +++++++++++++-- app/src/split/bluetooth/service.c | 34 ++++++++++++++++-- app/src/split/central.c | 36 +++++++++++++++++++ app/src/split/peripheral.c | 5 +++ app/src/split/peripheral_layers.c | 25 +++++++++++++ app/src/split/wired/central.c | 2 ++ 14 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 app/include/zmk/events/split_peripheral_layer_changed.h create mode 100644 app/include/zmk/split/peripheral_layers.h create mode 100644 app/src/events/split_peripheral_layer_changed.c create mode 100644 app/src/split/peripheral_layers.c diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index cc38244a4..8d3f9bb46 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -97,6 +97,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 app PRIVATE src/events/split_peripheral_layer_changed.c) add_subdirectory_ifdef(CONFIG_ZMK_SPLIT src/split) target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/usb.c) diff --git a/app/include/zmk/events/split_peripheral_layer_changed.h b/app/include/zmk/events/split_peripheral_layer_changed.h new file mode 100644 index 000000000..2445c164f --- /dev/null +++ b/app/include/zmk/events/split_peripheral_layer_changed.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +struct zmk_split_peripheral_layer_changed { + uint32_t layers; +}; + +ZMK_EVENT_DECLARE(zmk_split_peripheral_layer_changed); diff --git a/app/include/zmk/split/bluetooth/uuid.h b/app/include/zmk/split/bluetooth/uuid.h index c9a63efa7..6380e08f2 100644 --- a/app/include/zmk/split/bluetooth/uuid.h +++ b/app/include/zmk/split/bluetooth/uuid.h @@ -20,3 +20,4 @@ #define ZMK_SPLIT_BT_UPDATE_HID_INDICATORS_UUID ZMK_BT_SPLIT_UUID(0x00000004) #define ZMK_SPLIT_BT_SELECT_PHYS_LAYOUT_UUID ZMK_BT_SPLIT_UUID(0x00000005) #define ZMK_SPLIT_BT_INPUT_EVENT_UUID ZMK_BT_SPLIT_UUID(0x00000006) +#define ZMK_SPLIT_BT_UPDATE_LAYERS_UUID ZMK_BT_SPLIT_UUID(0x00000007) diff --git a/app/include/zmk/split/central.h b/app/include/zmk/split/central.h index ff971bfc6..3fcf17f23 100644 --- a/app/include/zmk/split/central.h +++ b/app/include/zmk/split/central.h @@ -46,3 +46,5 @@ int zmk_split_central_update_hid_indicator(zmk_hid_indicators_t indicators); int zmk_split_central_get_peripheral_battery_level(uint8_t source, uint8_t *level); #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) + +int zmk_split_central_update_layers(uint32_t layers); diff --git a/app/include/zmk/split/peripheral_layers.h b/app/include/zmk/split/peripheral_layers.h new file mode 100644 index 000000000..974a322c9 --- /dev/null +++ b/app/include/zmk/split/peripheral_layers.h @@ -0,0 +1,5 @@ +#pragma once + +void set_peripheral_layers_state(uint32_t new_layers); +bool peripheral_layer_active(uint8_t layer); +uint8_t peripheral_highest_layer_active(void); \ No newline at end of file diff --git a/app/include/zmk/split/transport/types.h b/app/include/zmk/split/transport/types.h index 1d6eb734c..1ce264673 100644 --- a/app/include/zmk/split/transport/types.h +++ b/app/include/zmk/split/transport/types.h @@ -66,6 +66,7 @@ enum zmk_split_transport_central_command_type { ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR, ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT, ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS, + ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS, } __packed; struct zmk_split_transport_central_command { @@ -87,5 +88,9 @@ struct zmk_split_transport_central_command { struct { zmk_hid_indicators_t indicators; } set_hid_indicators; + + struct { + uint32_t layers; + } set_rgb_layers; } data; } __packed; \ No newline at end of file diff --git a/app/src/events/split_peripheral_layer_changed.c b/app/src/events/split_peripheral_layer_changed.c new file mode 100644 index 000000000..81f2ab8de --- /dev/null +++ b/app/src/events/split_peripheral_layer_changed.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_split_peripheral_layer_changed); \ No newline at end of file diff --git a/app/src/split/CMakeLists.txt b/app/src/split/CMakeLists.txt index 20c730892..2d6ee8e1f 100644 --- a/app/src/split/CMakeLists.txt +++ b/app/src/split/CMakeLists.txt @@ -14,5 +14,6 @@ if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL) zephyr_linker_sources(SECTIONS ../../include/linker/zmk-split-transport-central.ld) else() target_sources(app PRIVATE peripheral.c) + target_sources(app PRIVATE peripheral_layers.c) zephyr_linker_sources(SECTIONS ../../include/linker/zmk-split-transport-peripheral.ld) endif() \ No newline at end of file diff --git a/app/src/split/bluetooth/central.c b/app/src/split/bluetooth/central.c index b4b61ce77..a98e47d4f 100644 --- a/app/src/split/bluetooth/central.c +++ b/app/src/split/bluetooth/central.c @@ -33,6 +33,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include static int start_scanning(void); @@ -60,6 +61,8 @@ struct peripheral_slot { uint16_t update_hid_indicators; #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) uint16_t selected_physical_layout_handle; + uint16_t update_layers_handle; + uint8_t position_state[POSITION_STATE_DATA_LEN]; uint8_t changed_positions[POSITION_STATE_DATA_LEN]; }; @@ -219,6 +222,7 @@ 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) + slot->update_layers_handle = 0; return 0; } @@ -620,6 +624,10 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn, LOG_DBG("Found update HID indicators handle"); slot->update_hid_indicators = bt_gatt_attr_value_handle(attr); #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) + } else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid, + BT_UUID_DECLARE_128(ZMK_SPLIT_BT_UPDATE_LAYERS_UUID))) { + LOG_DBG("Found update Layers handle"); + slot->update_layers_handle = bt_gatt_attr_value_handle(attr); #if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) } else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid, BT_UUID_BAS_BATTERY_LEVEL)) { @@ -707,6 +715,8 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn, } #endif // IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) + subscribed = subscribed && slot->update_layers_handle; + return subscribed ? BT_GATT_ITER_STOP : BT_GATT_ITER_CONTINUE; } @@ -1024,6 +1034,7 @@ K_MSGQ_DEFINE(zmk_split_central_split_run_msgq, sizeof(struct central_cmd_wrappe void split_central_split_run_callback(struct k_work *work) { struct central_cmd_wrapper payload_wrapper; + int err; LOG_DBG(""); @@ -1056,7 +1067,7 @@ void split_central_split_run_callback(struct k_work *work) { payload.behavior_dev); } - int err = bt_gatt_write_without_response( + err = bt_gatt_write_without_response( peripherals[payload_wrapper.source].conn, peripherals[payload_wrapper.source].run_behavior_handle, &payload, sizeof(struct zmk_split_run_behavior_payload), true); @@ -1082,7 +1093,7 @@ void split_central_split_run_callback(struct k_work *work) { break; } - int err = bt_gatt_write_without_response( + err = bt_gatt_write_without_response( peripherals[payload_wrapper.source].conn, peripherals[payload_wrapper.source].update_hid_indicators, &payload_wrapper.cmd.data.set_hid_indicators.indicators, @@ -1093,6 +1104,18 @@ void split_central_split_run_callback(struct k_work *work) { } break; #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS: + err = bt_gatt_write_without_response( + peripherals[payload_wrapper.source].conn, + peripherals[payload_wrapper.source].update_layers_handle, + &payload_wrapper.cmd.data.set_rgb_layers.layers, + sizeof(payload_wrapper.cmd.data.set_rgb_layers.layers), true); + + if (err) { + LOG_ERR("Failed to send layers to peripheral (err %d)", err); + } + break; + default: LOG_WRN("Unsupported wrapped central command type %d", payload_wrapper.cmd.type); return; @@ -1174,6 +1197,7 @@ static int split_central_bt_send_command(uint8_t source, } switch (cmd.type) { + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS: case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS: case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_PHYSICAL_LAYOUT: case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_INVOKE_BEHAVIOR: { diff --git a/app/src/split/bluetooth/service.c b/app/src/split/bluetooth/service.c index 5bbed1373..e83a6ea04 100644 --- a/app/src/split/bluetooth/service.c +++ b/app/src/split/bluetooth/service.c @@ -31,9 +31,11 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) #include #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) +#include #include #include +#include #if ZMK_KEYMAP_HAS_SENSORS static struct sensor_event last_sensor_event; @@ -139,6 +141,31 @@ static ssize_t split_svc_get_selected_phys_layout(struct bt_conn *conn, return bt_gatt_attr_read(conn, attrs, buf, len, offset, &selected, sizeof(selected)); } +static uint32_t layers = 0; + +static void split_svc_update_layers_callback(struct k_work *work) { + LOG_DBG("Setting peripheral layers: %x", layers); + // set_peripheral_layers_state(layers); + raise_zmk_split_peripheral_layer_changed( + (struct zmk_split_peripheral_layer_changed){.layers = layers}); +} + +static K_WORK_DEFINE(split_svc_update_layers_work, split_svc_update_layers_callback); + +static ssize_t split_svc_update_layers(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(uint32_t)) { + return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET); + } + + memcpy((uint8_t *)&layers + offset, buf, len); + + k_work_submit(&split_svc_update_layers_work); + + return len; +} + #if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT) static void split_input_events_ccc(const struct bt_gatt_attr *attr, uint16_t value) { @@ -204,8 +231,11 @@ BT_GATT_SERVICE_DEFINE( BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_SELECT_PHYS_LAYOUT_UUID), BT_GATT_CHRC_WRITE | BT_GATT_CHRC_READ, BT_GATT_PERM_WRITE_ENCRYPT | BT_GATT_PERM_READ_ENCRYPT, - split_svc_get_selected_phys_layout, split_svc_select_phys_layout, - NULL), ); + split_svc_get_selected_phys_layout, split_svc_select_phys_layout, NULL), + + BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_UPDATE_LAYERS_UUID), + BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE_ENCRYPT, NULL, + split_svc_update_layers, NULL), ); K_THREAD_STACK_DEFINE(service_q_stack, CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE); diff --git a/app/src/split/central.c b/app/src/split/central.c index e2b806437..5503b8527 100644 --- a/app/src/split/central.c +++ b/app/src/split/central.c @@ -150,6 +150,42 @@ int zmk_split_central_update_hid_indicator(zmk_hid_indicators_t indicators) { #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) +int zmk_split_central_update_layers(uint32_t new_layers) { + if (!active_transport || !active_transport->api || + !active_transport->api->get_available_source_ids || !active_transport->api->send_command) { + return -ENODEV; + } + + uint8_t source_ids[ZMK_SPLIT_CENTRAL_PERIPHERAL_COUNT]; + + int ret = active_transport->api->get_available_source_ids(source_ids); + + if (ret < 0) { + return ret; + } + + struct zmk_split_transport_central_command command = + (struct zmk_split_transport_central_command){ + .type = ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS, + .data = + { + .set_rgb_layers = + { + .layers = new_layers, + }, + }, + }; + + for (size_t i = 0; i < ret; i++) { + ret = active_transport->api->send_command(source_ids[i], command); + if (ret < 0) { + return ret; + } + } + + return 0; +} + #if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) int zmk_split_central_get_peripheral_battery_level(uint8_t source, uint8_t *level) { diff --git a/app/src/split/peripheral.c b/app/src/split/peripheral.c index b814df619..527e0dac0 100644 --- a/app/src/split/peripheral.c +++ b/app/src/split/peripheral.c @@ -21,6 +21,7 @@ #if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) #include #endif +#include #include #include @@ -66,6 +67,10 @@ int zmk_split_transport_peripheral_command_handler( .indicators = cmd.data.set_hid_indicators.indicators}); } #endif + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS: { + return raise_zmk_split_peripheral_layer_changed( + (struct zmk_split_peripheral_layer_changed){.layers = cmd.data.set_rgb_layers.layers}); + } default: LOG_WRN("Unhandled command type %d", cmd.type); return -ENOTSUP; diff --git a/app/src/split/peripheral_layers.c b/app/src/split/peripheral_layers.c new file mode 100644 index 000000000..95de8f8e9 --- /dev/null +++ b/app/src/split/peripheral_layers.c @@ -0,0 +1,25 @@ + +#include +#include + +#include +#include + +static uint32_t peripheral_layers = 0; + +void set_peripheral_layers_state(uint32_t new_layers) { peripheral_layers = new_layers; } + +bool peripheral_layer_active(uint8_t layer) { + return (peripheral_layers & (BIT(layer))) == (BIT(layer)); +}; + +uint8_t peripheral_highest_layer_active(void) { + if (peripheral_layers > 0) { + for (uint8_t layer = ZMK_KEYMAP_LAYERS_LEN - 1; layer > 0; layer--) { + if ((peripheral_layers & (BIT(layer))) == (BIT(layer)) || layer == 0) { + return layer; + } + } + } + return 0; +} \ No newline at end of file diff --git a/app/src/split/wired/central.c b/app/src/split/wired/central.c index 7ba661425..b8431b9fc 100644 --- a/app/src/split/wired/central.c +++ b/app/src/split/wired/central.c @@ -185,6 +185,8 @@ static ssize_t get_payload_data_size(const struct zmk_split_transport_central_co return sizeof(cmd->data.set_physical_layout); case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_HID_INDICATORS: return sizeof(cmd->data.set_hid_indicators); + case ZMK_SPLIT_TRANSPORT_CENTRAL_CMD_TYPE_SET_RGB_LAYERS: + return sizeof(cmd->data.set_rgb_layers); default: return -ENOTSUP; } From 3672cc498960ef9cb9c635d5690668513abe6016 Mon Sep 17 00:00:00 2001 From: darknao Date: Thu, 15 Jan 2026 21:32:33 +0100 Subject: [PATCH 2/6] feat(underglow): per-key/layer RGB underglow --- app/CMakeLists.txt | 2 + app/Kconfig | 4 + app/dts/behaviors.dtsi | 1 + app/dts/behaviors/ug_color.dtsi | 15 ++ .../zmk,behavior-underglow-color.yaml | 8 + app/dts/bindings/zmk,underglow-layer.yaml | 24 +++ app/include/dt-bindings/zmk/rgb_colors.h | 18 ++ app/include/zmk/rgb_underglow.h | 2 + app/include/zmk/rgb_underglow_layer.h | 31 +++ app/src/activity.c | 4 + app/src/behaviors/behavior_underglow_color.c | 39 ++++ app/src/keymap.c | 9 + app/src/rgb_underglow.c | 196 +++++++++++++++++- app/src/rgb_underglow_layer.c | 84 ++++++++ 14 files changed, 426 insertions(+), 11 deletions(-) create mode 100644 app/dts/behaviors/ug_color.dtsi create mode 100644 app/dts/bindings/behaviors/zmk,behavior-underglow-color.yaml create mode 100644 app/dts/bindings/zmk,underglow-layer.yaml create mode 100644 app/include/dt-bindings/zmk/rgb_colors.h create mode 100644 app/include/zmk/rgb_underglow_layer.h create mode 100644 app/src/behaviors/behavior_underglow_color.c create mode 100644 app/src/rgb_underglow_layer.c diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 8d3f9bb46..80f1c9537 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -89,6 +89,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) endif() target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c) +target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_underglow_color.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/behaviors/behavior_backlight.c) target_sources_ifdef(CONFIG_ZMK_BATTERY_REPORTING app PRIVATE src/events/battery_state_changed.c) @@ -103,6 +104,7 @@ add_subdirectory_ifdef(CONFIG_ZMK_SPLIT src/split) target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/usb.c) target_sources_ifdef(CONFIG_ZMK_USB app PRIVATE src/usb_hid.c) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/rgb_underglow.c) +target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/rgb_underglow_layer.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/backlight.c) target_sources_ifdef(CONFIG_ZMK_LOW_PRIORITY_WORK_QUEUE app PRIVATE src/workqueue.c) target_sources(app PRIVATE src/main.c) diff --git a/app/Kconfig b/app/Kconfig index e1c4ada9f..db9a3b90c 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -330,6 +330,10 @@ config ZMK_RGB_UNDERGLOW_AUTO_OFF_USB bool "Turn off RGB underglow when USB is disconnected" depends on USB_DEVICE_STACK +config EXPERIMENTAL_RGB_LAYER + bool "Experimental per-key per-layer RGB underglow" + default n + endif # ZMK_RGB_UNDERGLOW menuconfig ZMK_BACKLIGHT diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index 653b085d5..c70125498 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -28,3 +28,4 @@ #include #include #include +#include diff --git a/app/dts/behaviors/ug_color.dtsi b/app/dts/behaviors/ug_color.dtsi new file mode 100644 index 000000000..15d6c8630 --- /dev/null +++ b/app/dts/behaviors/ug_color.dtsi @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/ { + behaviors { + ug: ugcolor { + compatible = "zmk,behavior-underglow-color"; + #binding-cells = <1>; + display-name = "Underglow Color"; + }; + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-underglow-color.yaml b/app/dts/bindings/behaviors/zmk,behavior-underglow-color.yaml new file mode 100644 index 000000000..b3e902787 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-underglow-color.yaml @@ -0,0 +1,8 @@ +# Copyright (c) 2024, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Set underglow to specified color + +compatible: "zmk,behavior-underglow-color" + +include: one_param.yaml diff --git a/app/dts/bindings/zmk,underglow-layer.yaml b/app/dts/bindings/zmk,underglow-layer.yaml new file mode 100644 index 000000000..282210392 --- /dev/null +++ b/app/dts/bindings/zmk,underglow-layer.yaml @@ -0,0 +1,24 @@ +description: | + Allows defining a rgbmap composed of multiple layers + +compatible: "zmk,underglow-layer" + +properties: + pixel-lookup: + type: array + required: true + +child-binding: + description: "A layer to be used in a rgbmap" + + properties: + bindings: + type: phandle-array + required: true + layer-id: + type: int + required: true + fade-delay: + type: int + required: false + default: -1 diff --git a/app/include/dt-bindings/zmk/rgb_colors.h b/app/include/dt-bindings/zmk/rgb_colors.h new file mode 100644 index 000000000..73e9cc1f4 --- /dev/null +++ b/app/include/dt-bindings/zmk/rgb_colors.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2021 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define GREEN 0x00ff00 +#define RED 0xff0000 +#define BLUE 0x0000ff +#define TEAL 0x008080 +#define ORANGE 0xffa500 +#define YELLOW 0xffff00 +#define GOLD 0xffd700 +#define PURPLE 0x800080 +#define PINK 0xffc0cb +#define WHITE 0xffffff +#define ___ 0x000000 +#define BLACK 0x000000 \ No newline at end of file diff --git a/app/include/zmk/rgb_underglow.h b/app/include/zmk/rgb_underglow.h index be0ef2522..8feebb911 100644 --- a/app/include/zmk/rgb_underglow.h +++ b/app/include/zmk/rgb_underglow.h @@ -16,6 +16,8 @@ int zmk_rgb_underglow_toggle(void); int zmk_rgb_underglow_get_state(bool *state); int zmk_rgb_underglow_on(void); int zmk_rgb_underglow_off(void); +int zmk_rgb_underglow_transient_on(void); +int zmk_rgb_underglow_transient_off(void); int zmk_rgb_underglow_cycle_effect(int direction); int zmk_rgb_underglow_calc_effect(int direction); int zmk_rgb_underglow_select_effect(int effect); diff --git a/app/include/zmk/rgb_underglow_layer.h b/app/include/zmk/rgb_underglow_layer.h new file mode 100644 index 000000000..1b14359e6 --- /dev/null +++ b/app/include/zmk/rgb_underglow_layer.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once +#include + +#define ZMK_RGB_CHILD_LEN_PLUS_ONE(node) 1 + + +#define ZMK_RGBMAP_LAYERS_LEN \ + (DT_FOREACH_CHILD(DT_INST(0, zmk_underglow_layer), ZMK_RGB_CHILD_LEN_PLUS_ONE) 0) + +#define ZMK_RGBMAP_EXTRACT_BINDING(idx, drv_inst) \ + { \ + .behavior_dev = DEVICE_DT_NAME(DT_PHANDLE_BY_IDX(drv_inst, bindings, idx)), \ + .param1 = COND_CODE_0(DT_PHA_HAS_CELL_AT_IDX(drv_inst, bindings, idx, param1), (0), \ + (DT_PHA_BY_IDX(drv_inst, bindings, idx, param1))), \ + .param2 = COND_CODE_0(DT_PHA_HAS_CELL_AT_IDX(drv_inst, bindings, idx, param2), (0), \ + (DT_PHA_BY_IDX(drv_inst, bindings, idx, param2))), \ + } + +const int rgb_pixel_lookup(int idx); +const int zmk_rgbmap_id(uint8_t layer); +const int zmk_rgbmap_fade_delay(uint8_t layer); + +const struct zmk_behavior_binding *rgb_underglow_get_bindings(uint8_t layer); + +uint8_t rgb_underglow_top_layer_with_state(uint32_t state_to_test); +uint8_t rgb_underglow_top_layer(void); \ No newline at end of file diff --git a/app/src/activity.c b/app/src/activity.c index f4dc35624..0452c6265 100644 --- a/app/src/activity.c +++ b/app/src/activity.c @@ -16,6 +16,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include #include #include @@ -109,6 +110,9 @@ static int activity_init(void) { 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) +ZMK_SUBSCRIPTION(activity, zmk_split_peripheral_layer_changed); +#endif #if IS_ENABLED(CONFIG_ZMK_POINTING) diff --git a/app/src/behaviors/behavior_underglow_color.c b/app/src/behaviors/behavior_underglow_color.c new file mode 100644 index 000000000..29000aab6 --- /dev/null +++ b/app/src/behaviors/behavior_underglow_color.c @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_underglow_color + +// Dependencies +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +// Initialization Function +static int underglow_color_init(const struct device *dev) { return 0; }; + +static int underglow_color_process(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + return binding->param1; +} + +// API Structure +static const struct behavior_driver_api underglow_color_driver_api = { + .binding_pressed = underglow_color_process, + .locality = BEHAVIOR_LOCALITY_GLOBAL, +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, +#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + +}; + +BEHAVIOR_DT_INST_DEFINE(0, underglow_color_init, NULL, NULL, NULL, POST_KERNEL, + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &underglow_color_driver_api); + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/app/src/keymap.c b/app/src/keymap.c index a291d5f01..b825cece1 100644 --- a/app/src/keymap.c +++ b/app/src/keymap.c @@ -17,10 +17,14 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) +#include +#endif #include #include #include +#include #include static zmk_keymap_layers_state_t _zmk_keymap_layer_locks = 0; @@ -161,6 +165,11 @@ static inline int set_layer_state(zmk_keymap_layer_id_t layer_id, bool state, bo if (ret < 0) { LOG_WRN("Failed to raise layer state changed (%d)", ret); } +#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) + zmk_split_central_update_layers(_zmk_keymap_layer_state); + raise_zmk_split_peripheral_layer_changed( + (struct zmk_split_peripheral_layer_changed){.layers = _zmk_keymap_layer_state}); +#endif } return ret; diff --git a/app/src/rgb_underglow.c b/app/src/rgb_underglow.c index 3453fb44e..a2bd87697 100644 --- a/app/src/rgb_underglow.c +++ b/app/src/rgb_underglow.c @@ -16,15 +16,25 @@ #include #include +#include #include +#include #include +#include +#include +#include #include #include #include #include #include +#include + +#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) +#include +#endif LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); @@ -37,6 +47,11 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #define STRIP_CHOSEN DT_CHOSEN(zmk_underglow) #define STRIP_NUM_PIXELS DT_PROP(STRIP_CHOSEN, chain_length) +#if DT_HAS_COMPAT_STATUS_OKAY(zmk_underglow_layer) && IS_ENABLED(CONFIG_EXPERIMENTAL_RGB_LAYER) +#define UNDERGLOW_LAYER_ENABLED 1 +static void zmk_rgb_underglow_set_layer(uint8_t layer, bool wakeup); +#endif + #define HUE_MAX 360 #define SAT_MAX 100 #define BRT_MAX 100 @@ -49,6 +64,9 @@ enum rgb_underglow_effect { UNDERGLOW_EFFECT_BREATHE, UNDERGLOW_EFFECT_SPECTRUM, UNDERGLOW_EFFECT_SWIRL, +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + UNDERGLOW_EFFECT_LAYER_INDICATORS, +#endif UNDERGLOW_EFFECT_NUMBER // Used to track number of underglow effects }; @@ -58,6 +76,7 @@ struct rgb_underglow_state { uint8_t current_effect; uint16_t animation_step; bool on; + bool layer_enabled; }; static const struct device *led_strip; @@ -175,6 +194,25 @@ static void zmk_rgb_underglow_effect_swirl(void) { state.animation_step = state.animation_step % HUE_MAX; } +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) +static void zmk_rgb_underglow_effect_layer(void) { + bool active = false; + for (int i = 0; i < STRIP_NUM_PIXELS; i++) { + pixels[i].r -= state.animation_speed < pixels[i].r ? state.animation_speed : pixels[i].r; + pixels[i].g -= state.animation_speed < pixels[i].g ? state.animation_speed : pixels[i].g; + pixels[i].b -= state.animation_speed < pixels[i].b ? state.animation_speed : pixels[i].b; + if (pixels[i].r || pixels[i].g || pixels[i].b) { + active = true; + } + } + state.animation_step += state.animation_speed; + + if (state.animation_step > 255 || !active) { + zmk_rgb_underglow_transient_off(); + } +} +#endif // IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + static void zmk_rgb_underglow_tick(struct k_work *work) { switch (state.current_effect) { case UNDERGLOW_EFFECT_SOLID: @@ -189,6 +227,11 @@ static void zmk_rgb_underglow_tick(struct k_work *work) { case UNDERGLOW_EFFECT_SWIRL: zmk_rgb_underglow_effect_swirl(); break; +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + case UNDERGLOW_EFFECT_LAYER_INDICATORS: + zmk_rgb_underglow_effect_layer(); + break; +#endif } int err = led_strip_update_rgb(led_strip, pixels, STRIP_NUM_PIXELS); @@ -224,7 +267,11 @@ static int rgb_settings_set(const char *name, size_t len, settings_read_cb read_ if (state.on) { k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50)); } - +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + if (state.layer_enabled) { + zmk_rgb_underglow_set_layer(rgb_underglow_top_layer(), true); + } +#endif return 0; } @@ -276,7 +323,11 @@ static int zmk_rgb_underglow_init(void) { if (state.on) { k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50)); } - +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + if (state.layer_enabled) { + zmk_rgb_underglow_set_layer(rgb_underglow_top_layer(), true); + } +#endif return 0; } @@ -293,11 +344,21 @@ int zmk_rgb_underglow_get_state(bool *on_off) { if (!led_strip) return -ENODEV; - *on_off = state.on; + *on_off = state.on || state.layer_enabled; return 0; } int zmk_rgb_underglow_on(void) { + zmk_rgb_underglow_transient_on(); +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + if (state.current_effect == UNDERGLOW_EFFECT_LAYER_INDICATORS) { + state.layer_enabled = true; + } +#endif + return zmk_rgb_underglow_save_state(); +} + +int zmk_rgb_underglow_transient_on(void) { if (!led_strip) return -ENODEV; @@ -314,7 +375,7 @@ int zmk_rgb_underglow_on(void) { state.animation_step = 0; k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50)); - return zmk_rgb_underglow_save_state(); + return 0; } static void zmk_rgb_underglow_off_handler(struct k_work *work) { @@ -328,6 +389,12 @@ static void zmk_rgb_underglow_off_handler(struct k_work *work) { K_WORK_DEFINE(underglow_off_work, zmk_rgb_underglow_off_handler); int zmk_rgb_underglow_off(void) { + zmk_rgb_underglow_transient_off(); + state.layer_enabled = false; + return zmk_rgb_underglow_save_state(); +} + +int zmk_rgb_underglow_transient_off(void) { if (!led_strip) return -ENODEV; @@ -345,7 +412,7 @@ int zmk_rgb_underglow_off(void) { k_timer_stop(&underglow_tick); state.on = false; - return zmk_rgb_underglow_save_state(); + return 0; } int zmk_rgb_underglow_calc_effect(int direction) { @@ -362,7 +429,9 @@ int zmk_rgb_underglow_select_effect(int effect) { state.current_effect = effect; state.animation_step = 0; - +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + state.layer_enabled = (effect == UNDERGLOW_EFFECT_LAYER_INDICATORS); +#endif return zmk_rgb_underglow_save_state(); } @@ -374,6 +443,85 @@ int zmk_rgb_underglow_toggle(void) { return state.on ? zmk_rgb_underglow_off() : zmk_rgb_underglow_on(); } +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + +static struct led_rgb hex_to_rgb(uint8_t r, uint8_t g, uint8_t b) { + struct zmk_led_hsb hsb = state.color; + return (struct led_rgb){ + r : (hsb.b * (r)) / 0xff, + g : (hsb.b * (g)) / 0xff, + b : (hsb.b * (b)) / 0xff + }; +} + +static int zmk_rgb_underglow_apply_rgbmap(const struct zmk_behavior_binding *bindings, + size_t rgbmap_len, uint8_t layer) { + int rc = 0; + for (int i = 0; i < STRIP_NUM_PIXELS; i++) { + uint8_t midx = rgb_pixel_lookup(i); + if (midx >= ZMK_KEYMAP_LEN) { + LOG_DBG("out of range"); + } else { + const struct device *dev = zmk_behavior_get_binding(bindings[midx].behavior_dev); + + if (dev == NULL) { + continue; + } + + const struct behavior_driver_api *api = (const struct behavior_driver_api *)dev->api; + + if (api->binding_pressed == NULL) { + continue; + } + struct zmk_behavior_binding_event event = { + .position = midx, .layer = layer, .timestamp = k_uptime_get()}; + + int color = api->binding_pressed((struct zmk_behavior_binding *)&bindings[midx], event); + + if (color > 0) { + pixels[i] = + hex_to_rgb((color & 0xFF0000) >> 16, (color & 0xFF00) >> 8, color & 0xFF); + rc = 1; + } else { + pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0}; + } + } + } + return rc; +} + +static void zmk_rgb_underglow_set_layer(uint8_t layer, bool wakeup) { + LOG_DBG("state.layer: %d state.on: %d", state.layer_enabled, state.on); + if (!state.layer_enabled) + return; + + const struct zmk_behavior_binding *rgbmap = rgb_underglow_get_bindings(layer); + if (rgbmap != NULL && zmk_rgb_underglow_apply_rgbmap(rgbmap, ZMK_KEYMAP_LEN, layer)) { + if (!state.on) { + if (!wakeup) { + LOG_DBG("rgb off and no wakeup, abort refresh"); + return; + } + zmk_rgb_underglow_transient_on(); + } + k_timer_stop(&underglow_tick); + state.animation_step = 0; + int fade_delay = zmk_rgbmap_fade_delay(layer); + if (fade_delay >= 0) { + k_timer_start(&underglow_tick, K_SECONDS(fade_delay), K_MSEC(50)); + } + LOG_DBG("write pixels"); + int err = led_strip_update_rgb(led_strip, pixels, STRIP_NUM_PIXELS); + if (err < 0) { + LOG_ERR("Failed to update the RGB strip (%d)", err); + } + } else { + if (state.on) + zmk_rgb_underglow_transient_off(); + } +} +#endif /* IS_ENABLED(UNDERGLOW_LAYER_ENABLED) */ + int zmk_rgb_underglow_set_hsb(struct zmk_led_hsb color) { if (color.h > HUE_MAX || color.s > SAT_MAX || color.b > BRT_MAX) { return -ENOTSUP; @@ -461,7 +609,7 @@ int zmk_rgb_underglow_change_spd(int direction) { } #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) || \ - IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) + IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) || IS_ENABLED(UNDERGLOW_LAYER_ENABLED) struct rgb_underglow_sleep_state { bool is_awake; bool rgb_state_before_sleeping; @@ -480,14 +628,20 @@ static int rgb_underglow_auto_state(bool target_wake_state) { sleep_state.is_awake = target_wake_state; if (sleep_state.is_awake) { +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + if (state.layer_enabled) { + zmk_rgb_underglow_set_layer(rgb_underglow_top_layer(), true); + return 0; + } +#endif if (sleep_state.rgb_state_before_sleeping) { - return zmk_rgb_underglow_on(); + return zmk_rgb_underglow_transient_on(); } else { - return zmk_rgb_underglow_off(); + return zmk_rgb_underglow_transient_off(); } } else { sleep_state.rgb_state_before_sleeping = state.on; - return zmk_rgb_underglow_off(); + return zmk_rgb_underglow_transient_off(); } } @@ -499,6 +653,21 @@ static int rgb_underglow_event_listener(const zmk_event_t *eh) { } #endif +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) + if (as_zmk_split_peripheral_layer_changed(eh)) { + const struct zmk_split_peripheral_layer_changed *ev = + as_zmk_split_peripheral_layer_changed(eh); + LOG_DBG("zmk_split_peripheral_layer_changed: %08x", ev->layers); +#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + set_peripheral_layers_state(ev->layers); +#endif + uint8_t layer = rgb_underglow_top_layer(); + LOG_DBG("top layer: %d", layer); + zmk_rgb_underglow_set_layer(layer, true); + return 0; + } +#endif /* UNDERGLOW_LAYER_ENABLED */ + #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) if (as_zmk_usb_conn_state_changed(eh)) { return rgb_underglow_auto_state(zmk_usb_is_powered()); @@ -510,7 +679,8 @@ static int rgb_underglow_event_listener(const zmk_event_t *eh) { ZMK_LISTENER(rgb_underglow, rgb_underglow_event_listener); #endif // IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) || - // IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) + // IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) || + // IS_ENABLED(UNDERGLOW_LAYER_ENABLED) #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) ZMK_SUBSCRIPTION(rgb_underglow, zmk_activity_state_changed); @@ -520,4 +690,8 @@ ZMK_SUBSCRIPTION(rgb_underglow, zmk_activity_state_changed); ZMK_SUBSCRIPTION(rgb_underglow, zmk_usb_conn_state_changed); #endif +#if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) +ZMK_SUBSCRIPTION(rgb_underglow, zmk_split_peripheral_layer_changed); +#endif + SYS_INIT(zmk_rgb_underglow_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/src/rgb_underglow_layer.c b/app/src/rgb_underglow_layer.c new file mode 100644 index 000000000..c1d2965a6 --- /dev/null +++ b/app/src/rgb_underglow_layer.c @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include +#include + +#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) +#include +#endif + +#define DT_DRV_COMPAT zmk_underglow_layer +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +#define UNDERGLOW_LAYER_ENABLED +#define LAYER_ID(node) DT_PROP(node, layer_id) +#define FADE_DELAY(node) DT_PROP(node, fade_delay) + +#define TRANSFORMED_RGB_LAYER(node) \ + {COND_CODE_1(DT_NODE_HAS_PROP(node, bindings), \ + (LISTIFY(DT_PROP_LEN(node, bindings), ZMK_RGBMAP_EXTRACT_BINDING, (, ), node)), \ + ())} + +#define RGBMAP_VAR(_name, _opts) \ + static _opts struct zmk_behavior_binding _name[ZMK_RGBMAP_LAYERS_LEN][ZMK_KEYMAP_LEN] = { \ + DT_INST_FOREACH_CHILD_STATUS_OKAY_SEP(0, TRANSFORMED_RGB_LAYER, (, ))}; + +RGBMAP_VAR(zmk_rgbmap, COND_CODE_1(IS_ENABLED(CONFIG_ZMK_KEYMAP_SETTINGS_STORAGE), (), (const))) + +const int pixel_lookup_table[] = DT_INST_PROP(0, pixel_lookup); + +static int zmk_rgbmap_ids[ZMK_RGBMAP_LAYERS_LEN] = {DT_INST_FOREACH_CHILD_SEP(0, LAYER_ID, (, ))}; +static int zmk_rgbmap_fds[ZMK_RGBMAP_LAYERS_LEN] = {DT_INST_FOREACH_CHILD_SEP(0, FADE_DELAY, (, ))}; + +const int rgb_pixel_lookup(int idx) { return pixel_lookup_table[idx]; }; + +const int zmk_rgbmap_id(uint8_t layer) { + for (uint8_t i = 0; i < ZMK_RGBMAP_LAYERS_LEN; i++) { + if (zmk_rgbmap_ids[i] == layer) { + return i; + } + } + return -1; +} + +const int zmk_rgbmap_fade_delay(uint8_t layer) { return zmk_rgbmap_fds[zmk_rgbmap_id(layer)]; } + +const struct zmk_behavior_binding *rgb_underglow_get_bindings(uint8_t layer) { + int rgblayer = zmk_rgbmap_id(layer); + if (rgblayer == -1) { + return NULL; + } else { + return zmk_rgbmap[rgblayer]; + } +} + +uint8_t rgb_underglow_top_layer_with_state(uint32_t state_to_test) { + for (uint8_t layer = ZMK_KEYMAP_LAYERS_LEN - 1; layer > 0; layer--) { + if ((state_to_test & (BIT(layer))) == (BIT(layer)) || layer == 0) { + return layer; + } + } + // return default layer (0) + return 0; +} + +uint8_t rgb_underglow_top_layer(void) { +#if IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + return zmk_keymap_highest_layer_active(); +#else + return peripheral_highest_layer_active(); +#endif +} +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ \ No newline at end of file From 240e57b3918d5a8f71977bd8ab335909b09a2753 Mon Sep 17 00:00:00 2001 From: darknao Date: Thu, 15 Jan 2026 21:34:03 +0100 Subject: [PATCH 3/6] feat(underglow): add underglow_color_changed event --- app/CMakeLists.txt | 1 + app/include/zmk/events/underglow_color_changed.h | 16 ++++++++++++++++ app/src/events/underglow_color_changed.c | 10 ++++++++++ app/src/rgb_underglow.c | 12 ++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 app/include/zmk/events/underglow_color_changed.h create mode 100644 app/src/events/underglow_color_changed.c diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 80f1c9537..ab18e1586 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -90,6 +90,7 @@ endif() target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_underglow_color.c) +target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/events/underglow_color_changed.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/behaviors/behavior_backlight.c) target_sources_ifdef(CONFIG_ZMK_BATTERY_REPORTING app PRIVATE src/events/battery_state_changed.c) diff --git a/app/include/zmk/events/underglow_color_changed.h b/app/include/zmk/events/underglow_color_changed.h new file mode 100644 index 000000000..0d0ddf37d --- /dev/null +++ b/app/include/zmk/events/underglow_color_changed.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +struct zmk_underglow_color_changed { + uint32_t layers; + bool wakeup; +}; + +ZMK_EVENT_DECLARE(zmk_underglow_color_changed); diff --git a/app/src/events/underglow_color_changed.c b/app/src/events/underglow_color_changed.c new file mode 100644 index 000000000..c00bdc6b3 --- /dev/null +++ b/app/src/events/underglow_color_changed.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_underglow_color_changed); \ No newline at end of file diff --git a/app/src/rgb_underglow.c b/app/src/rgb_underglow.c index a2bd87697..bd5947ea9 100644 --- a/app/src/rgb_underglow.c +++ b/app/src/rgb_underglow.c @@ -29,6 +29,8 @@ #include #include #include +#include + #include #include @@ -666,6 +668,15 @@ static int rgb_underglow_event_listener(const zmk_event_t *eh) { zmk_rgb_underglow_set_layer(layer, true); return 0; } + if (as_zmk_underglow_color_changed(eh)) { + const struct zmk_underglow_color_changed *ev = as_zmk_underglow_color_changed(eh); + uint8_t layer = rgb_underglow_top_layer(); + LOG_DBG("refresh layers %d, current: %d, wakeup: %d", ev->layers, layer, ev->wakeup); + if ((ev->layers & (BIT(layer))) == BIT(layer)) { + zmk_rgb_underglow_set_layer(rgb_underglow_top_layer(), ev->wakeup); + } + return 0; + } #endif /* UNDERGLOW_LAYER_ENABLED */ #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) @@ -692,6 +703,7 @@ ZMK_SUBSCRIPTION(rgb_underglow, zmk_usb_conn_state_changed); #if IS_ENABLED(UNDERGLOW_LAYER_ENABLED) ZMK_SUBSCRIPTION(rgb_underglow, zmk_split_peripheral_layer_changed); +ZMK_SUBSCRIPTION(rgb_underglow, zmk_underglow_color_changed); #endif SYS_INIT(zmk_rgb_underglow_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); From 2f430dd5ed814ea0f4f0fb247a0436d62c0f6583 Mon Sep 17 00:00:00 2001 From: darknao Date: Thu, 15 Jan 2026 21:00:54 +0100 Subject: [PATCH 4/6] feat(underglow): Add HID indicator behavior to per-key RGB --- app/CMakeLists.txt | 3 + app/dts/behaviors.dtsi | 1 + app/dts/behaviors/ug_indicators.dtsi | 33 ++++++++ .../zmk,behavior-underglow-indicators.yaml | 13 +++ app/include/dt-bindings/zmk/hid_indicators.h | 9 ++ .../behaviors/behavior_underglow_indicators.c | 83 +++++++++++++++++++ 6 files changed, 142 insertions(+) create mode 100644 app/dts/behaviors/ug_indicators.dtsi create mode 100644 app/dts/bindings/behaviors/zmk,behavior-underglow-indicators.yaml create mode 100644 app/include/dt-bindings/zmk/hid_indicators.h create mode 100644 app/src/behaviors/behavior_underglow_indicators.c diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index ab18e1586..5dc0b8a61 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -90,6 +90,9 @@ endif() target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_underglow_color.c) +if (CONFIG_ZMK_HID_INDICATORS) + target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_underglow_indicators.c) +endif() target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/events/underglow_color_changed.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/behaviors/behavior_backlight.c) diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index c70125498..ff56705d2 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -29,3 +29,4 @@ #include #include #include +#include diff --git a/app/dts/behaviors/ug_indicators.dtsi b/app/dts/behaviors/ug_indicators.dtsi new file mode 100644 index 000000000..b62c79b77 --- /dev/null +++ b/app/dts/behaviors/ug_indicators.dtsi @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + behaviors { + ug_nl: ugnumlk { + compatible = "zmk,behavior-underglow-indicators"; + indicator = ; + #binding-cells = <2>; + display-name = "Underglow NumLock indicator"; + }; + + ug_cl: ugcapslk { + compatible = "zmk,behavior-underglow-indicators"; + indicator = ; + #binding-cells = <2>; + display-name = "Underglow CapsLock indicator"; + }; + + ug_sl: ugscrllk { + compatible = "zmk,behavior-underglow-indicators"; + indicator = ; + #binding-cells = <2>; + display-name = "Underglow ScrollLock indicator"; + }; + + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-underglow-indicators.yaml b/app/dts/bindings/behaviors/zmk,behavior-underglow-indicators.yaml new file mode 100644 index 000000000..3bd19081f --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-underglow-indicators.yaml @@ -0,0 +1,13 @@ +# Copyright (c) 2024, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Set underglow for HID indicators + +compatible: "zmk,behavior-underglow-indicators" + +include: two_param.yaml + +properties: + indicator: + type: int + default: 0 diff --git a/app/include/dt-bindings/zmk/hid_indicators.h b/app/include/dt-bindings/zmk/hid_indicators.h new file mode 100644 index 000000000..860c81dbd --- /dev/null +++ b/app/include/dt-bindings/zmk/hid_indicators.h @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2020 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define NUM_LOCK 0 +#define CAPS_LOCK 1 +#define SCROLL_LOCK 2 diff --git a/app/src/behaviors/behavior_underglow_indicators.c b/app/src/behaviors/behavior_underglow_indicators.c new file mode 100644 index 000000000..913e7f070 --- /dev/null +++ b/app/src/behaviors/behavior_underglow_indicators.c @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_underglow_indicators + +// Dependencies +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +struct underglow_indicators_data { + zmk_hid_indicators_t indicators; + uint32_t layers; +}; + +struct underglow_indicators_config { + int indicator; +}; + +static int underglow_indicators_init(const struct device *dev) { return 0; }; + +static int underglow_indicators_process(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev); + if (dev == NULL) { + return binding->param1; + } + struct underglow_indicators_data *data = dev->data; + const struct underglow_indicators_config *config = dev->config; + data->layers |= BIT(event.layer); + + if (data->indicators & BIT(config->indicator)) + return binding->param2; + else + return binding->param1; +} + +static const struct behavior_driver_api underglow_indicators_driver_api = { + .binding_pressed = underglow_indicators_process, + .locality = BEHAVIOR_LOCALITY_GLOBAL, +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, +#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) +}; + +static int underglow_indicators_listener(const zmk_event_t *eh); + +ZMK_LISTENER(behavior_underglow_indicators, underglow_indicators_listener); +ZMK_SUBSCRIPTION(behavior_underglow_indicators, zmk_hid_indicators_changed); + +static struct underglow_indicators_data underglow_indicators_data = {.indicators = 0, .layers = 0}; + +static int underglow_indicators_listener(const zmk_event_t *eh) { + const struct zmk_hid_indicators_changed *ev = as_zmk_hid_indicators_changed(eh); + underglow_indicators_data.indicators = ev->indicators; + raise_zmk_underglow_color_changed((struct zmk_underglow_color_changed){ + .layers = underglow_indicators_data.layers, .wakeup = true}); + + return ZMK_EV_EVENT_BUBBLE; +} + +#define KP_INST(n) \ + static struct underglow_indicators_config underglow_indicators_config_##n = { \ + .indicator = DT_INST_PROP(n, indicator)}; \ + BEHAVIOR_DT_INST_DEFINE(n, underglow_indicators_init, NULL, &underglow_indicators_data, \ + &underglow_indicators_config_##n, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, \ + &underglow_indicators_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(KP_INST) + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ From c73518229e2b231541b3816d3adb5c2aaac8794f Mon Sep 17 00:00:00 2001 From: darknao Date: Thu, 15 Jan 2026 21:01:32 +0100 Subject: [PATCH 5/6] feat(underglow): Add battery level behavior to per-key RGB --- app/CMakeLists.txt | 1 + app/dts/behaviors.dtsi | 1 + app/dts/behaviors/ug_battery.dtsi | 34 ++++++++ .../zmk,behavior-underglow-battery.yaml | 12 +++ .../behaviors/behavior_underglow_battery.c | 77 +++++++++++++++++++ 5 files changed, 125 insertions(+) create mode 100644 app/dts/behaviors/ug_battery.dtsi create mode 100644 app/dts/bindings/behaviors/zmk,behavior-underglow-battery.yaml create mode 100644 app/src/behaviors/behavior_underglow_battery.c diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 5dc0b8a61..36b7e661a 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -93,6 +93,7 @@ target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior if (CONFIG_ZMK_HID_INDICATORS) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_underglow_indicators.c) endif() +target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_underglow_battery.c) target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/events/underglow_color_changed.c) target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/behaviors/behavior_backlight.c) diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index ff56705d2..1f87ef1a7 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -30,3 +30,4 @@ #include #include #include +#include diff --git a/app/dts/behaviors/ug_battery.dtsi b/app/dts/behaviors/ug_battery.dtsi new file mode 100644 index 000000000..3e5f75cc6 --- /dev/null +++ b/app/dts/behaviors/ug_battery.dtsi @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +/ { + behaviors { + ug_b2: ugbat20 { + compatible = "zmk,behavior-underglow-battery"; + threshold = <20>; + #binding-cells = <2>; + display-name = "Underglow Battery level 20%"; + }; + ug_b4: ugbat40 { + compatible = "zmk,behavior-underglow-battery"; + threshold = <40>; + #binding-cells = <2>; + display-name = "Underglow Battery level 40%"; + }; + ug_b6: ugbat60 { + compatible = "zmk,behavior-underglow-battery"; + threshold = <60>; + #binding-cells = <2>; + display-name = "Underglow Battery level 60%"; + }; + ug_b8: ugbat80 { + compatible = "zmk,behavior-underglow-battery"; + threshold = <80>; + #binding-cells = <2>; + display-name = "Underglow Battery level 80%"; + }; + }; +}; diff --git a/app/dts/bindings/behaviors/zmk,behavior-underglow-battery.yaml b/app/dts/bindings/behaviors/zmk,behavior-underglow-battery.yaml new file mode 100644 index 000000000..4c535ea55 --- /dev/null +++ b/app/dts/bindings/behaviors/zmk,behavior-underglow-battery.yaml @@ -0,0 +1,12 @@ +# Copyright (c) 2024, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Set underglow color based on battery level + +compatible: "zmk,behavior-underglow-battery" + +include: two_param.yaml + +properties: + threshold: + type: int diff --git a/app/src/behaviors/behavior_underglow_battery.c b/app/src/behaviors/behavior_underglow_battery.c new file mode 100644 index 000000000..e369f8428 --- /dev/null +++ b/app/src/behaviors/behavior_underglow_battery.c @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_underglow_battery + +// Dependencies +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +struct underglow_battery_data { + uint32_t layers; +}; + +struct underglow_battery_config { + int threshold; +}; + +static struct underglow_battery_data underglow_battery_data = {.layers = 0}; + +static int underglow_battery_init(const struct device *dev) { return 0; }; + +static int underglow_battery_process(struct zmk_behavior_binding *binding, + struct zmk_behavior_binding_event event) { + const struct device *dev = zmk_behavior_get_binding(binding->behavior_dev); + const struct underglow_battery_config *config = dev->config; + struct underglow_battery_data *data = dev->data; + data->layers |= BIT(event.layer); + int bat = zmk_battery_state_of_charge(); + + if (bat >= config->threshold) + return binding->param2; + else + return binding->param1; +} + +static const struct behavior_driver_api underglow_battery_driver_api = { + .binding_pressed = underglow_battery_process, + .locality = BEHAVIOR_LOCALITY_GLOBAL, +#if IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) + .get_parameter_metadata = zmk_behavior_get_empty_param_metadata, +#endif // IS_ENABLED(CONFIG_ZMK_BEHAVIOR_METADATA) +}; + +static int underglow_battery_listener(const zmk_event_t *eh); + +ZMK_LISTENER(behavior_underglow_battery, underglow_battery_listener); +ZMK_SUBSCRIPTION(behavior_underglow_battery, zmk_battery_state_changed); + +static int underglow_battery_listener(const zmk_event_t *eh) { + raise_zmk_underglow_color_changed((struct zmk_underglow_color_changed){ + .layers = underglow_battery_data.layers, .wakeup = false}); + + return ZMK_EV_EVENT_BUBBLE; +} + +#define KP_INST(n) \ + static struct underglow_battery_config underglow_battery_config_##n = { \ + .threshold = DT_INST_PROP(n, threshold)}; \ + BEHAVIOR_DT_INST_DEFINE(n, underglow_battery_init, NULL, &underglow_battery_data, \ + &underglow_battery_config_##n, POST_KERNEL, \ + CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &underglow_battery_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(KP_INST) + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ From 453d2d8536106c1b5ba2ae68d6a2267965c16935 Mon Sep 17 00:00:00 2001 From: darknao Date: Thu, 15 Jan 2026 20:14:29 +0100 Subject: [PATCH 6/6] feat(boards): configure underglow-layer for glove80 --- app/boards/moergo/glove80/glove80_lh.dts | 9 +++++++++ app/boards/moergo/glove80/glove80_lh_defconfig | 7 +++++++ app/boards/moergo/glove80/glove80_rh.dts | 9 +++++++++ app/boards/moergo/glove80/glove80_rh_defconfig | 4 ++++ 4 files changed, 29 insertions(+) diff --git a/app/boards/moergo/glove80/glove80_lh.dts b/app/boards/moergo/glove80/glove80_lh.dts index 2ed56688a..a1f1f6156 100644 --- a/app/boards/moergo/glove80/glove80_lh.dts +++ b/app/boards/moergo/glove80/glove80_lh.dts @@ -19,6 +19,15 @@ zmk,battery = &vbatt; }; + underglow-layer { + compatible = "zmk,underglow-layer"; + pixel-lookup = + <52>, <53>, <54>, <69>, <70>, <71>, <15>, <27>, <39>, <51>, <4>, <14>, <26>, <38>, + <50>, <68>, <3>, <13>, <25>, <37>, <49>, <67>, <2>, <12>, <24>, <36>, <48>, <66>, + <1>, <11>, <23>, <35>, <47>, <65>, <0>, <10>, <22>, <34>, <46>, <64>; + }; + + back_led_backlight: pwmleds { compatible = "pwm-leds"; pwm_led_0 { diff --git a/app/boards/moergo/glove80/glove80_lh_defconfig b/app/boards/moergo/glove80/glove80_lh_defconfig index 9973e953e..4c381b84a 100644 --- a/app/boards/moergo/glove80/glove80_lh_defconfig +++ b/app/boards/moergo/glove80/glove80_lh_defconfig @@ -75,6 +75,13 @@ CONFIG_ZMK_BACKLIGHT_AUTO_OFF_USB=y # space. CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y +# Enable USB boot protocol support +CONFIG_ZMK_USB_BOOT=y +CONFIG_ZMK_HID_INDICATORS=y + +# Send HID indicator to peripherals +CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS=y + # Turn on debugging to disable optimization. Debug messages can result in larger # stacks, so enable stack protection and particularly a larger BLE peripheral stack. # CONFIG_DEBUG=y diff --git a/app/boards/moergo/glove80/glove80_rh.dts b/app/boards/moergo/glove80/glove80_rh.dts index 7b54f62c8..bcb6a41b7 100644 --- a/app/boards/moergo/glove80/glove80_rh.dts +++ b/app/boards/moergo/glove80/glove80_rh.dts @@ -20,6 +20,15 @@ zmk,battery = &vbatt; }; + underglow-layer { + compatible = "zmk,underglow-layer"; + pixel-lookup = + <57>, <56>, <55>, <74>, <73>, <72>, <16>, <28>, <40>, <58>, <5>, <17>, <29>, <41>, + <59>, <75>, <6>, <18>, <30>, <42>, <60>, <76>, <7>, <19>, <31>, <43>, <61>, <77>, + <8>, <20>, <32>, <44>, <62>, <78>, <9>, <21>, <33>, <45>, <63>, <79>; + }; + + back_led_backlight: pwmleds { compatible = "pwm-leds"; pwm_led_0 { diff --git a/app/boards/moergo/glove80/glove80_rh_defconfig b/app/boards/moergo/glove80/glove80_rh_defconfig index d45e0ded8..7dfe464f9 100644 --- a/app/boards/moergo/glove80/glove80_rh_defconfig +++ b/app/boards/moergo/glove80/glove80_rh_defconfig @@ -59,6 +59,10 @@ CONFIG_ZMK_RGB_UNDERGLOW_HUE_START=285 CONFIG_ZMK_RGB_UNDERGLOW_SAT_START=75 CONFIG_ZMK_RGB_UNDERGLOW_BRT_START=16 +# Enable HID indicators on peripheral +CONFIG_ZMK_HID_INDICATORS=y +CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS=y + # The power LED is implemented as a backlight # For now, the power LED is acting as a "USB connected" indicator CONFIG_ZMK_BACKLIGHT=y