Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 618aaea115 | |||
| e7b543c501 | |||
| e800222842 | |||
| e2a7af15f4 |
2 changed files with 240 additions and 10 deletions
205
README.md
Normal file
205
README.md
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
# 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 <dt-bindings/zmk/rgb_colors.h>
|
||||||
|
|
||||||
|
/ {
|
||||||
|
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 <zmk/rgb_effect.h>
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
### 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. 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
|
||||||
|
|
||||||
|
| 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)
|
||||||
|
|
@ -90,11 +90,16 @@ static void layer_effect_render(struct zmk_rgb_effect_ctx *ctx) {
|
||||||
uint8_t layer = rgb_underglow_top_layer();
|
uint8_t layer = rgb_underglow_top_layer();
|
||||||
const struct zmk_behavior_binding *rgbmap = rgb_underglow_get_bindings(layer);
|
const struct zmk_behavior_binding *rgbmap = rgb_underglow_get_bindings(layer);
|
||||||
if (rgbmap != NULL) {
|
if (rgbmap != NULL) {
|
||||||
apply_rgbmap(ctx->pixels, ctx->num_pixels, rgbmap, ZMK_KEYMAP_LEN,
|
if (apply_rgbmap(ctx->pixels, ctx->num_pixels, rgbmap, ZMK_KEYMAP_LEN,
|
||||||
layer, ctx->base_color.b);
|
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 {
|
} else {
|
||||||
for (int i = 0; i < ctx->num_pixels; i++) {
|
if (zmk_rgb_is_on()) {
|
||||||
ctx->pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0};
|
zmk_rgb_underglow_transient_off();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,11 +112,27 @@ static void layer_effect_on_deselect(void) {
|
||||||
layer_effect_active = false;
|
layer_effect_active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ZMK_RGB_EFFECT_DEFINE(effect_layer, "Layer Indicators",
|
static void layer_effect_on_idle(bool awake) {
|
||||||
layer_effect_render, ZMK_RGB_EFFECT_STATIC,
|
if (awake) {
|
||||||
layer_effect_on_select, layer_effect_on_deselect);
|
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) {
|
static int layer_effect_event_listener(const zmk_event_t *eh) {
|
||||||
if (!layer_effect_active) {
|
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)
|
#if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
|
||||||
set_peripheral_layers_state(ev->layers);
|
set_peripheral_layers_state(ev->layers);
|
||||||
#endif
|
#endif
|
||||||
zmk_rgb_request_refresh();
|
zmk_rgb_request_refresh_wakeup(true);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (as_zmk_underglow_color_changed(eh)) {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue