diff --git a/app/dts/bindings/zmk,underglow-indicators.yaml b/app/dts/bindings/zmk,underglow-indicators.yaml new file mode 100644 index 00000000..d5cdde80 --- /dev/null +++ b/app/dts/bindings/zmk,underglow-indicators.yaml @@ -0,0 +1,35 @@ +# Copyright (c) 2020, The ZMK Contributors +# SPDX-License-Identifier: MIT + +description: Underglow indicators + +compatible: "zmk,underglow-indicators" + +properties: + bat-lhs: + type: array + required: true + bat-rhs: + type: array + required: true + capslock: + type: int + required: true + numlock: + type: int + required: true + scrolllock: + type: int + required: true + layer-state: + type: array + required: true + ble-state: + type: array + required: true + usb-state: + type: int + required: true + output-fallback: + type: int + required: true diff --git a/app/include/dt-bindings/zmk/rgb.h b/app/include/dt-bindings/zmk/rgb.h index c1a80082..657ac2fd 100644 --- a/app/include/dt-bindings/zmk/rgb.h +++ b/app/include/dt-bindings/zmk/rgb.h @@ -19,6 +19,7 @@ #define RGB_EFR_CMD 12 #define RGB_EFS_CMD 13 #define RGB_COLOR_HSB_CMD 14 +#define RGB_STATUS_CMD 15 #define RGB_TOG RGB_TOG_CMD 0 #define RGB_ON RGB_ON_CMD 0 @@ -33,6 +34,7 @@ #define RGB_SPD RGB_SPD_CMD 0 #define RGB_EFF RGB_EFF_CMD 0 #define RGB_EFR RGB_EFR_CMD 0 +#define RGB_STATUS RGB_STATUS_CMD 0 #define RGB_COLOR_HSB_VAL(h, s, v) (((h) << 16) + ((s) << 8) + (v)) #define RGB_COLOR_HSB(h, s, v) RGB_COLOR_HSB_CMD##(RGB_COLOR_HSB_VAL(h, s, v)) #define RGB_COLOR_HSV RGB_COLOR_HSB \ No newline at end of file diff --git a/app/include/zmk/ble.h b/app/include/zmk/ble.h index 05a5bee6..81250cc1 100644 --- a/app/include/zmk/ble.h +++ b/app/include/zmk/ble.h @@ -32,6 +32,7 @@ bt_addr_le_t *zmk_ble_active_profile_addr(); bool zmk_ble_active_profile_is_open(); bool zmk_ble_active_profile_is_connected(); char *zmk_ble_active_profile_name(); +int8_t zmk_ble_profile_status(uint8_t index); int zmk_ble_unpair_all(); diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h index c8860533..4f7894e5 100644 --- a/app/include/zmk/endpoints.h +++ b/app/include/zmk/endpoints.h @@ -11,5 +11,6 @@ int zmk_endpoints_select(enum zmk_endpoint endpoint); int zmk_endpoints_toggle(); enum zmk_endpoint zmk_endpoints_selected(); +bool zmk_endpoints_preferred_is_active(); int zmk_endpoints_send_report(uint16_t usage_page); diff --git a/app/include/zmk/rgb_underglow.h b/app/include/zmk/rgb_underglow.h index 797f0b19..62bed44e 100644 --- a/app/include/zmk/rgb_underglow.h +++ b/app/include/zmk/rgb_underglow.h @@ -26,4 +26,5 @@ int zmk_rgb_underglow_change_hue(int direction); 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); \ No newline at end of file +int zmk_rgb_underglow_set_hsb(struct zmk_led_hsb color); +int zmk_rgb_underglow_status(); \ No newline at end of file diff --git a/app/src/behaviors/behavior_rgb_underglow.c b/app/src/behaviors/behavior_rgb_underglow.c index 3459cd22..5f2623c7 100644 --- a/app/src/behaviors/behavior_rgb_underglow.c +++ b/app/src/behaviors/behavior_rgb_underglow.c @@ -131,6 +131,8 @@ static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding, return zmk_rgb_underglow_set_hsb((struct zmk_led_hsb){.h = (binding->param2 >> 16) & 0xFFFF, .s = (binding->param2 >> 8) & 0xFF, .b = binding->param2 & 0xFF}); + case RGB_STATUS_CMD: + return zmk_rgb_underglow_status(); } return -ENOTSUP; diff --git a/app/src/ble.c b/app/src/ble.c index 50a5b616..57a47475 100644 --- a/app/src/ble.c +++ b/app/src/ble.c @@ -130,6 +130,23 @@ bool zmk_ble_active_profile_is_connected() { return true; } +int8_t zmk_ble_profile_status(uint8_t index) { + if (index >= ZMK_BLE_PROFILE_COUNT) + return -1; + bt_addr_le_t *addr = &profiles[index].peer; + struct bt_conn *conn; + int result; + if (!bt_addr_le_cmp(addr, BT_ADDR_LE_ANY)) { + result = 0; // disconnected + } else if ((conn = bt_conn_lookup_addr_le(BT_ID_DEFAULT, addr)) == NULL) { + result = 1; // paired + } else { + result = 2; // connected + bt_conn_unref(conn); + } + return result; +} + #define CHECKED_ADV_STOP() \ err = bt_le_adv_stop(); \ advertising_status = ZMK_ADV_NONE; \ diff --git a/app/src/endpoints.c b/app/src/endpoints.c index 593aceb5..21989301 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -63,6 +63,7 @@ int zmk_endpoints_select(enum zmk_endpoint endpoint) { } enum zmk_endpoint zmk_endpoints_selected() { return current_endpoint; } +bool zmk_endpoints_preferred_is_active() { return preferred_endpoint == current_endpoint; } int zmk_endpoints_toggle() { enum zmk_endpoint new_endpoint = diff --git a/app/src/rgb_underglow.c b/app/src/rgb_underglow.c index 65fc248b..65b19d59 100644 --- a/app/src/rgb_underglow.c +++ b/app/src/rgb_underglow.c @@ -12,6 +12,13 @@ #include #include +#include +#include +#include +#include +#include +#include + #include #include @@ -20,7 +27,6 @@ #include #include -#include #include #include #include @@ -51,11 +57,14 @@ struct rgb_underglow_state { 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; @@ -63,6 +72,8 @@ static struct rgb_underglow_state state; static const struct device *ext_power; #endif +int zmk_rgb_set_ext_power(); + 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; @@ -168,6 +179,171 @@ static void zmk_rgb_underglow_effect_swirl() { state.animation_step = state.animation_step % HUE_MAX; } +static int zmk_led_generate_status(); + +static void zmk_led_write_pixels() { + static struct led_rgb led_buffer[STRIP_NUM_PIXELS]; + int bat0 = zmk_battery_state_of_charge(); + int blend = 0; + 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; + } + + 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); + } + } + + led_strip_update_rgb(led_strip, led_buffer, STRIP_NUM_PIXELS); +} + +#define UNDERGLOW_INDICATORS DT_PATH(underglow_indicators) +#define UNDERGLOW_INDICATORS_ENABLED defined(DT_N_S_underglow_indicators_EXISTS) + +#if !UNDERGLOW_INDICATORS_ENABLED +static int zmk_led_generate_status() { 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); + +void zmk_led_battery_level(int bat_level, const uint8_t *addresses, int 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 int zmk_led_generate_status() { + for (int i = 0; i < STRIP_NUM_PIXELS; i++) { + status_pixels[i] = (struct led_rgb){r : 0, g : 0, b : 0}; + } + + 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_ROLE_CENTRAL) + zmk_led_battery_level(zmk_battery_state_of_peripheral_charge(), underglow_bat_rhs, + DT_PROP_LEN(UNDERGLOW_INDICATORS, bat_rhs)); + + // CAPSLOCK/NUMLOCK/SCROLLOCK STATUS + zmk_leds_flags_t led_flags = zmk_leds_get_current_flags(); + + 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; + } + + enum zmk_endpoint active_endpoint = zmk_endpoints_selected(); + + if (!zmk_endpoints_preferred_is_active()) + status_pixels[DT_PROP(UNDERGLOW_INDICATORS, output_fallback)] = red; + + 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 == ZMK_ENDPOINT_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; + } + } + + enum zmk_usb_conn_state usb_state = zmk_usb_get_conn_state(); + if (usb_state == ZMK_USB_CONN_HID && + active_endpoint == ZMK_ENDPOINT_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; + } +#endif + + 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 void zmk_rgb_underglow_tick(struct k_work *work) { switch (state.current_effect) { case UNDERGLOW_EFFECT_SOLID: @@ -184,7 +360,7 @@ static void zmk_rgb_underglow_tick(struct k_work *work) { break; } - led_strip_update_rgb(led_strip, pixels, STRIP_NUM_PIXELS); + zmk_led_write_pixels(); } K_WORK_DEFINE(underglow_work, zmk_rgb_underglow_tick); @@ -299,20 +475,37 @@ int zmk_rgb_underglow_get_state(bool *on_off) { return 0; } -int zmk_rgb_underglow_on() { - if (!led_strip) - return -ENODEV; - +int zmk_rgb_set_ext_power() { #if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER) - if (ext_power != NULL) { + 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 (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 +} + +int zmk_rgb_underglow_on() { + 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)); @@ -323,23 +516,15 @@ int zmk_rgb_underglow_off() { 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 - 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); + zmk_led_write_pixels(); k_timer_stop(&underglow_tick); state.on = false; + zmk_rgb_set_ext_power(); return zmk_rgb_underglow_save_state(); } @@ -370,6 +555,48 @@ int zmk_rgb_underglow_toggle() { 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() { + 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;