commit 569dc85e0c1fedbd3afa0c347d25a38217560ca3 Author: afiqzudinhadi Date: Wed Jul 1 08:46:43 2026 +0800 Initial: modular per-key RGB layer effect for ZMK ZMK module providing per-key per-layer RGB effects via the modular effect API (zmk_rgb_effect). Extracted from darknao's fork patches. Contents: - Layer effect: registers via ZMK_RGB_EFFECT_DEFINE, renders per-key colors based on active keymap layer - Behaviors: underglow_color, underglow_battery, underglow_indicators - DTS bindings: zmk,underglow-layer + behavior bindings - Event: underglow_color_changed for reactive refresh Requires: ZMK core with rgb-modular branch (effect registration API) diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..258b4ff --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,9 @@ +if(CONFIG_ZMK_RGB_EFFECT_LAYER) + target_sources(app PRIVATE src/rgb_underglow_layer.c) + target_sources(app PRIVATE src/effects/layer_effect.c) + target_sources(app PRIVATE src/behaviors/behavior_underglow_color.c) + target_sources(app PRIVATE src/behaviors/behavior_underglow_battery.c) + target_sources(app PRIVATE src/behaviors/behavior_underglow_indicators.c) + target_sources(app PRIVATE src/events/underglow_color_changed.c) + target_include_directories(app PRIVATE include) +endif() diff --git a/Kconfig b/Kconfig new file mode 100644 index 0000000..0366189 --- /dev/null +++ b/Kconfig @@ -0,0 +1,7 @@ +config ZMK_RGB_EFFECT_LAYER + bool "Per-key per-layer RGB effect" + depends on ZMK_RGB_UNDERGLOW + default n + help + Enable per-key RGB colors that change based on the active keymap layer. + Requires the modular RGB effect API in the ZMK core (rgb-modular branch). diff --git a/dts/bindings/behaviors/zmk,behavior-underglow-battery.yaml b/dts/bindings/behaviors/zmk,behavior-underglow-battery.yaml new file mode 100644 index 0000000..4c535ea --- /dev/null +++ b/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/dts/bindings/behaviors/zmk,behavior-underglow-color.yaml b/dts/bindings/behaviors/zmk,behavior-underglow-color.yaml new file mode 100644 index 0000000..b3e9027 --- /dev/null +++ b/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/dts/bindings/behaviors/zmk,behavior-underglow-indicators.yaml b/dts/bindings/behaviors/zmk,behavior-underglow-indicators.yaml new file mode 100644 index 0000000..553bb6c --- /dev/null +++ b/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 num lock indicator + +compatible: "zmk,behavior-underglow-indicators" + +include: two_param.yaml + +properties: + indicator: + type: int + default: 0 diff --git a/dts/bindings/zmk,underglow-layer.yaml b/dts/bindings/zmk,underglow-layer.yaml new file mode 100644 index 0000000..2822103 --- /dev/null +++ b/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/include/zmk/events/underglow_color_changed.h b/include/zmk/events/underglow_color_changed.h new file mode 100644 index 0000000..0d0ddf3 --- /dev/null +++ b/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/include/zmk/rgb_underglow_layer.h b/include/zmk/rgb_underglow_layer.h new file mode 100644 index 0000000..8a27903 --- /dev/null +++ b/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); diff --git a/src/behaviors/behavior_underglow_battery.c b/src/behaviors/behavior_underglow_battery.c new file mode 100644 index 0000000..7ad71cb --- /dev/null +++ b/src/behaviors/behavior_underglow_battery.c @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_underglow_battery + +#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 +}; + +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) */ diff --git a/src/behaviors/behavior_underglow_color.c b/src/behaviors/behavior_underglow_color.c new file mode 100644 index 0000000..4a6c81a --- /dev/null +++ b/src/behaviors/behavior_underglow_color.c @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_underglow_color + +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +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; +} + +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 +}; + +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/src/behaviors/behavior_underglow_indicators.c b/src/behaviors/behavior_underglow_indicators.c new file mode 100644 index 0000000..588685e --- /dev/null +++ b/src/behaviors/behavior_underglow_indicators.c @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define DT_DRV_COMPAT zmk_behavior_underglow_indicators + +#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); + 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 +}; + +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) */ diff --git a/src/effects/layer_effect.c b/src/effects/layer_effect.c new file mode 100644 index 0000000..e43fde3 --- /dev/null +++ b/src/effects/layer_effect.c @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#if IS_ENABLED(CONFIG_ZMK_SPLIT) +#include +#endif + +#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) +#include +#endif + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#define DT_DRV_COMPAT zmk_underglow_layer +#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) + +static bool layer_effect_active = false; + +static struct led_rgb hex_to_rgb_scaled(uint8_t r, uint8_t g, uint8_t b, uint8_t brightness) { + return (struct led_rgb){ + r : (brightness * r) / 0xff, + g : (brightness * g) / 0xff, + b : (brightness * b) / 0xff + }; +} + +static int apply_rgbmap(struct led_rgb *pixels, uint16_t num_pixels, + const struct zmk_behavior_binding *bindings, + size_t rgbmap_len, uint8_t layer, uint8_t brightness) { + int rc = 0; + for (int i = 0; i < 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_scaled( + (color & 0xFF0000) >> 16, (color & 0xFF00) >> 8, color & 0xFF, brightness); + rc = 1; + } else { + pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0}; + } + } + } + return rc; +} + +static void layer_effect_render(struct zmk_rgb_effect_ctx *ctx) { + if (!layer_effect_active) { + return; + } + + uint8_t layer = rgb_underglow_top_layer(); + const struct zmk_behavior_binding *rgbmap = rgb_underglow_get_bindings(layer); + if (rgbmap != NULL) { + apply_rgbmap(ctx->pixels, ctx->num_pixels, rgbmap, ZMK_KEYMAP_LEN, + layer, ctx->base_color.b); + } else { + for (int i = 0; i < ctx->num_pixels; i++) { + ctx->pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0}; + } + } +} + +static void layer_effect_on_select(void) { + layer_effect_active = true; +} + +static void layer_effect_on_deselect(void) { + layer_effect_active = false; +} + +ZMK_RGB_EFFECT_DEFINE(effect_layer, "Layer Indicators", + layer_effect_render, ZMK_RGB_EFFECT_STATIC, + layer_effect_on_select, layer_effect_on_deselect); + +/* Event listeners — refresh pixels on layer/color change */ + +static int layer_effect_event_listener(const zmk_event_t *eh) { + if (!layer_effect_active) { + return ZMK_EV_EVENT_BUBBLE; + } + +#if IS_ENABLED(CONFIG_ZMK_SPLIT) + 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("peripheral layer changed: %08x", ev->layers); +#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) + set_peripheral_layers_state(ev->layers); +#endif + zmk_rgb_request_refresh(); + return 0; + } +#endif + + if (as_zmk_underglow_color_changed(eh)) { + zmk_rgb_request_refresh(); + return 0; + } + + return ZMK_EV_EVENT_BUBBLE; +} + +ZMK_LISTENER(layer_effect, layer_effect_event_listener); + +#if IS_ENABLED(CONFIG_ZMK_SPLIT) +ZMK_SUBSCRIPTION(layer_effect, zmk_split_peripheral_layer_changed); +#endif +ZMK_SUBSCRIPTION(layer_effect, zmk_underglow_color_changed); + +#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */ diff --git a/src/events/underglow_color_changed.c b/src/events/underglow_color_changed.c new file mode 100644 index 0000000..468ce83 --- /dev/null +++ b/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); diff --git a/src/rgb_underglow_layer.c b/src/rgb_underglow_layer.c new file mode 100644 index 0000000..6c4aa2e --- /dev/null +++ b/src/rgb_underglow_layer.c @@ -0,0 +1,83 @@ +/* + * 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 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) */ diff --git a/zephyr/module.yml b/zephyr/module.yml new file mode 100644 index 0000000..219b2cf --- /dev/null +++ b/zephyr/module.yml @@ -0,0 +1,5 @@ +build: + cmake: . + kconfig: Kconfig + settings: + dts_root: .