/* * 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 #include #include #include #include #include #include #include #include #if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) #include #endif #if !IS_ENABLED(CONFIG_ZMK_SPLIT_ROLE_CENTRAL) #include #endif 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_LAYER_INDICATORS, 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; bool status_active; uint16_t status_animation_step; }; static const struct device *led_strip; static struct led_rgb pixels[STRIP_NUM_PIXELS]; static struct led_rgb status_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 void zmk_rgb_set_ext_power(void); 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 int zmk_led_generate_status(void); static void zmk_led_write_pixels(void) { static struct led_rgb led_buffer[STRIP_NUM_PIXELS]; int bat0; int blend = 0; int reset_ext_power = 0; #if IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING) bat0 = zmk_battery_state_of_charge(); #else bat0 = 100; #endif if (state.status_active) { blend = zmk_led_generate_status(); } // fast path: no status indicators, battery level OK if (blend == 0 && bat0 >= 20) { led_strip_update_rgb(led_strip, pixels, STRIP_NUM_PIXELS); return; } // battery below minimum charge if (bat0 < 10) { memset(pixels, 0, sizeof(struct led_rgb) * STRIP_NUM_PIXELS); if (state.on) { int c_power = ext_power_get(ext_power); if (c_power && !state.status_active) { // power is on, RGB underglow is on, but battery is too low state.on = false; reset_ext_power = true; } } } if (blend == 0) { for (int i = 0; i < STRIP_NUM_PIXELS; i++) { led_buffer[i] = pixels[i]; } } else if (blend >= 256) { for (int i = 0; i < STRIP_NUM_PIXELS; i++) { led_buffer[i] = status_pixels[i]; } } else if (blend < 256) { uint16_t blend_l = blend; uint16_t blend_r = 256 - blend; for (int i = 0; i < STRIP_NUM_PIXELS; i++) { led_buffer[i].r = ((status_pixels[i].r * blend_l) >> 8) + ((pixels[i].r * blend_r) >> 8); led_buffer[i].g = ((status_pixels[i].g * blend_l) >> 8) + ((pixels[i].g * blend_r) >> 8); led_buffer[i].b = ((status_pixels[i].b * blend_l) >> 8) + ((pixels[i].b * blend_r) >> 8); } } // battery below 20%, reduce LED brightness if (bat0 < 20) { for (int i = 0; i < STRIP_NUM_PIXELS; i++) { led_buffer[i].r = led_buffer[i].r >> 1; led_buffer[i].g = led_buffer[i].g >> 1; led_buffer[i].b = led_buffer[i].b >> 1; } } int err = led_strip_update_rgb(led_strip, led_buffer, STRIP_NUM_PIXELS); if (err < 0) { LOG_ERR("Failed to update the RGB strip (%d)", err); } if (reset_ext_power) { zmk_rgb_set_ext_power(); } } #define UNDERGLOW_INDICATORS DT_PATH(underglow_indicators) #if defined(DT_N_S_underglow_indicators_EXISTS) #define UNDERGLOW_INDICATORS_ENABLED 1 #define LEFT_HALF #else #define UNDERGLOW_INDICATORS_ENABLED 0 #define RIGHT_HALF #endif #if !UNDERGLOW_INDICATORS_ENABLED static int zmk_led_generate_status(void) { return 0; } #else const uint8_t underglow_layer_state[] = DT_PROP(UNDERGLOW_INDICATORS, layer_state); const uint8_t underglow_ble_state[] = DT_PROP(UNDERGLOW_INDICATORS, ble_state); const uint8_t underglow_bat_lhs[] = DT_PROP(UNDERGLOW_INDICATORS, bat_lhs); const uint8_t underglow_bat_rhs[] = DT_PROP(UNDERGLOW_INDICATORS, bat_rhs); #define HEXRGB(R, G, B) \ ((struct led_rgb){ \ r : (CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX * (R)) / 0xff, \ g : (CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX * (G)) / 0xff, \ b : (CONFIG_ZMK_RGB_UNDERGLOW_BRT_MAX * (B)) / 0xff \ }) const struct led_rgb red = HEXRGB(0xff, 0x00, 0x00); const struct led_rgb yellow = HEXRGB(0xff, 0xff, 0x00); const struct led_rgb green = HEXRGB(0x00, 0xff, 0x00); const struct led_rgb dull_green = HEXRGB(0x00, 0xff, 0x68); const struct led_rgb magenta = HEXRGB(0xff, 0x00, 0xff); const struct led_rgb white = HEXRGB(0xff, 0xff, 0xff); const struct led_rgb lilac = HEXRGB(0x6b, 0x1f, 0xce); static void zmk_led_battery_level(int bat_level, const uint8_t *addresses, size_t addresses_len) { struct led_rgb bat_colour; if (bat_level > 40) { bat_colour = green; } else if (bat_level > 20) { bat_colour = yellow; } else { bat_colour = red; } // originally, six levels, 0 .. 100 for (int i = 0; i < addresses_len; i++) { int min_level = (i * 100) / (addresses_len - 1); if (bat_level >= min_level) { status_pixels[addresses[i]] = bat_colour; } } } static void zmk_led_fill(struct led_rgb color, const uint8_t *addresses, size_t addresses_len) { for (int i = 0; i < addresses_len; i++) { status_pixels[addresses[i]] = color; } } #define ZMK_LED_NUMLOCK_BIT BIT(0) #define ZMK_LED_CAPSLOCK_BIT BIT(1) #define ZMK_LED_SCROLLLOCK_BIT BIT(2) static int zmk_led_generate_status(void) { for (int i = 0; i < STRIP_NUM_PIXELS; i++) { status_pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0}; } // BATTERY STATUS #if IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING) zmk_led_battery_level(zmk_battery_state_of_charge(), underglow_bat_lhs, DT_PROP_LEN(UNDERGLOW_INDICATORS, bat_lhs)); #if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING) uint8_t peripheral_level = 0; int rc = zmk_split_central_get_peripheral_battery_level(0, &peripheral_level); if (rc == 0) { zmk_led_battery_level(peripheral_level, underglow_bat_rhs, DT_PROP_LEN(UNDERGLOW_INDICATORS, bat_rhs)); } else if (rc == -ENOTCONN) { zmk_led_fill(red, underglow_bat_rhs, DT_PROP_LEN(UNDERGLOW_INDICATORS, bat_rhs)); } else if (rc == -EINVAL) { LOG_ERR("Invalid peripheral index requested for battery level read: 0"); } #endif // CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING #endif // CONFIG_ZMK_BATTERY_REPORTING // CAPSLOCK/NUMLOCK/SCROLLOCK STATUS zmk_hid_indicators_t led_flags = zmk_hid_indicators_get_current_profile(); if (led_flags & ZMK_LED_CAPSLOCK_BIT) status_pixels[DT_PROP(UNDERGLOW_INDICATORS, capslock)] = red; if (led_flags & ZMK_LED_NUMLOCK_BIT) status_pixels[DT_PROP(UNDERGLOW_INDICATORS, numlock)] = red; if (led_flags & ZMK_LED_SCROLLLOCK_BIT) status_pixels[DT_PROP(UNDERGLOW_INDICATORS, scrolllock)] = red; // LAYER STATUS for (uint8_t i = 0; i < DT_PROP_LEN(UNDERGLOW_INDICATORS, layer_state); i++) { if (zmk_keymap_layer_active(i)) status_pixels[underglow_layer_state[i]] = magenta; } struct zmk_endpoint_instance active_endpoint = zmk_endpoints_selected(); if (!zmk_endpoints_preferred_transport_is_active()) status_pixels[DT_PROP(UNDERGLOW_INDICATORS, output_fallback)] = red; #if IS_ENABLED(CONFIG_ZMK_BLE) int active_ble_profile_index = zmk_ble_active_profile_index(); for (uint8_t i = 0; i < MIN(ZMK_BLE_PROFILE_COUNT, DT_PROP_LEN(UNDERGLOW_INDICATORS, ble_state)); i++) { int8_t status = zmk_ble_profile_status(i); int ble_pixel = underglow_ble_state[i]; if (status == 2 && active_endpoint.transport == ZMK_TRANSPORT_BLE && active_ble_profile_index == i) { // connected AND active status_pixels[ble_pixel] = white; } else if (status == 2) { // connected status_pixels[ble_pixel] = dull_green; } else if (status == 1) { // paired status_pixels[ble_pixel] = red; } else if (status == 0) { // unused status_pixels[ble_pixel] = lilac; } } #endif enum zmk_usb_conn_state usb_state = zmk_usb_get_conn_state(); if (usb_state == ZMK_USB_CONN_HID && active_endpoint.transport == ZMK_TRANSPORT_USB) { // connected AND active status_pixels[DT_PROP(UNDERGLOW_INDICATORS, usb_state)] = white; } else if (usb_state == ZMK_USB_CONN_HID) { // connected status_pixels[DT_PROP(UNDERGLOW_INDICATORS, usb_state)] = dull_green; } else if (usb_state == ZMK_USB_CONN_POWERED) { // powered status_pixels[DT_PROP(UNDERGLOW_INDICATORS, usb_state)] = red; } else if (usb_state == ZMK_USB_CONN_NONE) { // disconnected status_pixels[DT_PROP(UNDERGLOW_INDICATORS, usb_state)] = lilac; } int16_t blend = 256; if (state.status_animation_step < (500 / 25)) { blend = ((state.status_animation_step * 256) / (500 / 25)); } else if (state.status_animation_step > (8000 / 25)) { blend = 256 - (((state.status_animation_step - (8000 / 25)) * 256) / (2000 / 25)); } if (blend < 0) blend = 0; if (blend > 256) blend = 256; return blend; } #endif // underglow_indicators exists static inline struct led_rgb hue_sat(int hue, int sat) { struct zmk_led_hsb hsb = state.color; hsb.h = hue; hsb.s = sat; return hsb_to_rgb(hsb_scale_min_max(hsb)); } static struct led_rgb hex_to_rgb(uint8_t r, uint8_t g, uint8_t b) { struct zmk_led_hsb hsb = state.color; return (struct led_rgb){ r : (hsb.b * (r)) / 0xff, g : (hsb.b * (g)) / 0xff, b : (hsb.b * (b)) / 0xff }; } static void zmk_rgb_underglow_apply_rgbmap(uint32_t rgbmap[], size_t rgbmap_len) { // TODO: Glove80 specifics, move that part to board's devicetree #ifdef LEFT_HALF const uint8_t LED_MATRIX[] = {52, 53, 54, 69, 70, 71, 15, 27, 39, 51, 4, 14, 26, 38, 50, 68, 3, 13, 25, 37, 49, 67, 2, 12, 24, 36, 48, 66, 1, 11, 23, 35, 47, 65, 0, 10, 22, 34, 46, 64}; #else const uint8_t LED_MATRIX[] = {57, 56, 55, 74, 73, 72, 16, 28, 40, 58, 5, 17, 29, 41, 59, 75, 6, 18, 30, 42, 60, 76, 7, 19, 31, 43, 61, 77, 8, 20, 32, 44, 62, 78, 9, 21, 33, 45, 63, 79}; #endif for (int i = 0; i < STRIP_NUM_PIXELS; i++) { uint8_t midx = LED_MATRIX[i]; if (midx >= ZMK_KEYMAP_LEN) { LOG_DBG("out of range"); } else { pixels[i] = hex_to_rgb((rgbmap[midx] & 0xFF0000) >> 16, (rgbmap[midx] & 0xFF00) >> 8, rgbmap[midx] & 0xFF); } } } static void zmk_rgb_underglow_set_layer(uint8_t layer) { state.on = true; uint32_t *rgbmap = rgb_underglow_get_bindings(layer); if (rgbmap != NULL) { zmk_rgb_underglow_apply_rgbmap(rgbmap, ZMK_KEYMAP_LEN); } else { for (int i = 0; i < STRIP_NUM_PIXELS; i++) { pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0}; } state.on = false; } zmk_led_write_pixels(); zmk_rgb_set_ext_power(); } 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; case UNDERGLOW_EFFECT_LAYER_INDICATORS: //zmk_rgb_underglow_set_layer(); break; } zmk_led_write_pixels(); } K_WORK_DEFINE(underglow_tick_work, zmk_rgb_underglow_tick); static void zmk_rgb_underglow_tick_handler(struct k_timer *timer) { if (!state.on && state.current_effect == UNDERGLOW_EFFECT_LAYER_INDICATORS) { 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(25)); } 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; } void zmk_rgb_set_ext_power(void) { #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) if (ext_power == NULL) return; int c_power = ext_power_get(ext_power); if (c_power < 0) { LOG_ERR("Unable to examine EXT_POWER: %d", c_power); c_power = 0; } int desired_state = state.on || state.status_active; #if IS_ENABLED(CONFIG_ZMK_BATTERY_REPORTING) // force power off, when battery low (<10%) if (state.on && !state.status_active) { if (zmk_battery_state_of_charge() < 10) { desired_state = false; } } #endif // CONFIG_ZMK_BATTERY_REPORTING if (desired_state && !c_power) { int rc = ext_power_enable(ext_power); if (rc != 0) { LOG_ERR("Unable to enable EXT_POWER: %d", rc); } } else if (!desired_state && c_power) { int rc = ext_power_disable(ext_power); if (rc != 0) { LOG_ERR("Unable to disable EXT_POWER: %d", rc); } } #endif // CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER } int zmk_rgb_underglow_on(void) { if (!led_strip) return -ENODEV; state.on = true; zmk_rgb_set_ext_power(); state.animation_step = 0; k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25)); 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}; } zmk_led_write_pixels(); } K_WORK_DEFINE(underglow_off_work, zmk_rgb_underglow_off_handler); int zmk_rgb_underglow_off(void) { if (!led_strip) return -ENODEV; k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_off_work); k_timer_stop(&underglow_tick); state.on = false; zmk_rgb_set_ext_power(); 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(); } static void zmk_led_write_pixels_work(struct k_work *work); static void zmk_rgb_underglow_status_update(struct k_timer *timer); K_WORK_DEFINE(underglow_write_work, zmk_led_write_pixels_work); K_TIMER_DEFINE(underglow_status_update_timer, zmk_rgb_underglow_status_update, NULL); static void zmk_rgb_underglow_status_update(struct k_timer *timer) { if (!state.status_active) return; state.status_animation_step++; if (state.status_animation_step > (10000 / 25)) { state.status_active = false; k_timer_stop(&underglow_status_update_timer); } if (!k_work_is_pending(&underglow_write_work)) k_work_submit(&underglow_write_work); } static void zmk_led_write_pixels_work(struct k_work *work) { zmk_led_write_pixels(); if (!state.status_active) { zmk_rgb_set_ext_power(); } } int zmk_rgb_underglow_status(void) { if (!state.status_active) { state.status_animation_step = 0; } else { if (state.status_animation_step > (500 / 25)) { state.status_animation_step = 500 / 25; } } state.status_active = true; zmk_led_write_pixels(); zmk_rgb_set_ext_power(); k_timer_start(&underglow_status_update_timer, K_NO_WAIT, K_MSEC(25)); return 0; } 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); } 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("zmk_split_peripheral_layer_changed: %08x", ev->layers); uint8_t layer = rgb_underglow_top_layer_with_state(ev->layers); LOG_DBG("top layer: %d", layer); zmk_rgb_underglow_set_layer(layer); return 0; } #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); ZMK_SUBSCRIPTION(rgb_underglow, zmk_split_peripheral_layer_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);