From e2a7af15f44b0c9698a2218ad1554c88336e4c59 Mon Sep 17 00:00:00 2001 From: afiqzudinhadi Date: Wed, 1 Jul 2026 15:17:06 +0800 Subject: [PATCH 1/4] feat: implement all 6 fork parity behaviors - ZMK_RGB_EFFECT_PERSISTENT flag: layer effect survives RGB toggle - on_idle callback: wake re-renders layer, sleep turns off without clearing layer_effect_active - is_active callback: ext power stays on for layer RGB - Wakeup filtering: battery events (wakeup=false) don't wake sleeping RGB, layer changes (wakeup=true) do - Layer filtering: only refresh when changed layer matches active top layer, skip unnecessary re-renders - Fade delay: calls zmk_rgb_set_tick_delay() with per-layer DT fade-delay prop after rendering static pixels Requires: zmk fork rgb-modular-v1.1 branch --- src/effects/layer_effect.c | 45 +++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/effects/layer_effect.c b/src/effects/layer_effect.c index e43fde3..58e8eaa 100644 --- a/src/effects/layer_effect.c +++ b/src/effects/layer_effect.c @@ -90,11 +90,16 @@ static void layer_effect_render(struct zmk_rgb_effect_ctx *ctx) { 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); + if (apply_rgbmap(ctx->pixels, ctx->num_pixels, rgbmap, ZMK_KEYMAP_LEN, + layer, ctx->base_color.b)) { + int fade_delay = zmk_rgbmap_fade_delay(layer); + if (fade_delay >= 0) { + zmk_rgb_set_tick_delay(fade_delay); + } + } } else { - for (int i = 0; i < ctx->num_pixels; i++) { - ctx->pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0}; + if (zmk_rgb_is_on()) { + zmk_rgb_underglow_transient_off(); } } } @@ -107,11 +112,27 @@ 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); +static void layer_effect_on_idle(bool awake) { + if (awake) { + if (layer_effect_active) { + zmk_rgb_request_refresh_wakeup(true); + } + } else { + zmk_rgb_underglow_transient_off(); + } +} -/* Event listeners — refresh pixels on layer/color change */ +static bool layer_effect_is_active(void) { + return layer_effect_active; +} + +ZMK_RGB_EFFECT_DEFINE(effect_layer, "Layer Indicators", + layer_effect_render, + ZMK_RGB_EFFECT_STATIC | ZMK_RGB_EFFECT_PERSISTENT, + layer_effect_on_select, layer_effect_on_deselect, + layer_effect_on_idle, layer_effect_is_active); + +/* Event listeners */ static int layer_effect_event_listener(const zmk_event_t *eh) { if (!layer_effect_active) { @@ -126,13 +147,17 @@ static int layer_effect_event_listener(const zmk_event_t *eh) { #if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) set_peripheral_layers_state(ev->layers); #endif - zmk_rgb_request_refresh(); + zmk_rgb_request_refresh_wakeup(true); return 0; } #endif if (as_zmk_underglow_color_changed(eh)) { - zmk_rgb_request_refresh(); + const struct zmk_underglow_color_changed *ev = as_zmk_underglow_color_changed(eh); + uint8_t layer = rgb_underglow_top_layer(); + if ((ev->layers & BIT(layer)) == BIT(layer)) { + zmk_rgb_request_refresh_wakeup(ev->wakeup); + } return 0; } From e8002228427a74e1b63de68e51a0293444658843 Mon Sep 17 00:00:00 2001 From: afiqzudinhadi Date: Wed, 1 Jul 2026 15:46:36 +0800 Subject: [PATCH 2/4] Add README with setup, usage, and custom effect guide --- README.md | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b4ec84 --- /dev/null +++ b/README.md @@ -0,0 +1,153 @@ +# zmk-rgb-effects + +Modular RGB effects for ZMK keyboards. Provides per-key per-layer RGB indicators and a framework for adding custom effects without forking ZMK. + +Requires: [ZMK fork](https://github.com/afiqzudinhadi/zmk) branch `rgb-modular-v1.1` (modular effect registration API). + +## Setup + +Add to your `west.yml`: + +```yaml +manifest: + remotes: + - name: afiqzudinhadi + url-base: https://github.com/afiqzudinhadi + projects: + - name: zmk + remote: afiqzudinhadi + revision: rgb-modular-v1.1 + import: app/west.yml + - name: zmk-rgb-effects + remote: afiqzudinhadi + revision: v1.1.1 +``` + +Enable in your `.conf`: + +``` +CONFIG_ZMK_RGB_UNDERGLOW=y +CONFIG_ZMK_RGB_EFFECT_LAYER=y +``` + +## Per-key layer effect + +Define per-key colors in your keymap/overlay using devicetree: + +```dts +#include + +/ { + underglow-layer { + compatible = "zmk,underglow-layer"; + pixel-lookup = < + 5 4 3 2 1 0 + 6 7 8 9 10 11 + 17 16 15 14 13 12 + >; + + layer0 { + layer-id = <0>; + bindings = < + &ugc RGB_RED &ugc RGB_RED &ugc RGB_OFF ... + >; + }; + + layer1 { + layer-id = <1>; + fade-delay = <15>; + bindings = < + &ugc RGB_BLUE &ugc RGB_BLUE &ugc RGB_OFF ... + >; + }; + }; +}; +``` + +### Properties + +- `pixel-lookup` — maps LED chain index to key position +- `layer-id` — which keymap layer this color map applies to +- `fade-delay` — seconds before reverting to animated effect (-1 = never) +- `bindings` — color per key using behaviors below + +### Behaviors + +| Behavior | Description | Params | +|----------|-------------|--------| +| `&ugc` | Static color | `param1` = RGB hex color | +| `&ubi` | Battery indicator | `param1` = low color, `param2` = ok color, `threshold` DT prop | +| `&ugi` | HID indicator (caps/num/scroll lock) | `param1` = off color, `param2` = on color, `indicator` DT prop | + +## Adding custom effects + +Create a `.c` file with `ZMK_RGB_EFFECT_DEFINE`: + +```c +#include + +static void my_render(struct zmk_rgb_effect_ctx *ctx) { + for (int i = 0; i < ctx->num_pixels; i++) { + struct zmk_led_hsb hsb = ctx->base_color; + hsb.h = (*ctx->animation_step + i * 30) % 360; + ctx->pixels[i] = zmk_rgb_hsb_to_rgb(zmk_rgb_hsb_scale_min_max(hsb)); + } + *ctx->animation_step += ctx->animation_speed; +} + +ZMK_RGB_EFFECT_DEFINE(my_effect, "My Effect", + my_render, 0, NULL, NULL, NULL, NULL); +``` + +Add to `CMakeLists.txt`: + +```cmake +target_sources(app PRIVATE src/effects/my_effect.c) +``` + +The effect auto-registers via linker section. Accessible by cycling with `RGB_EFF`. + +### Effect flags + +| Flag | Description | +|------|-------------| +| `0` | Animated — core runs render at 25ms tick | +| `ZMK_RGB_EFFECT_STATIC` | No periodic tick — render called on demand via `zmk_rgb_request_refresh()` | +| `ZMK_RGB_EFFECT_PERSISTENT` | Survives RGB toggle and idle sleep | + +### Effect callbacks + +| Callback | When called | +|----------|-------------| +| `render` | Each tick (animated) or on refresh (static) | +| `on_select` | Effect becomes active | +| `on_deselect` | User cycles to different effect | +| `on_idle(bool awake)` | Keyboard sleeps/wakes (persistent effects only) | +| `is_active()` | Queried for ext power gating — return `true` to keep LEDs powered | + +### Render context + +| Field | Type | Description | +|-------|------|-------------| +| `pixels` | `struct led_rgb *` | LED buffer to write | +| `num_pixels` | `uint16_t` | Strip length | +| `base_color` | `struct zmk_led_hsb` | User-configured HSB color | +| `animation_step` | `uint16_t *` | Read/write animation counter | +| `animation_speed` | `uint8_t` | User-configured speed (1-5) | + +### Utility functions + +- `zmk_rgb_hsb_to_rgb(hsb)` — HSB to RGB conversion +- `zmk_rgb_hsb_scale_min_max(hsb)` — scale brightness to configured min/max +- `zmk_rgb_hsb_scale_zero_max(hsb)` — scale brightness from 0 to max +- `zmk_rgb_request_refresh()` — trigger re-render for static effects +- `zmk_rgb_request_refresh_wakeup(bool)` — refresh with conditional wake from sleep +- `zmk_rgb_set_tick_delay(int seconds)` — delayed tick restart (for fade effects) +- `zmk_rgb_is_on()` — query RGB on/off state + +## Versioning + +`vX.Y.Z` where: +- **X** — major version (breaking API changes) +- **Y** — ZMK core dependency version (matches `rgb-modular-vX.Y`) +- **Z** — custom effects (0 = defaults only, 1+ = custom effects included) From e7b543c501e6d65dc5879fb53b7bc8ae7e0cf74d Mon Sep 17 00:00:00 2001 From: afiqzudinhadi Date: Wed, 1 Jul 2026 15:49:32 +0800 Subject: [PATCH 3/4] README: add separate module repo guide --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index 3b4ec84..f23d471 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,53 @@ target_sources(app PRIVATE src/effects/my_effect.c) The effect auto-registers via linker section. Accessible by cycling with `RGB_EFF`. +### As a separate module repo + +You can also create effects in their own repo without modifying this module: + +``` +my-zmk-rgb-rainbow/ +├── zephyr/ +│ └── module.yml +├── CMakeLists.txt +├── Kconfig +└── src/ + └── rainbow.c +``` + +`zephyr/module.yml`: +```yaml +build: + cmake: . + kconfig: Kconfig +``` + +`Kconfig`: +``` +config ZMK_RGB_EFFECT_RAINBOW + bool "Rainbow RGB effect" + depends on ZMK_RGB_UNDERGLOW + default y +``` + +`CMakeLists.txt`: +```cmake +if(CONFIG_ZMK_RGB_EFFECT_RAINBOW) + target_sources(app PRIVATE src/rainbow.c) +endif() +``` + +`src/rainbow.c` uses `ZMK_RGB_EFFECT_DEFINE` exactly as above. + +Add to your `west.yml`: +```yaml +- name: my-zmk-rgb-rainbow + remote: your-github-remote + revision: main +``` + +Requires the ZMK fork with the modular effect API (`rgb-modular-v1.1` or later). No dependency on this module — the linker section collects effect registrations across all modules. + ### Effect flags | Flag | Description | From 618aaea1153ac32a82e23ee4a2cde88c17cc3c31 Mon Sep 17 00:00:00 2001 From: afiqzudinhadi Date: Wed, 1 Jul 2026 16:08:01 +0800 Subject: [PATCH 4/4] README: clarify v1.0 vs v1.1 API for separate module repos --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f23d471..057215a 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,12 @@ Add to your `west.yml`: revision: main ``` -Requires the ZMK fork with the modular effect API (`rgb-modular-v1.1` or later). No dependency on this module — the linker section collects effect registrations across all modules. +Requires the ZMK fork with the modular effect API. No dependency on this module — the linker section collects effect registrations across all modules. + +- `rgb-modular-v1.0` — basic effect API (`ZMK_RGB_EFFECT_DEFINE` with 6 params: render, flags, on_select, on_deselect). Sufficient for simple animated or static effects. +- `rgb-modular-v1.1` — extended API (adds `on_idle`, `is_active` callbacks, `ZMK_RGB_EFFECT_PERSISTENT` flag, `zmk_rgb_request_refresh_wakeup()`, `zmk_rgb_set_tick_delay()`, `zmk_rgb_is_on()`). Needed for effects that persist across RGB toggle/idle or use fade-delay. + +Start with `rgb-modular-v1.0` if you want to explore the API from scratch. ### Effect flags