From ce94160d0ac01d35f83b3467e2886226cf5e497d Mon Sep 17 00:00:00 2001 From: Willy Straussberger Date: Sat, 28 Jun 2025 10:22:04 +0200 Subject: [PATCH] feat(underglow): Add battery and BLE status to underglow Adds functionality to the RGB underglow system, allowing it to display battery status and BLE connection status. Battery status is shown on startup and when reaching critical levels. BLE status is shown on startup and when the connection state changes. --- app/CMakeLists.txt | 21 +- app/Kconfig | 26 + .../zmk/rgb_underglow/battery_status.h | 11 + .../zmk/rgb_underglow/ble_peripheral_status.h | 16 + app/include/zmk/rgb_underglow/ble_status.h | 19 + .../current_status.h} | 10 +- app/include/zmk/rgb_underglow/init.h | 10 + .../zmk/rgb_underglow/rgb_underglow_base.h | 26 + app/include/zmk/rgb_underglow/startup_mutex.h | 13 + app/include/zmk/rgb_underglow/state.h | 52 ++ app/src/behaviors/behavior_rgb_underglow.c | 2 +- app/src/rgb_underglow.c | 523 ------------------ app/src/rgb_underglow/battery_status.c | 97 ++++ app/src/rgb_underglow/ble_peripheral_status.c | 66 +++ app/src/rgb_underglow/ble_status.c | 86 +++ app/src/rgb_underglow/current_status.c | 197 +++++++ app/src/rgb_underglow/init.c | 127 +++++ app/src/rgb_underglow/rgb_underglow_base.c | 214 +++++++ app/src/rgb_underglow/startup_mutex.c | 37 ++ app/src/rgb_underglow/state.c | 74 +++ app/src/rgb_underglow/status_on_startup.c | 199 +++++++ docs/docs/config/lighting.md | 38 +- 22 files changed, 1316 insertions(+), 548 deletions(-) create mode 100644 app/include/zmk/rgb_underglow/battery_status.h create mode 100644 app/include/zmk/rgb_underglow/ble_peripheral_status.h create mode 100644 app/include/zmk/rgb_underglow/ble_status.h rename app/include/zmk/{rgb_underglow.h => rgb_underglow/current_status.h} (81%) create mode 100644 app/include/zmk/rgb_underglow/init.h create mode 100644 app/include/zmk/rgb_underglow/rgb_underglow_base.h create mode 100644 app/include/zmk/rgb_underglow/startup_mutex.h create mode 100644 app/include/zmk/rgb_underglow/state.h delete mode 100644 app/src/rgb_underglow.c create mode 100644 app/src/rgb_underglow/battery_status.c create mode 100644 app/src/rgb_underglow/ble_peripheral_status.c create mode 100644 app/src/rgb_underglow/ble_status.c create mode 100644 app/src/rgb_underglow/current_status.c create mode 100644 app/src/rgb_underglow/init.c create mode 100644 app/src/rgb_underglow/rgb_underglow_base.c create mode 100644 app/src/rgb_underglow/startup_mutex.c create mode 100644 app/src/rgb_underglow/state.c create mode 100644 app/src/rgb_underglow/status_on_startup.c diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 60c502fcd2a..4a41bf24346 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -84,6 +84,11 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL) target_sources(app PRIVATE src/behaviors/behavior_bt.c) target_sources(app PRIVATE src/ble.c) target_sources(app PRIVATE src/hog.c) + target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS app PRIVATE src/rgb_underglow/ble_status.c) + endif() +else() + if (CONFIG_ZMK_BLE) + target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS app PRIVATE src/rgb_underglow/ble_peripheral_status.c) endif() endif() @@ -100,7 +105,21 @@ 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) + +if (CONFIG_ZMK_RGB_UNDERGLOW) + target_sources(app PRIVATE src/rgb_underglow/current_status.c) + target_sources(app PRIVATE src/rgb_underglow/rgb_underglow_base.c) + target_sources(app PRIVATE src/rgb_underglow/init.c) + target_sources(app PRIVATE src/rgb_underglow/state.c) + if (CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS OR CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS) + target_sources( app PRIVATE src/rgb_underglow/startup_mutex.c) + target_sources( app PRIVATE src/rgb_underglow/status_on_startup.c) + endif() + if (CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS) + target_sources( app PRIVATE src/rgb_underglow/battery_status.c) + endif() +endif() + 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 6b4e3509a0d..1ce221d0f8b 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -335,6 +335,32 @@ config ZMK_RGB_UNDERGLOW_AUTO_OFF_USB bool "Turn off RGB underglow when USB is disconnected" depends on USB_DEVICE_STACK +if ZMK_BATTERY_REPORTING +config ZMK_RGB_UNDERGLOW_BATTERY_STATUS + bool "Reflect the battery status with underglow" + default n + +config ZMK_RGB_UNDERGLOW_BATTERY_CRIT + int "Threshold for showing the RGB value for critically low battery charge" + range 0 100 + default 5 + +config ZMK_RGB_UNDERGLOW_BATTERY_LOW + int "Threshold for showing the RGB value for low battery charge" + range 0 100 + default 15 + +#ZMK_BATTERY_REPORTING +endif + +if ZMK_BLE +config ZMK_RGB_UNDERGLOW_BLE_STATUS + bool "Reflect the ble connection states with underglow" + default n + +#ZMK_BLE +endif + endif # ZMK_RGB_UNDERGLOW menuconfig ZMK_BACKLIGHT diff --git a/app/include/zmk/rgb_underglow/battery_status.h b/app/include/zmk/rgb_underglow/battery_status.h new file mode 100644 index 00000000000..8ad8ec20a59 --- /dev/null +++ b/app/include/zmk/rgb_underglow/battery_status.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +int rgb_underglow_set_color_battery(uint8_t state_of_charge); diff --git a/app/include/zmk/rgb_underglow/ble_peripheral_status.h b/app/include/zmk/rgb_underglow/ble_peripheral_status.h new file mode 100644 index 00000000000..678652450da --- /dev/null +++ b/app/include/zmk/rgb_underglow/ble_peripheral_status.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +struct peripheral_ble_state { + bool connected; +}; + +struct peripheral_ble_state zmk_get_ble_peripheral_state(); +int zmk_rgb_underglow_set_color_ble_peripheral(struct peripheral_ble_state ps); diff --git a/app/include/zmk/rgb_underglow/ble_status.h b/app/include/zmk/rgb_underglow/ble_status.h new file mode 100644 index 00000000000..c68d3f96e6c --- /dev/null +++ b/app/include/zmk/rgb_underglow/ble_status.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +struct output_state { + struct zmk_endpoint_instance selected_endpoint; + bool active_profile_connected; + bool active_profile_bonded; +}; + +struct output_state zmk_get_output_state(void); +int zmk_rgb_underglow_set_color_ble(struct output_state os); diff --git a/app/include/zmk/rgb_underglow.h b/app/include/zmk/rgb_underglow/current_status.h similarity index 81% rename from app/include/zmk/rgb_underglow.h rename to app/include/zmk/rgb_underglow/current_status.h index be0ef252290..e57d6b2475f 100644 --- a/app/include/zmk/rgb_underglow.h +++ b/app/include/zmk/rgb_underglow/current_status.h @@ -1,16 +1,12 @@ /* - * Copyright (c) 2020 The ZMK Contributors + * Copyright (c) 2025 The ZMK Contributors * * SPDX-License-Identifier: MIT */ #pragma once -struct zmk_led_hsb { - uint16_t h; - uint8_t s; - uint8_t b; -}; +#include int zmk_rgb_underglow_toggle(void); int zmk_rgb_underglow_get_state(bool *state); @@ -27,3 +23,5 @@ int zmk_rgb_underglow_change_sat(int direction); int zmk_rgb_underglow_change_brt(int direction); int zmk_rgb_underglow_change_spd(int direction); int zmk_rgb_underglow_set_hsb(struct zmk_led_hsb color); +int zmk_rgb_underglow_apply_current_state(void); +void zmk_rgb_underglow_init(void); \ No newline at end of file diff --git a/app/include/zmk/rgb_underglow/init.h b/app/include/zmk/rgb_underglow/init.h new file mode 100644 index 00000000000..a7ceafb2a0e --- /dev/null +++ b/app/include/zmk/rgb_underglow/init.h @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +int zmk_rgb_ug_on(void); +int zmk_rgb_ug_off(void); \ No newline at end of file diff --git a/app/include/zmk/rgb_underglow/rgb_underglow_base.h b/app/include/zmk/rgb_underglow/rgb_underglow_base.h new file mode 100644 index 00000000000..bb1936e2053 --- /dev/null +++ b/app/include/zmk/rgb_underglow/rgb_underglow_base.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include +#include + +#if !DT_HAS_CHOSEN(zmk_underglow) + +#error "A zmk,underglow chosen node must be declared" + +#endif + +#define STRIP_CHOSEN DT_CHOSEN(zmk_underglow) +#define STRIP_NUM_PIXELS DT_PROP(STRIP_CHOSEN, chain_length) + +int zmk_rgb_ug_select_effect(int effect); +int zmk_rgb_ug_set_spd(int speed); +int zmk_rgb_ug_set_hsb(struct zmk_led_hsb color); +void zmk_rgb_ug_tick(struct k_work *work); +void zmk_rgb_ug_tools_init(const struct device *led_strip); diff --git a/app/include/zmk/rgb_underglow/startup_mutex.h b/app/include/zmk/rgb_underglow/startup_mutex.h new file mode 100644 index 00000000000..0141d20e235 --- /dev/null +++ b/app/include/zmk/rgb_underglow/startup_mutex.h @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +bool is_starting_up(void); +bool start_startup(void); +void stop_startup(void); diff --git a/app/include/zmk/rgb_underglow/state.h b/app/include/zmk/rgb_underglow/state.h new file mode 100644 index 00000000000..5ff6f24eacf --- /dev/null +++ b/app/include/zmk/rgb_underglow/state.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include + +#define HUE_MAX 360 +#define SAT_MAX 100 +#define BRT_MAX 100 + +struct zmk_led_hsb { + uint16_t h; + uint8_t s; + uint8_t b; +}; + +struct rgb_underglow_state { + struct zmk_led_hsb color; + uint8_t animation_speed; + uint8_t current_effect; + uint16_t animation_step; + bool on; +}; + +enum rgb_underglow_effect { + UNDERGLOW_EFFECT_SOLID, + UNDERGLOW_EFFECT_BREATHE, + UNDERGLOW_EFFECT_SPECTRUM, + UNDERGLOW_EFFECT_SWIRL, + UNDERGLOW_EFFECT_NUMBER // Used to track number of underglow effects +}; + +static const struct rgb_underglow_state default_rgb_settings = (struct rgb_underglow_state){ + color : { + h : CONFIG_ZMK_RGB_UNDERGLOW_HUE_START, + s : CONFIG_ZMK_RGB_UNDERGLOW_SAT_START, + b : CONFIG_ZMK_RGB_UNDERGLOW_BRT_START, + }, + animation_speed : CONFIG_ZMK_RGB_UNDERGLOW_SPD_START, + current_effect : CONFIG_ZMK_RGB_UNDERGLOW_EFF_START, + animation_step : 0, + on : IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_ON_START) +}; + +struct rgb_underglow_state *zmk_rgb_ug_get_state(void); +struct rgb_underglow_state *zmk_rgb_ug_get_save_state(void); +int zmk_rgb_ug_save_state(void); +int zmk_rgb_ug_state_init(void); diff --git a/app/src/behaviors/behavior_rgb_underglow.c b/app/src/behaviors/behavior_rgb_underglow.c index 80cd518204c..7e8de1220ce 100644 --- a/app/src/behaviors/behavior_rgb_underglow.c +++ b/app/src/behaviors/behavior_rgb_underglow.c @@ -11,7 +11,7 @@ #include #include -#include +#include #include LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); diff --git a/app/src/rgb_underglow.c b/app/src/rgb_underglow.c deleted file mode 100644 index 3453fb44e0d..00000000000 --- a/app/src/rgb_underglow.c +++ /dev/null @@ -1,523 +0,0 @@ -/* - * Copyright (c) 2020 The ZMK Contributors - * - * SPDX-License-Identifier: MIT - */ - -#include -#include -#include -#include - -#include -#include - -#include - -#include -#include - -#include - -#include -#include -#include -#include -#include -#include - -LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); - -#if !DT_HAS_CHOSEN(zmk_underglow) - -#error "A zmk,underglow chosen node must be declared" - -#endif - -#define STRIP_CHOSEN DT_CHOSEN(zmk_underglow) -#define STRIP_NUM_PIXELS DT_PROP(STRIP_CHOSEN, chain_length) - -#define HUE_MAX 360 -#define SAT_MAX 100 -#define BRT_MAX 100 - -BUILD_ASSERT(CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN <= CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX, - "ERROR: RGB underglow maximum brightness is less than minimum brightness"); - -enum rgb_underglow_effect { - UNDERGLOW_EFFECT_SOLID, - UNDERGLOW_EFFECT_BREATHE, - UNDERGLOW_EFFECT_SPECTRUM, - UNDERGLOW_EFFECT_SWIRL, - UNDERGLOW_EFFECT_NUMBER // Used to track number of underglow effects -}; - -struct rgb_underglow_state { - struct zmk_led_hsb color; - uint8_t animation_speed; - uint8_t current_effect; - uint16_t animation_step; - bool on; -}; - -static const struct device *led_strip; - -static struct led_rgb pixels[STRIP_NUM_PIXELS]; - -static struct rgb_underglow_state state; - -#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) -static const struct device *const ext_power = DEVICE_DT_GET(DT_INST(0, zmk_ext_power_generic)); -#endif - -static struct zmk_led_hsb hsb_scale_min_max(struct zmk_led_hsb hsb) { - hsb.b = CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN + - (CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX - CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN) * hsb.b / BRT_MAX; - return hsb; -} - -static struct zmk_led_hsb hsb_scale_zero_max(struct zmk_led_hsb hsb) { - hsb.b = hsb.b * CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX / BRT_MAX; - return hsb; -} - -static struct led_rgb hsb_to_rgb(struct zmk_led_hsb hsb) { - float r = 0, g = 0, b = 0; - - uint8_t i = hsb.h / 60; - float v = hsb.b / ((float)BRT_MAX); - float s = hsb.s / ((float)SAT_MAX); - float f = hsb.h / ((float)HUE_MAX) * 6 - i; - float p = v * (1 - s); - float q = v * (1 - f * s); - float t = v * (1 - (1 - f) * s); - - switch (i % 6) { - case 0: - r = v; - g = t; - b = p; - break; - case 1: - r = q; - g = v; - b = p; - break; - case 2: - r = p; - g = v; - b = t; - break; - case 3: - r = p; - g = q; - b = v; - break; - case 4: - r = t; - g = p; - b = v; - break; - case 5: - r = v; - g = p; - b = q; - break; - } - - struct led_rgb rgb = {r : r * 255, g : g * 255, b : b * 255}; - - return rgb; -} - -static void zmk_rgb_underglow_effect_solid(void) { - for (int i = 0; i < STRIP_NUM_PIXELS; i++) { - pixels[i] = hsb_to_rgb(hsb_scale_min_max(state.color)); - } -} - -static void zmk_rgb_underglow_effect_breathe(void) { - for (int i = 0; i < STRIP_NUM_PIXELS; i++) { - struct zmk_led_hsb hsb = state.color; - hsb.b = abs(state.animation_step - 1200) / 12; - - pixels[i] = hsb_to_rgb(hsb_scale_zero_max(hsb)); - } - - state.animation_step += state.animation_speed * 10; - - if (state.animation_step > 2400) { - state.animation_step = 0; - } -} - -static void zmk_rgb_underglow_effect_spectrum(void) { - for (int i = 0; i < STRIP_NUM_PIXELS; i++) { - struct zmk_led_hsb hsb = state.color; - hsb.h = state.animation_step; - - pixels[i] = hsb_to_rgb(hsb_scale_min_max(hsb)); - } - - state.animation_step += state.animation_speed; - state.animation_step = state.animation_step % HUE_MAX; -} - -static void zmk_rgb_underglow_effect_swirl(void) { - for (int i = 0; i < STRIP_NUM_PIXELS; i++) { - struct zmk_led_hsb hsb = state.color; - hsb.h = (HUE_MAX / STRIP_NUM_PIXELS * i + state.animation_step) % HUE_MAX; - - pixels[i] = hsb_to_rgb(hsb_scale_min_max(hsb)); - } - - state.animation_step += state.animation_speed * 2; - state.animation_step = state.animation_step % HUE_MAX; -} - -static void zmk_rgb_underglow_tick(struct k_work *work) { - switch (state.current_effect) { - case UNDERGLOW_EFFECT_SOLID: - zmk_rgb_underglow_effect_solid(); - break; - case UNDERGLOW_EFFECT_BREATHE: - zmk_rgb_underglow_effect_breathe(); - break; - case UNDERGLOW_EFFECT_SPECTRUM: - zmk_rgb_underglow_effect_spectrum(); - break; - case UNDERGLOW_EFFECT_SWIRL: - zmk_rgb_underglow_effect_swirl(); - break; - } - - 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); - } -} - -K_WORK_DEFINE(underglow_tick_work, zmk_rgb_underglow_tick); - -static void zmk_rgb_underglow_tick_handler(struct k_timer *timer) { - if (!state.on) { - return; - } - - k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_tick_work); -} - -K_TIMER_DEFINE(underglow_tick, zmk_rgb_underglow_tick_handler, NULL); - -#if IS_ENABLED(CONFIG_SETTINGS) -static int rgb_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) { - const char *next; - int rc; - - if (settings_name_steq(name, "state", &next) && !next) { - if (len != sizeof(state)) { - return -EINVAL; - } - - rc = read_cb(cb_arg, &state, sizeof(state)); - if (rc >= 0) { - if (state.on) { - k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50)); - } - - return 0; - } - - return rc; - } - - return -ENOENT; -} - -SETTINGS_STATIC_HANDLER_DEFINE(rgb_underglow, "rgb/underglow", NULL, rgb_settings_set, NULL, NULL); - -static void zmk_rgb_underglow_save_state_work(struct k_work *_work) { - settings_save_one("rgb/underglow/state", &state, sizeof(state)); -} - -static struct k_work_delayable underglow_save_work; -#endif - -static int zmk_rgb_underglow_init(void) { - led_strip = DEVICE_DT_GET(STRIP_CHOSEN); - -#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) - if (!device_is_ready(ext_power)) { - LOG_ERR("External power device \"%s\" is not ready", ext_power->name); - return -ENODEV; - } -#endif - - state = (struct rgb_underglow_state){ - color : { - h : CONFIG_ZMK_RGB_UNDERGLOW_HUE_START, - s : CONFIG_ZMK_RGB_UNDERGLOW_SAT_START, - b : CONFIG_ZMK_RGB_UNDERGLOW_BRT_START, - }, - animation_speed : CONFIG_ZMK_RGB_UNDERGLOW_SPD_START, - current_effect : CONFIG_ZMK_RGB_UNDERGLOW_EFF_START, - animation_step : 0, - on : IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_ON_START) - }; - -#if IS_ENABLED(CONFIG_SETTINGS) - k_work_init_delayable(&underglow_save_work, zmk_rgb_underglow_save_state_work); -#endif - -#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) - state.on = zmk_usb_is_powered(); -#endif - - if (state.on) { - k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50)); - } - - return 0; -} - -int zmk_rgb_underglow_save_state(void) { -#if IS_ENABLED(CONFIG_SETTINGS) - int ret = k_work_reschedule(&underglow_save_work, K_MSEC(CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE)); - return MIN(ret, 0); -#else - return 0; -#endif -} - -int zmk_rgb_underglow_get_state(bool *on_off) { - if (!led_strip) - return -ENODEV; - - *on_off = state.on; - return 0; -} - -int zmk_rgb_underglow_on(void) { - if (!led_strip) - return -ENODEV; - -#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) - if (ext_power != NULL) { - int rc = ext_power_enable(ext_power); - if (rc != 0) { - LOG_ERR("Unable to enable EXT_POWER: %d", rc); - } - } -#endif - - state.on = true; - state.animation_step = 0; - k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50)); - - return zmk_rgb_underglow_save_state(); -} - -static void zmk_rgb_underglow_off_handler(struct k_work *work) { - for (int i = 0; i < STRIP_NUM_PIXELS; i++) { - pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0}; - } - - led_strip_update_rgb(led_strip, pixels, STRIP_NUM_PIXELS); -} - -K_WORK_DEFINE(underglow_off_work, zmk_rgb_underglow_off_handler); - -int zmk_rgb_underglow_off(void) { - if (!led_strip) - return -ENODEV; - -#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) - if (ext_power != NULL) { - int rc = ext_power_disable(ext_power); - if (rc != 0) { - LOG_ERR("Unable to disable EXT_POWER: %d", rc); - } - } -#endif - - k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_off_work); - - k_timer_stop(&underglow_tick); - state.on = false; - - return zmk_rgb_underglow_save_state(); -} - -int zmk_rgb_underglow_calc_effect(int direction) { - return (state.current_effect + UNDERGLOW_EFFECT_NUMBER + direction) % UNDERGLOW_EFFECT_NUMBER; -} - -int zmk_rgb_underglow_select_effect(int effect) { - if (!led_strip) - return -ENODEV; - - if (effect < 0 || effect >= UNDERGLOW_EFFECT_NUMBER) { - return -EINVAL; - } - - state.current_effect = effect; - state.animation_step = 0; - - return zmk_rgb_underglow_save_state(); -} - -int zmk_rgb_underglow_cycle_effect(int direction) { - return zmk_rgb_underglow_select_effect(zmk_rgb_underglow_calc_effect(direction)); -} - -int zmk_rgb_underglow_toggle(void) { - return state.on ? zmk_rgb_underglow_off() : zmk_rgb_underglow_on(); -} - -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; - } - - state.color = color; - - return 0; -} - -struct zmk_led_hsb zmk_rgb_underglow_calc_hue(int direction) { - struct zmk_led_hsb color = state.color; - - color.h += HUE_MAX + (direction * CONFIG_ZMK_RGB_UNDERGLOW_HUE_STEP); - color.h %= HUE_MAX; - - return color; -} - -struct zmk_led_hsb zmk_rgb_underglow_calc_sat(int direction) { - struct zmk_led_hsb color = state.color; - - int s = color.s + (direction * CONFIG_ZMK_RGB_UNDERGLOW_SAT_STEP); - if (s < 0) { - s = 0; - } else if (s > SAT_MAX) { - s = SAT_MAX; - } - color.s = s; - - return color; -} - -struct zmk_led_hsb zmk_rgb_underglow_calc_brt(int direction) { - struct zmk_led_hsb color = state.color; - - int b = color.b + (direction * CONFIG_ZMK_RGB_UNDERGLOW_BRT_STEP); - color.b = CLAMP(b, 0, BRT_MAX); - - return color; -} - -int zmk_rgb_underglow_change_hue(int direction) { - if (!led_strip) - return -ENODEV; - - state.color = zmk_rgb_underglow_calc_hue(direction); - - return zmk_rgb_underglow_save_state(); -} - -int zmk_rgb_underglow_change_sat(int direction) { - if (!led_strip) - return -ENODEV; - - state.color = zmk_rgb_underglow_calc_sat(direction); - - return zmk_rgb_underglow_save_state(); -} - -int zmk_rgb_underglow_change_brt(int direction) { - if (!led_strip) - return -ENODEV; - - state.color = zmk_rgb_underglow_calc_brt(direction); - - return zmk_rgb_underglow_save_state(); -} - -int zmk_rgb_underglow_change_spd(int direction) { - if (!led_strip) - return -ENODEV; - - if (state.animation_speed == 1 && direction < 0) { - return 0; - } - - state.animation_speed += direction; - - if (state.animation_speed > 5) { - state.animation_speed = 5; - } - - return zmk_rgb_underglow_save_state(); -} - -#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) || \ - IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) -struct rgb_underglow_sleep_state { - bool is_awake; - bool rgb_state_before_sleeping; -}; - -static int rgb_underglow_auto_state(bool target_wake_state) { - static struct rgb_underglow_sleep_state sleep_state = { - is_awake : true, - rgb_state_before_sleeping : false - }; - - // wake up event while awake, or sleep event while sleeping -> no-op - if (target_wake_state == sleep_state.is_awake) { - return 0; - } - sleep_state.is_awake = target_wake_state; - - if (sleep_state.is_awake) { - if (sleep_state.rgb_state_before_sleeping) { - return zmk_rgb_underglow_on(); - } else { - return zmk_rgb_underglow_off(); - } - } else { - sleep_state.rgb_state_before_sleeping = state.on; - return zmk_rgb_underglow_off(); - } -} - -static int rgb_underglow_event_listener(const zmk_event_t *eh) { - -#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) - if (as_zmk_activity_state_changed(eh)) { - return rgb_underglow_auto_state(zmk_activity_get_state() == ZMK_ACTIVITY_ACTIVE); - } -#endif - -#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()); - } -#endif - - return -ENOTSUP; -} - -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) - -#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) -ZMK_SUBSCRIPTION(rgb_underglow, zmk_activity_state_changed); -#endif - -#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) -ZMK_SUBSCRIPTION(rgb_underglow, zmk_usb_conn_state_changed); -#endif - -SYS_INIT(zmk_rgb_underglow_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/src/rgb_underglow/battery_status.c b/app/src/rgb_underglow/battery_status.c new file mode 100644 index 00000000000..d98915834d4 --- /dev/null +++ b/app/src/rgb_underglow/battery_status.c @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); +uint8_t last_state_of_charge = 100; + +int rgb_underglow_set_color_battery(uint8_t state_of_charge) { + last_state_of_charge = state_of_charge; + if (state_of_charge < CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_CRIT) { + struct zmk_led_hsb color = {h : 0, s : 100, b : 5}; + return zmk_rgb_ug_on() | zmk_rgb_ug_set_spd(5) | + zmk_rgb_ug_select_effect(UNDERGLOW_EFFECT_BREATHE) | zmk_rgb_ug_set_hsb(color); + } else if (state_of_charge < CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_LOW) { + struct zmk_led_hsb color = {h : 60, s : 100, b : 30}; + return zmk_rgb_ug_on() | zmk_rgb_ug_select_effect(UNDERGLOW_EFFECT_SOLID) | + zmk_rgb_ug_set_hsb(color); + } else { + struct zmk_led_hsb color = {h : 120, s : 100, b : 30}; + return zmk_rgb_ug_on() | zmk_rgb_ug_select_effect(UNDERGLOW_EFFECT_SOLID) | + zmk_rgb_ug_set_hsb(color); + } +} + +static void rgb_underglow_status_timeout_work(struct k_work *work) { + zmk_rgb_underglow_apply_current_state(); +} + +K_WORK_DEFINE(underglow_timeout_work, rgb_underglow_status_timeout_work); + +static void rgb_underglow_status_timeout_timer(struct k_timer *timer) { + k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_timeout_work); +} + +K_TIMER_DEFINE(underglow_timeout_timer, rgb_underglow_status_timeout_timer, NULL); + +static int rgb_underglow_battery_state_event_listener(const zmk_event_t *eh) { + const struct zmk_battery_state_changed *sc = as_zmk_battery_state_changed(eh); + if (!sc) { + LOG_ERR("underglow battery state listener called with unsupported argument"); + return -ENOTSUP; + } + + if (is_starting_up()) + return 0; + + if (sc->state_of_charge < CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_CRIT) { + struct zmk_led_hsb color = {h : 0, s : 100, b : 5}; + last_state_of_charge = sc->state_of_charge; + return zmk_rgb_ug_on() | zmk_rgb_ug_set_spd(5) | + zmk_rgb_ug_select_effect(UNDERGLOW_EFFECT_BREATHE) | zmk_rgb_ug_set_hsb(color); + } + + if (sc->state_of_charge < CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_LOW && + (last_state_of_charge >= CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_LOW || + last_state_of_charge < CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_CRIT)) { + k_timer_start(&underglow_timeout_timer, K_SECONDS(5), K_NO_WAIT); + last_state_of_charge = sc->state_of_charge; + return rgb_underglow_set_color_battery(sc->state_of_charge); + } + + if (sc->state_of_charge >= CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_LOW && + last_state_of_charge < CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_LOW) { + k_timer_start(&underglow_timeout_timer, K_SECONDS(5), K_NO_WAIT); + last_state_of_charge = sc->state_of_charge; + return rgb_underglow_set_color_battery(sc->state_of_charge); + } + + last_state_of_charge = sc->state_of_charge; + return 0; +} + +ZMK_LISTENER(rgb_battery, rgb_underglow_battery_state_event_listener); +ZMK_SUBSCRIPTION(rgb_battery, zmk_battery_state_changed); diff --git a/app/src/rgb_underglow/ble_peripheral_status.c b/app/src/rgb_underglow/ble_peripheral_status.c new file mode 100644 index 00000000000..fb951c94ab1 --- /dev/null +++ b/app/src/rgb_underglow/ble_peripheral_status.c @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +struct peripheral_ble_state zmk_get_ble_peripheral_state() { + return (struct peripheral_ble_state){.connected = zmk_split_bt_peripheral_is_connected()}; +} + +int zmk_rgb_underglow_set_color_ble_peripheral(struct peripheral_ble_state ps) { + struct zmk_led_hsb color = {h : 240, s : 100, b : 30}; + if (ps.connected) + return zmk_rgb_ug_on() | zmk_rgb_ug_select_effect(UNDERGLOW_EFFECT_SOLID) | + zmk_rgb_ug_set_hsb(color); + return zmk_rgb_ug_on() | zmk_rgb_ug_set_spd(5) | + zmk_rgb_ug_select_effect(UNDERGLOW_EFFECT_BREATHE) | zmk_rgb_ug_set_hsb(color); +} + +static void rgb_underglow_ble_peripheral_status_timeout_work(struct k_work *work) { + zmk_rgb_underglow_apply_current_state(); +} + +K_WORK_DEFINE(underglow_ble_peripheral_timeout_work, + rgb_underglow_ble_peripheral_status_timeout_work); + +static void rgb_underglow_ble_peripheral_status_timeout_timer(struct k_timer *timer) { + k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_ble_peripheral_timeout_work); +} + +K_TIMER_DEFINE(underglow_ble_peripheral_timeout_timer, + rgb_underglow_ble_peripheral_status_timeout_timer, NULL); + +static int rgb_underglow_ble_peripheral_state_event_listener(const zmk_event_t *eh) { + const struct peripheral_ble_state state = zmk_get_ble_peripheral_state(); + + if (is_starting_up()) + return 0; + + if (state.connected) + k_timer_start(&underglow_ble_peripheral_timeout_timer, K_SECONDS(2), K_NO_WAIT); + + return zmk_rgb_underglow_set_color_ble_peripheral(state); +} + +ZMK_LISTENER(rgb_ble_peripheral, rgb_underglow_ble_peripheral_state_event_listener); +ZMK_SUBSCRIPTION(rgb_ble_peripheral, zmk_split_peripheral_status_changed); diff --git a/app/src/rgb_underglow/ble_status.c b/app/src/rgb_underglow/ble_status.c new file mode 100644 index 00000000000..46320e6f6d8 --- /dev/null +++ b/app/src/rgb_underglow/ble_status.c @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +struct output_state zmk_get_output_state() { + return (struct output_state){.selected_endpoint = zmk_endpoints_selected(), + .active_profile_connected = zmk_ble_active_profile_is_connected(), + .active_profile_bonded = !zmk_ble_active_profile_is_open()}; + ; +} + +int zmk_rgb_underglow_set_color_ble(struct output_state os) { + if (os.selected_endpoint.transport == ZMK_TRANSPORT_BLE) { + struct zmk_led_hsb color = { + h : os.selected_endpoint.ble.profile_index * 60, + s : 100, + b : 30 + }; + if (os.active_profile_bonded) { + if (os.active_profile_connected) + return zmk_rgb_ug_on() | zmk_rgb_ug_select_effect(UNDERGLOW_EFFECT_SOLID) | + zmk_rgb_ug_set_hsb(color); + return zmk_rgb_ug_on() | zmk_rgb_ug_set_spd(2) | + zmk_rgb_ug_select_effect(UNDERGLOW_EFFECT_BREATHE) | zmk_rgb_ug_set_hsb(color); + } + return zmk_rgb_ug_on() | zmk_rgb_ug_set_spd(5) | + zmk_rgb_ug_select_effect(UNDERGLOW_EFFECT_BREATHE) | zmk_rgb_ug_set_hsb(color); + } + return 0; +} + +static void rgb_underglow_ble_status_timeout_work(struct k_work *work) { + zmk_rgb_underglow_apply_current_state(); +} + +K_WORK_DEFINE(underglow_ble_timeout_work, rgb_underglow_ble_status_timeout_work); + +static void rgb_underglow_ble_status_timeout_timer(struct k_timer *timer) { + k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_ble_timeout_work); +} + +K_TIMER_DEFINE(underglow_ble_timeout_timer, rgb_underglow_ble_status_timeout_timer, NULL); + +static int rgb_underglow_ble_state_event_listener(const zmk_event_t *eh) { + const struct output_state sc = zmk_get_output_state(); + + if (sc.selected_endpoint.transport == ZMK_TRANSPORT_USB) + return 0; + + if (is_starting_up()) + return 0; + + if (sc.active_profile_connected) + k_timer_start(&underglow_ble_timeout_timer, K_SECONDS(2), K_NO_WAIT); + + return zmk_rgb_underglow_set_color_ble(sc); +} + +ZMK_LISTENER(rgb_ble, rgb_underglow_ble_state_event_listener); +ZMK_SUBSCRIPTION(rgb_ble, zmk_endpoint_changed); +ZMK_SUBSCRIPTION(rgb_ble, zmk_ble_active_profile_changed); diff --git a/app/src/rgb_underglow/current_status.c b/app/src/rgb_underglow/current_status.c new file mode 100644 index 00000000000..2d27bbdefd7 --- /dev/null +++ b/app/src/rgb_underglow/current_status.c @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#if !DT_HAS_CHOSEN(zmk_underglow) + +#error "A zmk,underglow chosen node must be declared" + +#endif + +BUILD_ASSERT(CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN <= CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX, + "ERROR: RGB underglow maximum brightness is less than minimum brightness"); + +static struct rgb_underglow_state *preserved_state; + +int zmk_rgb_underglow_get_state(bool *on_off) { + *on_off = preserved_state->on; + return 0; +} + +int zmk_rgb_underglow_on(void) { + preserved_state->on = true; + return zmk_rgb_ug_on() | zmk_rgb_ug_save_state(); +} + +int zmk_rgb_underglow_off(void) { + preserved_state->on = false; + return zmk_rgb_ug_off() | zmk_rgb_ug_save_state(); +} + +int zmk_rgb_underglow_calc_effect(int direction) { + return (preserved_state->current_effect + UNDERGLOW_EFFECT_NUMBER + direction) % + UNDERGLOW_EFFECT_NUMBER; +} + +int zmk_rgb_underglow_select_effect(int effect) { + preserved_state->current_effect = effect; + return zmk_rgb_ug_select_effect(effect) | zmk_rgb_ug_save_state(); +} + +int zmk_rgb_underglow_cycle_effect(int direction) { + return zmk_rgb_underglow_select_effect(zmk_rgb_underglow_calc_effect(direction)); +} + +int zmk_rgb_underglow_toggle(void) { + return preserved_state->on ? zmk_rgb_underglow_off() + : zmk_rgb_underglow_on() | zmk_rgb_ug_save_state(); +} + +int zmk_rgb_underglow_set_hsb(struct zmk_led_hsb color) { + preserved_state->color = color; + return zmk_rgb_ug_set_hsb(color) | zmk_rgb_ug_save_state(); +} + +struct zmk_led_hsb zmk_rgb_underglow_calc_hue(int direction) { + struct zmk_led_hsb color = preserved_state->color; + + color.h += HUE_MAX + (direction * CONFIG_ZMK_RGB_UNDERGLOW_HUE_STEP); + color.h %= HUE_MAX; + + return color; +} + +struct zmk_led_hsb zmk_rgb_underglow_calc_sat(int direction) { + struct zmk_led_hsb color = preserved_state->color; + + int s = color.s + (direction * CONFIG_ZMK_RGB_UNDERGLOW_SAT_STEP); + if (s < 0) { + s = 0; + } else if (s > SAT_MAX) { + s = SAT_MAX; + } + color.s = s; + + return color; +} + +struct zmk_led_hsb zmk_rgb_underglow_calc_brt(int direction) { + struct zmk_led_hsb color = preserved_state->color; + + int b = color.b + (direction * CONFIG_ZMK_RGB_UNDERGLOW_BRT_STEP); + color.b = CLAMP(b, 0, BRT_MAX); + + return color; +} + +int zmk_rgb_underglow_change_hue(int direction) { + preserved_state->color = zmk_rgb_underglow_calc_hue(direction); + return zmk_rgb_ug_set_hsb(preserved_state->color) | zmk_rgb_ug_save_state(); +} + +int zmk_rgb_underglow_change_sat(int direction) { + preserved_state->color = zmk_rgb_underglow_calc_sat(direction); + return zmk_rgb_ug_set_hsb(preserved_state->color) | zmk_rgb_ug_save_state(); +} + +int zmk_rgb_underglow_change_brt(int direction) { + preserved_state->color = zmk_rgb_underglow_calc_brt(direction); + return zmk_rgb_ug_set_hsb(preserved_state->color) | zmk_rgb_ug_save_state(); +} + +int zmk_rgb_underglow_change_spd(int direction) { + int speed = preserved_state->animation_speed + direction; + speed = CLAMP(speed, 1, 5); + preserved_state->animation_speed = speed; + return zmk_rgb_ug_set_spd(speed) | zmk_rgb_ug_save_state(); +} + +int zmk_rgb_underglow_apply_current_state(void) { + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) + if (zmk_activity_get_state() != ZMK_ACTIVITY_ACTIVE) { + return zmk_rgb_ug_off(); + } +#endif + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) + if (!zmk_usb_is_powered()) { + return zmk_rgb_ug_off(); + } +#endif + + if (zmk_rgb_ug_set_hsb(preserved_state->color) || + zmk_rgb_ug_set_spd(preserved_state->animation_speed) || + zmk_rgb_ug_select_effect(preserved_state->current_effect)) { + LOG_ERR("Failed to set the current rgb config"); + return 0; + } + + return preserved_state->on ? zmk_rgb_ug_on() : zmk_rgb_ug_off(); +} + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) || \ + IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) +static int rgb_underglow_auto_state(bool new_state) { + if (new_state) { + return zmk_rgb_underglow_on(); + } else { + return zmk_rgb_underglow_off(); + } +} + +static int rgb_underglow_event_listener(const zmk_event_t *eh) { + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) + if (as_zmk_activity_state_changed(eh)) { + return rgb_underglow_auto_state(zmk_activity_get_state() == ZMK_ACTIVITY_ACTIVE); + } +#endif + +#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()); + } +#endif + + return -ENOTSUP; +} + +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) + +void zmk_rgb_underglow_init(void) { preserved_state = zmk_rgb_ug_get_save_state(); } + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) +ZMK_SUBSCRIPTION(rgb_underglow, zmk_activity_state_changed); +#endif + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) +ZMK_SUBSCRIPTION(rgb_underglow, zmk_usb_conn_state_changed); +#endif diff --git a/app/src/rgb_underglow/init.c b/app/src/rgb_underglow/init.c new file mode 100644 index 00000000000..007f0b8406d --- /dev/null +++ b/app/src/rgb_underglow/init.c @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +static const struct device *led_strip; +static struct rgb_underglow_state *state; + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) +static const struct device *const ext_power = DEVICE_DT_GET(DT_INST(0, zmk_ext_power_generic)); +#endif + +K_WORK_DEFINE(underglow_tick_work, zmk_rgb_ug_tick); + +static void zmk_rgb_ug_tick_handler(struct k_timer *timer) { + if (!state->on) { + return; + } + + k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_tick_work); +} + +K_TIMER_DEFINE(underglow_tick, zmk_rgb_ug_tick_handler, NULL); + +int zmk_rgb_ug_on(void) { + if (!led_strip) + return -ENODEV; + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) + if (ext_power != NULL) { + int rc = ext_power_enable(ext_power); + if (rc != 0) { + LOG_ERR("Unable to enable EXT_POWER: %d", rc); + } + } +#endif + + state->on = true; + state->animation_step = 0; + k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50)); + + return 0; +} + +static void zmk_rgb_ug_off_handler(struct k_work *work) { + struct led_rgb pixels[STRIP_NUM_PIXELS]; + for (int i = 0; i < STRIP_NUM_PIXELS; i++) { + pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0}; + } + + led_strip_update_rgb(led_strip, pixels, STRIP_NUM_PIXELS); +} + +K_WORK_DEFINE(underglow_off_work, zmk_rgb_ug_off_handler); + +int zmk_rgb_ug_off(void) { + if (!led_strip) + return -ENODEV; + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) + if (ext_power != NULL) { + int rc = ext_power_disable(ext_power); + if (rc != 0) { + LOG_ERR("Unable to disable EXT_POWER: %d", rc); + } + } +#endif + + k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_off_work); + + k_timer_stop(&underglow_tick); + state->on = false; + + return 0; +} + +static int zmk_rgb_ug_init(void) { + led_strip = DEVICE_DT_GET(STRIP_CHOSEN); + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) + if (!device_is_ready(ext_power)) { + LOG_ERR("External power device \"%s\" is not ready", ext_power->name); + return -ENODEV; + } +#endif + + zmk_rgb_ug_state_init(); + zmk_rgb_ug_tools_init(led_strip); + zmk_rgb_underglow_init(); + + state = zmk_rgb_ug_get_state(); +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB) + state->on = zmk_usb_is_powered(); +#endif + + if (state->on) { + k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50)); + } + + return 0; +} + +SYS_INIT(zmk_rgb_ug_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY); diff --git a/app/src/rgb_underglow/rgb_underglow_base.c b/app/src/rgb_underglow/rgb_underglow_base.c new file mode 100644 index 00000000000..6d087229a0b --- /dev/null +++ b/app/src/rgb_underglow/rgb_underglow_base.c @@ -0,0 +1,214 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include + +#include + +#include +#include + +#include + +#include +#include + +#include +#include + +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +BUILD_ASSERT(CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN <= CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX, + "ERROR: RGB underglow maximum brightness is less than minimum brightness"); + +static const struct device *led_strip; + +static struct led_rgb pixels[STRIP_NUM_PIXELS]; + +static struct rgb_underglow_state *state; + +#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) +static const struct device *const ext_power = DEVICE_DT_GET(DT_INST(0, zmk_ext_power_generic)); +#endif + +static struct zmk_led_hsb hsb_scale_min_max(struct zmk_led_hsb hsb) { + hsb.b = CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN + + (CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX - CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN) * hsb.b / BRT_MAX; + return hsb; +} + +static struct zmk_led_hsb hsb_scale_zero_max(struct zmk_led_hsb hsb) { + hsb.b = hsb.b * CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX / BRT_MAX; + return hsb; +} + +static struct led_rgb hsb_to_rgb(struct zmk_led_hsb hsb) { + float r, g, b; + + uint8_t i = hsb.h / 60; + float v = hsb.b / ((float)BRT_MAX); + float s = hsb.s / ((float)SAT_MAX); + float f = hsb.h / ((float)HUE_MAX) * 6 - i; + float p = v * (1 - s); + float q = v * (1 - f * s); + float t = v * (1 - (1 - f) * s); + + switch (i % 6) { + case 0: + r = v; + g = t; + b = p; + break; + case 1: + r = q; + g = v; + b = p; + break; + case 2: + r = p; + g = v; + b = t; + break; + case 3: + r = p; + g = q; + b = v; + break; + case 4: + r = t; + g = p; + b = v; + break; + case 5: + r = v; + g = p; + b = q; + break; + } + + struct led_rgb rgb = {r : r * 255, g : g * 255, b : b * 255}; + + return rgb; +} + +static void zmk_rgb_ug_effect_solid(void) { + for (int i = 0; i < STRIP_NUM_PIXELS; i++) { + pixels[i] = hsb_to_rgb(hsb_scale_min_max(state->color)); + } +} + +static void zmk_rgb_ug_effect_breathe(void) { + for (int i = 0; i < STRIP_NUM_PIXELS; i++) { + struct zmk_led_hsb hsb = state->color; + hsb.b = abs(state->animation_step - 1200) / 12; + + pixels[i] = hsb_to_rgb(hsb_scale_zero_max(hsb)); + } + + state->animation_step += state->animation_speed * 10; + + if (state->animation_step > 2400) { + state->animation_step = 0; + } +} + +static void zmk_rgb_ug_effect_spectrum(void) { + for (int i = 0; i < STRIP_NUM_PIXELS; i++) { + struct zmk_led_hsb hsb = state->color; + hsb.h = state->animation_step; + + pixels[i] = hsb_to_rgb(hsb_scale_min_max(hsb)); + } + + state->animation_step += state->animation_speed; + state->animation_step = state->animation_step % HUE_MAX; +} + +static void zmk_rgb_ug_effect_swirl(void) { + for (int i = 0; i < STRIP_NUM_PIXELS; i++) { + struct zmk_led_hsb hsb = state->color; + hsb.h = (HUE_MAX / STRIP_NUM_PIXELS * i + state->animation_step) % HUE_MAX; + + pixels[i] = hsb_to_rgb(hsb_scale_min_max(hsb)); + } + + state->animation_step += state->animation_speed * 2; + state->animation_step = state->animation_step % HUE_MAX; +} + +void zmk_rgb_ug_tick(struct k_work *work) { + switch (state->current_effect) { + case UNDERGLOW_EFFECT_SOLID: + zmk_rgb_ug_effect_solid(); + break; + case UNDERGLOW_EFFECT_BREATHE: + zmk_rgb_ug_effect_breathe(); + break; + case UNDERGLOW_EFFECT_SPECTRUM: + zmk_rgb_ug_effect_spectrum(); + break; + case UNDERGLOW_EFFECT_SWIRL: + zmk_rgb_ug_effect_swirl(); + break; + } + + 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); + } +} + +int zmk_rgb_ug_select_effect(int effect) { + if (!led_strip) + return -ENODEV; + + if (effect < 0 || effect >= UNDERGLOW_EFFECT_NUMBER) { + return -EINVAL; + } + + if (effect == state->current_effect) { + return 0; + } + + state->current_effect = effect; + state->animation_step = 0; + + return 0; +} + +int zmk_rgb_ug_set_spd(int speed) { + if (!led_strip) + return -ENODEV; + + int clamped_speed = CLAMP(speed, 1, 5); + + if (clamped_speed == state->animation_speed) + return 0; + + state->animation_speed = clamped_speed; + + return 0; +} + +int zmk_rgb_ug_set_hsb(struct zmk_led_hsb color) { + if (color.h > HUE_MAX || color.s > SAT_MAX || color.b > BRT_MAX) { + return -ENOTSUP; + } + + state->color = color; + + return 0; +} + +void zmk_rgb_ug_tools_init(const struct device *led_strip_dev) { + led_strip = led_strip_dev; + state = zmk_rgb_ug_get_state(); +} diff --git a/app/src/rgb_underglow/startup_mutex.c b/app/src/rgb_underglow/startup_mutex.c new file mode 100644 index 00000000000..c1bab8cdcd7 --- /dev/null +++ b/app/src/rgb_underglow/startup_mutex.c @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +static K_MUTEX_DEFINE(startup_mutex); +bool starting_up = false; + +bool set_starting_up(bool value) { + if (k_mutex_lock(&startup_mutex, K_MSEC(300)) != 0) { + LOG_WRN("Can't start startup sequence, mutex is locked"); + return false; + } + starting_up = value; + int unlock = k_mutex_unlock(&startup_mutex); + return true; +} + +bool is_starting_up() { + if (k_mutex_lock(&startup_mutex, K_MSEC(300)) != 0) { + return true; + } else { + bool ret = starting_up; + int unlock = k_mutex_unlock(&startup_mutex); + return ret; + } +} + +bool start_startup() { return set_starting_up(true); } + +void stop_startup() { return set_starting_up(false); } diff --git a/app/src/rgb_underglow/state.c b/app/src/rgb_underglow/state.c new file mode 100644 index 00000000000..89b8d3c5b19 --- /dev/null +++ b/app/src/rgb_underglow/state.c @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include +#include +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +static struct rgb_underglow_state state; +static struct rgb_underglow_state preserved_state; + +#if IS_ENABLED(CONFIG_SETTINGS) +static int rgb_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) { + const char *next; + int rc; + + if (settings_name_steq(name, "state", &next) && !next) { + if (len != sizeof(preserved_state)) { + return -EINVAL; + } + + rc = read_cb(cb_arg, &preserved_state, sizeof(preserved_state)); + if (rc >= 0) { + return 0; + } + + return rc; + } + + return -ENOENT; +} + +struct settings_handler rgb_conf = {.name = "rgb/underglow", .h_set = rgb_settings_set}; + +static void zmk_rgb_ug_save_state_work(struct k_work *_work) { + settings_save_one("rgb/underglow/state", &preserved_state, sizeof(preserved_state)); +} + +static struct k_work_delayable underglow_save_work; +#endif + +struct rgb_underglow_state *zmk_rgb_ug_get_state(void) { return &state; } +struct rgb_underglow_state *zmk_rgb_ug_get_save_state(void) { return &preserved_state; } + +int zmk_rgb_ug_state_init(void) { + state = default_rgb_settings; + preserved_state = default_rgb_settings; +#if IS_ENABLED(CONFIG_SETTINGS) + settings_subsys_init(); + + int err = settings_register(&rgb_conf); + if (err) { + LOG_ERR("Failed to register the ext_power settings handler (err %d)", err); + return err; + } + + k_work_init_delayable(&underglow_save_work, zmk_rgb_ug_save_state_work); + + settings_load_subtree("rgb/underglow"); +#endif + return 0; +} + +int zmk_rgb_ug_save_state(void) { +#if IS_ENABLED(CONFIG_SETTINGS) + int ret = k_work_reschedule(&underglow_save_work, K_MSEC(CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE)); + return MIN(ret, 0); +#else + return 0; +#endif +} \ No newline at end of file diff --git a/app/src/rgb_underglow/status_on_startup.c b/app/src/rgb_underglow/status_on_startup.c new file mode 100644 index 00000000000..9cac3be186f --- /dev/null +++ b/app/src/rgb_underglow/status_on_startup.c @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2025 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#define IS_PERIPHERAL (IS_ENABLED(CONFIG_ZMK_SPLIT) && !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL)) + +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +enum STARTUP_STATE { + BATTERY, + CONNECTING, + CONNECTED, +}; +static enum zmk_activity_state last_activity_state = ZMK_ACTIVITY_SLEEP; +static int64_t last_checkpoint = 0; +#if CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS +static enum STARTUP_STATE startup_state = BATTERY; +#else +static enum STARTUP_STATE startup_state = CONNECTING; +#endif // CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS +static bool switched_startup_state = true; +static struct k_timer *running_timer; + +static void zmk_on_startup_timer_tick_work(struct k_work *work) { +#if CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS + uint8_t state_of_charge = zmk_battery_state_of_charge(); +#endif // CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS +#if CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS +#if !IS_PERIPHERAL + struct output_state os = zmk_get_output_state(); + if (os.selected_endpoint.transport == ZMK_TRANSPORT_USB) { + k_timer_stop(running_timer); + zmk_rgb_underglow_apply_current_state(); + return; + } +#else + struct peripheral_ble_state ps = zmk_get_ble_peripheral_state(); +#endif // !IS_PERIPHERAL +#endif // CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS + + int64_t uptime = k_uptime_get(); + if (last_checkpoint + 3000 < uptime && startup_state != CONNECTING) { + switch (startup_state) { + case BATTERY: +#if CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS +#if !IS_PERIPHERAL + startup_state = os.active_profile_connected ? CONNECTED : CONNECTING; +#else + startup_state = ps.connected ? CONNECTED : CONNECTING; +#endif // !IS_PERIPHERAL + switched_startup_state = true; +#else + k_timer_stop(running_timer); // probably won't work + zmk_rgb_underglow_apply_current_state(); + return; +#endif // CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS + last_checkpoint = uptime; + break; + case CONNECTED: + k_timer_stop(running_timer); // probably won't work + zmk_rgb_underglow_apply_current_state(); + return; + } + } + +#if CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS +#if !IS_PERIPHERAL + if (startup_state == CONNECTING && os.active_profile_connected) { +#else + if (startup_state == CONNECTING && ps.connected) { +#endif // !IS_PERIPHERAL + startup_state = CONNECTED; + last_checkpoint = uptime; + switched_startup_state = true; + } +#endif // CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS + + if (switched_startup_state) { + switched_startup_state = false; + switch (startup_state) { +#if CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS + case BATTERY: + rgb_underglow_set_color_battery(state_of_charge); + return; +#endif // CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS +#if CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS + case CONNECTING: +#if !IS_PERIPHERAL + zmk_rgb_underglow_set_color_ble(os); +#else + zmk_rgb_underglow_set_color_ble_peripheral(ps); +#endif // !IS_PERIPHERAL + + case CONNECTED: +#if !IS_PERIPHERAL + zmk_rgb_underglow_set_color_ble(os); +#else + zmk_rgb_underglow_set_color_ble_peripheral(ps); +#endif // !IS_PERIPHERAL +#endif // CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS + return; + default: + return; + } + } +} + +K_WORK_DEFINE(on_startup_timer_tick_work, zmk_on_startup_timer_tick_work); + +static void on_startup_timer_tick_stop_cb(struct k_timer *timer) { stop_startup(); } + +static void on_startup_timer_tick_cb(struct k_timer *timer) { + running_timer = timer; + k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &on_startup_timer_tick_work); +} + +K_TIMER_DEFINE(on_startup_timer_tick, on_startup_timer_tick_cb, on_startup_timer_tick_stop_cb); + +void init() { + if (!start_startup()) { + LOG_ERR("Cannot start startup sequence, startup sequence already started"); + return; + } + +#if CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS + startup_state = BATTERY; +#else + startup_state = CONNECTING; +#endif // CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS + + last_checkpoint = k_uptime_get(); + k_timer_start(&on_startup_timer_tick, K_NO_WAIT, K_MSEC(100)); +} + +int startup(const enum zmk_activity_state state) { + switch (state) { + case ZMK_ACTIVITY_ACTIVE: + if (last_activity_state == ZMK_ACTIVITY_SLEEP) { + init(); + break; + } + default: + last_activity_state = state; + if (is_starting_up()) { + k_timer_stop(&on_startup_timer_tick); + return zmk_rgb_underglow_apply_current_state(); + } + break; + } + + return 0; +} + +int startup_handler(const zmk_event_t *eh) { + struct zmk_activity_state_changed *ev = as_zmk_activity_state_changed(eh); + if (ev == NULL) { + return -ENOTSUP; + } + + return startup(ev->state); +} + +static int startup_init(void) { + last_activity_state = ZMK_ACTIVITY_SLEEP; + return startup(ZMK_ACTIVITY_ACTIVE); +} + +ZMK_LISTENER(status_on_startup, startup_handler); +ZMK_SUBSCRIPTION(status_on_startup, zmk_activity_state_changed); +SYS_INIT(startup_init, APPLICATION, CONFIG_ZMK_USB_HID_INIT_PRIORITY); diff --git a/docs/docs/config/lighting.md b/docs/docs/config/lighting.md index 8734c70d647..a170f44fd11 100644 --- a/docs/docs/config/lighting.md +++ b/docs/docs/config/lighting.md @@ -17,23 +17,27 @@ RGB underglow depends on [Zephyr's LED strip driver](https://github.com/zephyrpr Definition file: [zmk/app/Kconfig](https://github.com/zmkfirmware/zmk/blob/main/app/Kconfig) -| Config | Type | Description | Default | -| ---------------------------------------- | ---- | --------------------------------------------------------- | ------- | -| `CONFIG_ZMK_RGB_UNDERGLOW` | bool | Enable RGB underglow | n | -| `CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER` | bool | Underglow toggling also controls external power | y | -| `CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE` | bool | Turn off RGB underglow when keyboard goes into idle state | n | -| `CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB` | bool | Turn off RGB underglow when USB is disconnected | n | -| `CONFIG_ZMK_RGB_UNDERGLOW_HUE_STEP` | int | Hue step in degrees (0-359) used by RGB actions | 10 | -| `CONFIG_ZMK_RGB_UNDERGLOW_SAT_STEP` | int | Saturation step in percent used by RGB actions | 10 | -| `CONFIG_ZMK_RGB_UNDERGLOW_BRT_STEP` | int | Brightness step in percent used by RGB actions | 10 | -| `CONFIG_ZMK_RGB_UNDERGLOW_HUE_START` | int | Default hue in degrees (0-359) | 0 | -| `CONFIG_ZMK_RGB_UNDERGLOW_SAT_START` | int | Default saturation percent (0-100) | 100 | -| `CONFIG_ZMK_RGB_UNDERGLOW_BRT_START` | int | Default brightness in percent (0-100) | 100 | -| `CONFIG_ZMK_RGB_UNDERGLOW_SPD_START` | int | Default effect speed (1-5) | 3 | -| `CONFIG_ZMK_RGB_UNDERGLOW_EFF_START` | int | Default effect index from the effect list (see below) | 0 | -| `CONFIG_ZMK_RGB_UNDERGLOW_ON_START` | bool | Default on state | y | -| `CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN` | int | Minimum brightness in percent (0-100) | 0 | -| `CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX` | int | Maximum brightness in percent (0-100) | 100 | +| Config | Type | Description | Default | +| ----------------------------------------- | ---- | ------------------------------------------------------------------------- | ------- | +| `CONFIG_ZMK_RGB_UNDERGLOW` | bool | Enable RGB underglow | n | +| `CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER` | bool | Underglow toggling also controls external power | y | +| `CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE` | bool | Turn off RGB underglow when keyboard goes into idle state | n | +| `CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB` | bool | Turn off RGB underglow when USB is disconnected | n | +| `CONFIG_ZMK_RGB_UNDERGLOW_HUE_STEP` | int | Hue step in degrees (0-359) used by RGB actions | 10 | +| `CONFIG_ZMK_RGB_UNDERGLOW_SAT_STEP` | int | Saturation step in percent used by RGB actions | 10 | +| `CONFIG_ZMK_RGB_UNDERGLOW_BRT_STEP` | int | Brightness step in percent used by RGB actions | 10 | +| `CONFIG_ZMK_RGB_UNDERGLOW_HUE_START` | int | Default hue in degrees (0-359) | 0 | +| `CONFIG_ZMK_RGB_UNDERGLOW_SAT_START` | int | Default saturation percent (0-100) | 100 | +| `CONFIG_ZMK_RGB_UNDERGLOW_BRT_START` | int | Default brightness in percent (0-100) | 100 | +| `CONFIG_ZMK_RGB_UNDERGLOW_SPD_START` | int | Default effect speed (1-5) | 3 | +| `CONFIG_ZMK_RGB_UNDERGLOW_EFF_START` | int | Default effect index from the effect list (see below) | 0 | +| `CONFIG_ZMK_RGB_UNDERGLOW_ON_START` | bool | Default on state | y | +| `CONFIG_ZMK_RGB_UNDERGLOW_BRT_MIN` | int | Minimum brightness in percent (0-100) | 0 | +| `CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX` | int | Maximum brightness in percent (0-100) | 100 | +| `CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_STATUS` | bool | Display battery status on startup and when reaching critcal level | n | +| `CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_CRIT` | int | Battery level considered critical in percent (0-100) | 5 | +| `CONFIG_ZMK_RGB_UNDERGLOW_BATTERY_LOW` | int | Battery level considered low in percent (0-100) | 15 | +| `CONFIG_ZMK_RGB_UNDERGLOW_BLE_STATUS` | bool | Display blutooth connection status on startup and when connection changes | n | Values for `CONFIG_ZMK_RGB_UNDERGLOW_EFF_START`: