From a36c7f26f8b25cb7bbf583544381d457531ed47b Mon Sep 17 00:00:00 2001 From: afiqzudinhadi Date: Wed, 1 Jul 2026 15:16:52 +0800 Subject: [PATCH] feat: expose persistent effect lifecycle, wakeup, fade-delay, idle hooks Core API additions for full parity with darknao fork behaviors: - ZMK_RGB_EFFECT_PERSISTENT flag: effect persists across RGB on/off - on_idle(bool awake) callback: effect controls sleep/wake behavior - is_active() callback: get_state returns on || effect->is_active() - zmk_rgb_request_refresh_wakeup(bool): conditional wake from sleep - zmk_rgb_set_tick_delay(int): delayed tick restart for fade-delay - zmk_rgb_is_on(): exposed state query for modules - state.layer_enabled restored: persistent effects survive RGB toggle - auto_state delegates to on_idle for effects that implement it - select_effect sets layer_enabled for PERSISTENT effects - transient_off no longer calls on_deselect (only true deselect does) --- app/include/zmk/rgb_effect.h | 13 +++++- app/src/rgb_underglow.c | 87 ++++++++++++++++++++++++++++++------ 2 files changed, 86 insertions(+), 14 deletions(-) diff --git a/app/include/zmk/rgb_effect.h b/app/include/zmk/rgb_effect.h index 98a57b8e..d70a9160 100644 --- a/app/include/zmk/rgb_effect.h +++ b/app/include/zmk/rgb_effect.h @@ -19,28 +19,39 @@ struct zmk_rgb_effect_ctx { typedef void (*zmk_rgb_effect_render_fn)(struct zmk_rgb_effect_ctx *ctx); typedef void (*zmk_rgb_effect_event_fn)(void); +typedef void (*zmk_rgb_effect_idle_fn)(bool awake); +typedef bool (*zmk_rgb_effect_is_active_fn)(void); #define ZMK_RGB_EFFECT_STATIC BIT(0) +#define ZMK_RGB_EFFECT_PERSISTENT BIT(1) struct zmk_rgb_effect { const char *name; zmk_rgb_effect_render_fn render; zmk_rgb_effect_event_fn on_select; zmk_rgb_effect_event_fn on_deselect; + zmk_rgb_effect_idle_fn on_idle; + zmk_rgb_effect_is_active_fn is_active; uint8_t flags; }; -#define ZMK_RGB_EFFECT_DEFINE(_sym, _name_str, _render, _flags, _on_select, _on_deselect) \ +#define ZMK_RGB_EFFECT_DEFINE(_sym, _name_str, _render, _flags, _on_select, _on_deselect, \ + _on_idle, _is_active) \ STRUCT_SECTION_ITERABLE(zmk_rgb_effect, _sym) = { \ .name = _name_str, \ .render = _render, \ .flags = _flags, \ .on_select = _on_select, \ .on_deselect = _on_deselect, \ + .on_idle = _on_idle, \ + .is_active = _is_active, \ } void zmk_rgb_request_refresh(void); +void zmk_rgb_request_refresh_wakeup(bool wakeup); int zmk_rgb_effect_get_count(void); +bool zmk_rgb_is_on(void); +void zmk_rgb_set_tick_delay(int delay_seconds); struct led_rgb zmk_rgb_hsb_to_rgb(struct zmk_led_hsb hsb); struct zmk_led_hsb zmk_rgb_hsb_scale_min_max(struct zmk_led_hsb hsb); diff --git a/app/src/rgb_underglow.c b/app/src/rgb_underglow.c index 567497c9..fff374ec 100644 --- a/app/src/rgb_underglow.c +++ b/app/src/rgb_underglow.c @@ -67,6 +67,7 @@ struct rgb_underglow_state { uint16_t animation_step; bool on; bool status_active; + bool layer_enabled; uint16_t status_animation_step; }; @@ -192,10 +193,11 @@ static void effect_swirl_render(struct zmk_rgb_effect_ctx *ctx) { *ctx->animation_step = *ctx->animation_step % HUE_MAX; } -ZMK_RGB_EFFECT_DEFINE(effect_solid, "Solid", effect_solid_render, 0, NULL, NULL); -ZMK_RGB_EFFECT_DEFINE(effect_breathe, "Breathe", effect_breathe_render, 0, NULL, NULL); -ZMK_RGB_EFFECT_DEFINE(effect_spectrum, "Spectrum", effect_spectrum_render, 0, NULL, NULL); -ZMK_RGB_EFFECT_DEFINE(effect_swirl, "Swirl", effect_swirl_render, 0, NULL, NULL); +ZMK_RGB_EFFECT_DEFINE(effect_solid, "Solid", effect_solid_render, 0, NULL, NULL, NULL, NULL); +ZMK_RGB_EFFECT_DEFINE(effect_breathe, "Breathe", effect_breathe_render, 0, NULL, NULL, NULL, NULL); +ZMK_RGB_EFFECT_DEFINE(effect_spectrum, "Spectrum", effect_spectrum_render, 0, NULL, NULL, NULL, + NULL); +ZMK_RGB_EFFECT_DEFINE(effect_swirl, "Swirl", effect_swirl_render, 0, NULL, NULL, NULL, NULL); /* --- Effect registry helpers --- */ @@ -215,6 +217,11 @@ static struct zmk_rgb_effect *zmk_rgb_effect_get(int index) { return effect; } +static bool zmk_rgb_effect_is_persistent(void) { + struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); + return effect && (effect->flags & ZMK_RGB_EFFECT_PERSISTENT); +} + /* --- Status indicator overlay (separate from effects) --- */ static int zmk_led_generate_status(void); @@ -462,7 +469,7 @@ static void zmk_rgb_underglow_tick(struct k_work *work) { K_WORK_DEFINE(underglow_tick_work, zmk_rgb_underglow_tick); static void zmk_rgb_underglow_tick_handler(struct k_timer *timer) { - if (!state.on) { + if (!state.on && !state.layer_enabled) { return; } @@ -471,10 +478,30 @@ static void zmk_rgb_underglow_tick_handler(struct k_timer *timer) { K_TIMER_DEFINE(underglow_tick, zmk_rgb_underglow_tick_handler, NULL); +bool zmk_rgb_is_on(void) { return state.on; } + void zmk_rgb_request_refresh(void) { k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_tick_work); } +void zmk_rgb_request_refresh_wakeup(bool wakeup) { + if (!state.on && !state.layer_enabled) { + if (!wakeup) { + return; + } + zmk_rgb_underglow_transient_on(); + } + zmk_rgb_request_refresh(); +} + +void zmk_rgb_set_tick_delay(int delay_seconds) { + k_timer_stop(&underglow_tick); + state.animation_step = 0; + if (delay_seconds >= 0) { + k_timer_start(&underglow_tick, K_SECONDS(delay_seconds), K_MSEC(50)); + } +} + /* --- Settings persistence --- */ #if IS_ENABLED(CONFIG_SETTINGS) @@ -495,6 +522,12 @@ 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 (state.layer_enabled) { + struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); + if (effect && effect->on_select) { + effect->on_select(); + } + } return 0; } @@ -555,6 +588,12 @@ static int zmk_rgb_underglow_init(void) { k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25)); } } + if (state.layer_enabled) { + struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); + if (effect && effect->on_select) { + effect->on_select(); + } + } return 0; } @@ -572,7 +611,7 @@ 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; } @@ -611,6 +650,10 @@ void zmk_rgb_set_ext_power(void) { int zmk_rgb_underglow_on(void) { zmk_rgb_underglow_transient_on(); + if (zmk_rgb_effect_is_persistent()) { + state.layer_enabled = true; + memset(pixels, 0, sizeof(struct led_rgb) * STRIP_NUM_PIXELS); + } return zmk_rgb_underglow_save_state(); } @@ -647,6 +690,7 @@ 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(); } @@ -654,11 +698,6 @@ int zmk_rgb_underglow_transient_off(void) { if (!led_strip) return -ENODEV; - struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); - if (effect && effect->on_deselect) { - effect->on_deselect(); - } - k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_off_work); k_timer_stop(&underglow_tick); @@ -684,6 +723,8 @@ int zmk_rgb_underglow_select_effect(int effect) { return -EINVAL; } + bool was_persistent = zmk_rgb_effect_is_persistent(); + struct zmk_rgb_effect *old_eff = zmk_rgb_effect_get(state.current_effect); if (old_eff && old_eff->on_deselect) { old_eff->on_deselect(); @@ -693,16 +734,26 @@ int zmk_rgb_underglow_select_effect(int effect) { state.animation_step = 0; struct zmk_rgb_effect *new_eff = zmk_rgb_effect_get(state.current_effect); + + state.layer_enabled = (new_eff && (new_eff->flags & ZMK_RGB_EFFECT_PERSISTENT)); + if (new_eff && new_eff->on_select) { new_eff->on_select(); } if (state.on) { - if (new_eff && (new_eff->flags & ZMK_RGB_EFFECT_STATIC)) { + if (state.layer_enabled) { + memset(pixels, 0, sizeof(struct led_rgb) * STRIP_NUM_PIXELS); + zmk_rgb_request_refresh(); + } else if (new_eff && (new_eff->flags & ZMK_RGB_EFFECT_STATIC)) { k_timer_stop(&underglow_tick); zmk_rgb_request_refresh(); } else { - k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25)); + if (was_persistent && !state.layer_enabled) { + k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25)); + } else { + k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25)); + } } } @@ -870,6 +921,11 @@ static int rgb_underglow_auto_state(bool target_wake_state) { sleep_state.is_awake = target_wake_state; if (sleep_state.is_awake) { + struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); + if (effect && effect->on_idle) { + effect->on_idle(true); + return 0; + } if (sleep_state.rgb_state_before_sleeping) { return zmk_rgb_underglow_transient_on(); } else { @@ -877,6 +933,11 @@ static int rgb_underglow_auto_state(bool target_wake_state) { } } else { sleep_state.rgb_state_before_sleeping = state.on; + struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); + if (effect && effect->on_idle) { + effect->on_idle(false); + return 0; + } return zmk_rgb_underglow_transient_off(); } }