Compare commits

...
Sign in to create a new pull request.

25 commits

Author SHA1 Message Date
8f3c4d3929 refactor: modular RGB effect registration API
Replace hardcoded effect enum/switch-case with STRUCT_SECTION_ITERABLE
registration. Effects register via ZMK_RGB_EFFECT_DEFINE macro.

Core API (rgb_effect.h):
- zmk_rgb_effect_ctx passed to render callbacks
- zmk_rgb_request_refresh() for static effects
- ZMK_RGB_EFFECT_STATIC flag skips animation timer
- on_select/on_deselect lifecycle hooks
- zmk_rgb_hsb_to_rgb/scale helpers now public

Built-in effects (solid/breathe/spectrum/swirl) use same API.
Per-key layer RGB, behaviors, DTS bindings extracted to external
zmk-rgb-effects module.
2026-07-01 14:29:24 +08:00
d94d1597c1 fix: paint layer colors on effect entry and RGB toggle
- select_effect: clear pixels + paint current layer when entering effect #4
- select_effect: restart tick timer when leaving effect #4 for normal effects
- on: paint current layer when toggling RGB on while on effect #4

Fixes: underglow stuck on old colors, frozen display until layer switch,
cycling effects requiring off/on toggle.
2026-06-25 15:32:31 +08:00
89a8f1e664 fix: disable fade animation in layer indicator effect
The effect_layer() tick handler was fading all pixels to black every
frame, then calling transient_off() which killed RGB entirely. Layer
colors are static — set by set_layer() on layer change events. No
per-frame animation needed.
2026-06-25 14:55:25 +08:00
9ab9c4619f fix: extend EFF_START range to include layer indicators (effect 4)
darknao added UNDERGLOW_EFFECT_LAYER_INDICATORS as effect #4 but
did not update the Kconfig range from [0,3] to [0,4].
2026-06-25 10:44:13 +08:00
aa79502d5a fix: guard ext_power usage behind CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER
The battery low-power code referenced ext_power unconditionally, causing
build failure when CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER=n.
2026-06-24 20:03:22 +08:00
a904f76092 fix: guard split_peripheral_layer_changed behind CONFIG_ZMK_SPLIT
Without this guard, non-split builds (e.g. settings_reset) fail with
undefined reference to zmk_event_zmk_split_peripheral_layer_changed.
2026-06-24 20:02:28 +08:00
darknao
28a1c4697e underglow-layer: keep RGB asleep on battery update 2026-06-24 18:57:47 +08:00
darknao
01f8f363f8 underglow-layer: add fade-delay property 2026-06-24 18:57:47 +08:00
darknao
62dc6d8ec5 underglow-layer: add battery indicators 2026-06-24 18:57:47 +08:00
darknao
0f43eeb033 fix: underglow-layer not showing on both side when powered on 2026-06-24 18:57:47 +08:00
darknao
f178f86a54 underglow-layer: use behaviors to control RGBs 2026-06-24 18:57:42 +08:00
ReFil
aa151f7473 feat(split): Compile additional events on peripheral
In preparation for the re-do of #2036 events that are useful for displays/indicators on the peripheral should be compiled on both sides so they can be raised on both sides
2026-06-24 18:55:49 +08:00
darknao
8999793d2f underglow-layer: cut off ext power if all leds are off 2026-06-24 18:55:49 +08:00
darknao
f20c011fc6 underglow-layer: enable with EXPERIMENTAL_RGB_LAYER Kconfig 2026-06-24 18:55:49 +08:00
darknao
a1d67a4e92 fix: ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE not required for underglow-layer 2026-06-24 18:55:49 +08:00
Nick Winans
bd5e07f842 feat(split): Increase split interval during idle 2026-06-24 18:55:49 +08:00
darknao
886cd1025d fix: RGB_TOG also toggle underglow-layer 2026-06-24 18:55:49 +08:00
darknao
9308ba806a underglow-layer: Don't save state on idle/resume & register activity on layer change 2026-06-24 18:55:49 +08:00
darknao
da994be0ba underglow-layer: enable only if config exists in devicetree 2026-06-24 18:55:49 +08:00
darknao
0ca086a845 underglow-layer: track layer changes with event & battery life optimization
I tried using event instead of the 25ms underglow_tick to update the underglow on
layer change only. That didn't improve the battery life much....
The second change is cutting off the led strip power if the underglow is
not defined for a layer. Power is restored if a layer with rgb is
activated, and cut off as soon as the layer is disabled.
This, on the other hand, improves the battery life a lot, especially if you
don't use rgb on your base layer.
If you are using rgb on your base layer, setting
CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE is highly recommended.
2026-06-24 18:55:49 +08:00
darknao
6f9a411793 underglow-layer: use devicetree & clean up code 2026-06-24 18:55:23 +08:00
darknao
01d0749411 valdur's mod 2026-06-24 18:53:46 +08:00
Donald Gordon
e1fc10651c ext_power: add Kconfig for initial ext_power state
Adds a Kconfig setting for the default EXT_POWER status at initialization time. Previously it was always initialized to on if no saved value was present.
2026-06-24 18:53:46 +08:00
Donald Gordon
954253c6f5 RGB underglow status support
Adds Glove80's status indicator using RGB underglow support. Requires ZMK
PR#999 and PR#1243.

The underglow status is able to show layer state, battery levels,
caps/num/scroll-lock, BLE and USB state. The underglow positions selected for
each of these indicators is configured using the new devicetree node
zmk,underglow-indicators, which takes an array of integer LED positions for each
feature.
2026-06-24 18:53:46 +08:00
Chris Andreae
319f03fd01 rgb_underglow: refresh more frequently for smoother RGB underglow 2026-06-24 18:53:46 +08:00
37 changed files with 1058 additions and 107 deletions

View file

@ -11,6 +11,9 @@ project(zmk)
zephyr_linker_sources(SECTIONS include/linker/zmk-behaviors.ld)
zephyr_linker_sources(RODATA include/linker/zmk-events.ld)
if(CONFIG_ZMK_RGB_UNDERGLOW)
zephyr_linker_sources(SECTIONS include/linker/zmk-rgb-effects.ld)
endif()
if(CONFIG_ZMK_BEHAVIOR_LOCAL_IDS)
zephyr_linker_sources(DATA_SECTIONS include/linker/zmk-behavior-local-id-map.ld)
@ -71,16 +74,12 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE src/behavior_queue.c)
target_sources(app PRIVATE src/conditional_layer.c)
target_sources(app PRIVATE src/endpoints.c)
target_sources(app PRIVATE src/events/endpoint_changed.c)
target_sources(app PRIVATE src/hid_listener.c)
target_sources(app PRIVATE src/keymap.c)
target_sources(app PRIVATE src/events/layer_state_changed.c)
target_sources(app PRIVATE src/events/modifiers_state_changed.c)
target_sources(app PRIVATE src/events/keycode_state_changed.c)
target_sources_ifdef(CONFIG_ZMK_HID_INDICATORS app PRIVATE src/hid_indicators.c)
if (CONFIG_ZMK_BLE)
target_sources(app PRIVATE src/events/ble_active_profile_changed.c)
target_sources(app PRIVATE src/behaviors/behavior_bt.c)
target_sources(app PRIVATE src/ble.c)
target_sources(app PRIVATE src/hog.c)
@ -95,7 +94,15 @@ target_sources_ifdef(CONFIG_ZMK_BATTERY_REPORTING app PRIVATE src/battery.c)
target_sources_ifdef(CONFIG_ZMK_HID_INDICATORS app PRIVATE src/events/hid_indicators_changed.c)
target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/events/ble_active_profile_changed.c)
target_sources_ifdef(CONFIG_ZMK_SPLIT app PRIVATE src/events/split_peripheral_status_changed.c)
target_sources_ifdef(CONFIG_ZMK_SPLIT app PRIVATE src/events/split_peripheral_layer_changed.c)
target_sources(app PRIVATE src/events/layer_state_changed.c)
target_sources(app PRIVATE src/events/modifiers_state_changed.c)
target_sources(app PRIVATE src/events/endpoint_changed.c)
add_subdirectory_ifdef(CONFIG_ZMK_SPLIT src/split)
target_sources_ifdef(CONFIG_USB_DEVICE_STACK app PRIVATE src/usb.c)

View file

@ -322,8 +322,8 @@ config ZMK_RGB_UNDERGLOW_SPD_START
range 1 5
config ZMK_RGB_UNDERGLOW_EFF_START
int "RGB underglow start effect int value related to the effect enum list"
range 0 3
int "RGB underglow start effect index (clamped to available effects at runtime)"
range 0 15
config ZMK_RGB_UNDERGLOW_ON_START
bool "RGB underglow starts on by default"
@ -435,6 +435,16 @@ config ZMK_GPIO_KEY_WAKEUP_TRIGGER
depends on DT_HAS_ZMK_GPIO_KEY_WAKEUP_TRIGGER_ENABLED && ZMK_PM_SOFT_OFF
# Power Management
if ZMK_EXT_POWER
config ZMK_EXT_POWER_START
bool "Enable external power output by default"
default y
#ZMK_EXT_POWER
endif
#Power Management
endmenu
menu "Combo options"

View file

@ -19,6 +19,14 @@
zmk,battery = &vbatt;
};
underglow-layer {
compatible = "zmk,underglow-layer";
pixel-lookup = <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>;
};
back_led_backlight: pwmleds {
compatible = "pwm-leds";
pwm_led_0 {

View file

@ -16,16 +16,32 @@ CONFIG_USB_DEVICE_VID=0x16c0
CONFIG_USB_DEVICE_MANUFACTURER="MoErgo"
CONFIG_USB_DEVICE_SN="moergo.com:GLV80-0123456789ABCDEF"
CONFIG_BT_DEVICE_NAME="Glove80"
CONFIG_BT_DIS_PNP_PID=0x27db
CONFIG_BT_DIS_PNP_VID=0x16c0
CONFIG_BT_DIS_MANUF="MoErgo"
CONFIG_BT_DIS_MODEL="Glove80"
### Bluetooth configuration workarounds
# Use higher radio transmit power
CONFIG_BT_CTLR_TX_PWR_PLUS_8=y
# Work-around for Windows bug with battery notifications
CONFIG_BT_GATT_ENFORCE_SUBSCRIPTION=n
# Allow unauthenticated re-pairing for already paired hosts. This would permit
# an attacker that can spoof the host's peer address to "steal" the keyboard
# pairing by overwriting it, but without access to the previous keys it can't
# establish a MITM, and the sudden loss of the keyboard would be very obvious to
# the previously-connected host.
CONFIG_BT_SMP_ALLOW_UNAUTH_OVERWRITE=y
CONFIG_ZMK_BLE_PASSKEY_ENTRY=n
# Fetch peripheral battery level for status display reporting
CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING=y
# Enable MPU
CONFIG_ARM_MPU=y
@ -51,6 +67,9 @@ CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL=y
# Enable RGB underglow
CONFIG_ZMK_RGB_UNDERGLOW=y
# disable EXT_POWER until underglow gets turned on
CONFIG_ZMK_EXT_POWER_START=n
CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER=y
CONFIG_ZMK_RGB_UNDERGLOW_ON_START=n
CONFIG_ZMK_RGB_UNDERGLOW_BRT_STEP=4
@ -79,6 +98,13 @@ CONFIG_ZMK_BACKLIGHT_AUTO_OFF_USB=y
# space.
CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y
# Enable USB boot protocol support
CONFIG_ZMK_USB_BOOT=y
CONFIG_ZMK_HID_INDICATORS=y
# Send HID indicator to peripherals
CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS=y
# Turn on debugging to disable optimization. Debug messages can result in larger
# stacks, so enable stack protection and particularly a larger BLE peripheral stack.
# CONFIG_DEBUG=y

View file

@ -20,6 +20,14 @@
zmk,battery = &vbatt;
};
underglow-layer {
compatible = "zmk,underglow-layer";
pixel-lookup = <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>;
};
back_led_backlight: pwmleds {
compatible = "pwm-leds";
pwm_led_0 {

View file

@ -63,6 +63,10 @@ CONFIG_ZMK_RGB_UNDERGLOW_HUE_START=285
CONFIG_ZMK_RGB_UNDERGLOW_SAT_START=75
CONFIG_ZMK_RGB_UNDERGLOW_BRT_START=16
# Enable HID indicators on peripheral
CONFIG_ZMK_HID_INDICATORS=y
CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS=y
# The power LED is implemented as a backlight
# For now, the power LED is acting as a "USB connected" indicator
CONFIG_ZMK_BACKLIGHT=y

View file

@ -28,3 +28,6 @@
#include <behaviors/soft_off.dtsi>
#include <behaviors/studio_unlock.dtsi>
#include <behaviors/mouse_keys.dtsi>
#include <behaviors/ug_color.dtsi>
#include <behaviors/ug_indicators.dtsi>
#include <behaviors/ug_battery.dtsi>

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
/ {
behaviors {
ug_b2: ugbat20 {
compatible = "zmk,behavior-underglow-battery";
threshold = <20>;
#binding-cells = <2>;
display-name = "Underglow Battery level 20%";
};
ug_b4: ugbat40 {
compatible = "zmk,behavior-underglow-battery";
threshold = <40>;
#binding-cells = <2>;
display-name = "Underglow Battery level 40%";
};
ug_b6: ugbat60 {
compatible = "zmk,behavior-underglow-battery";
threshold = <60>;
#binding-cells = <2>;
display-name = "Underglow Battery level 60%";
};
ug_b8: ugbat80 {
compatible = "zmk,behavior-underglow-battery";
threshold = <80>;
#binding-cells = <2>;
display-name = "Underglow Battery level 80%";
};
};
};

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
/ {
behaviors {
ug: ugcolor {
compatible = "zmk,behavior-underglow-color";
#binding-cells = <1>;
display-name = "Underglow Color";
};
};
};

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <dt-bindings/zmk/hid_indicators.h>
/ {
behaviors {
ug_nl: ugnumlk {
compatible = "zmk,behavior-underglow-indicators";
indicator = <NUM_LOCK>;
#binding-cells = <2>;
display-name = "Underglow NumLock indicator";
};
ug_cl: ugcapslk {
compatible = "zmk,behavior-underglow-indicators";
indicator = <CAPS_LOCK>;
#binding-cells = <2>;
display-name = "Underglow CapsLock indicator";
};
ug_sl: ugscrllk {
compatible = "zmk,behavior-underglow-indicators";
indicator = <SCROLL_LOCK>;
#binding-cells = <2>;
display-name = "Underglow ScrollLock indicator";
};
};
};

View file

@ -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

View file

@ -0,0 +1,9 @@
/*
* Copyright (c) 2020 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define NUM_LOCK 0
#define CAPS_LOCK 1
#define SCROLL_LOCK 2

View file

@ -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

View file

@ -0,0 +1,18 @@
/*
* Copyright (c) 2021 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define GREEN 0x00ff00
#define RED 0xff0000
#define BLUE 0x0000ff
#define TEAL 0x008080
#define ORANGE 0xffa500
#define YELLOW 0xffff00
#define GOLD 0xffd700
#define PURPLE 0x800080
#define PINK 0xffc0cb
#define WHITE 0xffffff
#define ___ 0x000000
#define BLACK 0x000000

View file

@ -0,0 +1,9 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/linker/linker-defs.h>
ITERABLE_SECTION_ROM(zmk_rgb_effect, 4)

View file

@ -39,6 +39,7 @@ bool zmk_ble_profile_is_open(uint8_t index);
bool zmk_ble_active_profile_is_open(void);
bool zmk_ble_active_profile_is_connected(void);
char *zmk_ble_active_profile_name(void);
int8_t zmk_ble_profile_status(uint8_t index);
int zmk_ble_unpair_all(void);

View file

@ -68,6 +68,8 @@ int zmk_endpoints_toggle_transport(void);
*/
struct zmk_endpoint_instance zmk_endpoints_selected(void);
bool zmk_endpoints_preferred_transport_is_active();
int zmk_endpoints_send_report(uint16_t usage_page);
#if IS_ENABLED(CONFIG_ZMK_POINTING)

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2022 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/kernel.h>
#include <zmk/event_manager.h>
struct zmk_split_peripheral_layer_changed {
uint32_t layers;
};
ZMK_EVENT_DECLARE(zmk_split_peripheral_layer_changed);

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2024 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#pragma once
#include <zephyr/drivers/led_strip.h>
#include <zmk/rgb_underglow.h>
struct zmk_rgb_effect_ctx {
struct led_rgb *pixels;
uint16_t num_pixels;
struct zmk_led_hsb base_color;
uint16_t *animation_step;
uint8_t animation_speed;
};
typedef void (*zmk_rgb_effect_render_fn)(struct zmk_rgb_effect_ctx *ctx);
typedef void (*zmk_rgb_effect_event_fn)(void);
#define ZMK_RGB_EFFECT_STATIC BIT(0)
struct zmk_rgb_effect {
const char *name;
zmk_rgb_effect_render_fn render;
zmk_rgb_effect_event_fn on_select;
zmk_rgb_effect_event_fn on_deselect;
uint8_t flags;
};
#define ZMK_RGB_EFFECT_DEFINE(_sym, _name_str, _render, _flags, _on_select, _on_deselect) \
STRUCT_SECTION_ITERABLE(zmk_rgb_effect, _sym) = { \
.name = _name_str, \
.render = _render, \
.flags = _flags, \
.on_select = _on_select, \
.on_deselect = _on_deselect, \
}
void zmk_rgb_request_refresh(void);
int zmk_rgb_effect_get_count(void);
struct led_rgb zmk_rgb_hsb_to_rgb(struct zmk_led_hsb hsb);
struct zmk_led_hsb zmk_rgb_hsb_scale_min_max(struct zmk_led_hsb hsb);
struct zmk_led_hsb zmk_rgb_hsb_scale_zero_max(struct zmk_led_hsb hsb);

View file

@ -16,6 +16,8 @@ int zmk_rgb_underglow_toggle(void);
int zmk_rgb_underglow_get_state(bool *state);
int zmk_rgb_underglow_on(void);
int zmk_rgb_underglow_off(void);
int zmk_rgb_underglow_transient_on(void);
int zmk_rgb_underglow_transient_off(void);
int zmk_rgb_underglow_cycle_effect(int direction);
int zmk_rgb_underglow_calc_effect(int direction);
int zmk_rgb_underglow_select_effect(int effect);
@ -27,3 +29,4 @@ 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_status(void);

View file

@ -0,0 +1,5 @@
#pragma once
void set_peripheral_layers_state(uint32_t new_layers);
bool peripheral_layer_active(uint8_t layer);
uint8_t peripheral_highest_layer_active(void);

View file

@ -20,3 +20,4 @@
#define ZMK_SPLIT_BT_UPDATE_HID_INDICATORS_UUID ZMK_BT_SPLIT_UUID(0x00000004)
#define ZMK_SPLIT_BT_SELECT_PHYS_LAYOUT_UUID ZMK_BT_SPLIT_UUID(0x00000005)
#define ZMK_SPLIT_BT_INPUT_EVENT_UUID ZMK_BT_SPLIT_UUID(0x00000006)
#define ZMK_SPLIT_BT_UPDATE_LAYERS_UUID ZMK_BT_SPLIT_UUID(0x00000007)

View file

@ -46,3 +46,5 @@ int zmk_split_central_update_hid_indicator(zmk_hid_indicators_t indicators);
int zmk_split_central_get_peripheral_battery_level(uint8_t source, uint8_t *level);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
int zmk_split_central_update_layers(uint32_t layers);

View file

@ -16,6 +16,9 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/event_manager.h>
#include <zmk/events/activity_state_changed.h>
#include <zmk/events/position_state_changed.h>
#if IS_ENABLED(CONFIG_ZMK_SPLIT)
#include <zmk/events/split_peripheral_layer_changed.h>
#endif
#include <zmk/events/sensor_event.h>
#include <zmk/pm.h>
@ -109,6 +112,9 @@ static int activity_init(void) {
ZMK_LISTENER(activity, activity_event_listener);
ZMK_SUBSCRIPTION(activity, zmk_position_state_changed);
ZMK_SUBSCRIPTION(activity, zmk_sensor_event);
#if IS_ENABLED(CONFIG_ZMK_SPLIT)
ZMK_SUBSCRIPTION(activity, zmk_split_peripheral_layer_changed);
#endif
#if IS_ENABLED(CONFIG_ZMK_POINTING)

View file

@ -242,6 +242,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;

View file

@ -143,6 +143,23 @@ bool zmk_ble_profile_is_connected(uint8_t index) {
return info.state == BT_CONN_STATE_CONNECTED;
}
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; \

View file

@ -319,6 +319,10 @@ static struct zmk_endpoint_instance get_selected_instance(void) {
return instance;
}
bool zmk_endpoints_preferred_transport_is_active(void) {
return preferred_transport == get_selected_transport();
}
static int zmk_endpoints_init(void) {
#if IS_ENABLED(CONFIG_SETTINGS)
k_work_init_delayable(&endpoints_save_work, endpoints_save_preferred_work);

View file

@ -0,0 +1,10 @@
/*
* Copyright (c) 2022 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/kernel.h>
#include <zmk/events/split_peripheral_layer_changed.h>
ZMK_EVENT_IMPL(zmk_split_peripheral_layer_changed);

View file

@ -136,7 +136,11 @@ static int ext_power_settings_commit() {
data->status = true;
k_work_schedule(&ext_power_save_work, K_NO_WAIT);
if (IS_ENABLED(CONFIG_ZMK_EXT_POWER_START)) {
ext_power_enable(dev);
} else {
ext_power_disable(dev);
}
}
return 0;
@ -162,8 +166,12 @@ static int ext_power_generic_init(const struct device *dev) {
k_work_init_delayable(&ext_power_save_work, ext_power_save_state_work);
#endif
// Enable by default. We may get disabled again once settings load.
// Set to default state by default. This may change again once settings load.
if (IS_ENABLED(CONFIG_ZMK_EXT_POWER_START)) {
ext_power_enable(dev);
} else {
ext_power_disable(dev);
}
if (config->init_delay_ms) {
k_msleep(config->init_delay_ms);

View file

@ -17,6 +17,9 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/matrix.h>
#include <zmk/sensors.h>
#include <zmk/virtual_key_position.h>
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE)
#include <zmk/split/central.h>
#endif
#include <zmk/event_manager.h>
#include <zmk/events/position_state_changed.h>
@ -155,6 +158,9 @@ static inline int set_layer_state(zmk_keymap_layer_id_t layer_id, bool state) {
if (ret < 0) {
LOG_WRN("Failed to raise layer state changed (%d)", ret);
}
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE)
zmk_split_central_update_layers(_zmk_keymap_layer_state);
#endif
}
return ret;

View file

@ -12,20 +12,36 @@
#include <math.h>
#include <stdlib.h>
#include <zmk/battery.h>
#include <zmk/ble.h>
#include <zmk/endpoints.h>
#include <zmk/keymap.h>
#include <zmk/matrix.h>
#include <zmk/behavior.h>
#include <zmk/hid_indicators.h>
#include <zmk/usb.h>
#include <zephyr/logging/log.h>
#include <zephyr/drivers/led_strip.h>
#include <drivers/ext_power.h>
#include <drivers/behavior.h>
#include <zmk/rgb_underglow.h>
#include <zmk/rgb_effect.h>
#include <zmk/activity.h>
#include <zmk/usb.h>
#include <zmk/event_manager.h>
#include <zmk/events/activity_state_changed.h>
#include <zmk/events/usb_conn_state_changed.h>
#include <zmk/workqueue.h>
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
#include <zmk/split/central.h>
#endif
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#if !DT_HAS_CHOSEN(zmk_underglow)
@ -44,25 +60,20 @@ 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");
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;
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;
@ -70,18 +81,22 @@ static struct rgb_underglow_state state;
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) {
void zmk_rgb_set_ext_power(void);
/* --- Public utility functions for effects --- */
struct zmk_led_hsb zmk_rgb_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) {
struct zmk_led_hsb zmk_rgb_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) {
struct led_rgb zmk_rgb_hsb_to_rgb(struct zmk_led_hsb hsb) {
float r = 0, g = 0, b = 0;
uint8_t i = hsb.h / 60;
@ -130,71 +145,318 @@ static struct led_rgb hsb_to_rgb(struct zmk_led_hsb hsb) {
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));
/* --- Built-in effects --- */
static void effect_solid_render(struct zmk_rgb_effect_ctx *ctx) {
for (int i = 0; i < ctx->num_pixels; i++) {
ctx->pixels[i] = zmk_rgb_hsb_to_rgb(zmk_rgb_hsb_scale_min_max(ctx->base_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;
static void effect_breathe_render(struct zmk_rgb_effect_ctx *ctx) {
for (int i = 0; i < ctx->num_pixels; i++) {
struct zmk_led_hsb hsb = ctx->base_color;
hsb.b = abs(*ctx->animation_step - 1200) / 12;
pixels[i] = hsb_to_rgb(hsb_scale_zero_max(hsb));
ctx->pixels[i] = zmk_rgb_hsb_to_rgb(zmk_rgb_hsb_scale_zero_max(hsb));
}
state.animation_step += state.animation_speed * 10;
*ctx->animation_step += ctx->animation_speed * 10;
if (state.animation_step > 2400) {
state.animation_step = 0;
if (*ctx->animation_step > 2400) {
*ctx->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;
static void effect_spectrum_render(struct zmk_rgb_effect_ctx *ctx) {
for (int i = 0; i < ctx->num_pixels; i++) {
struct zmk_led_hsb hsb = ctx->base_color;
hsb.h = *ctx->animation_step;
pixels[i] = hsb_to_rgb(hsb_scale_min_max(hsb));
ctx->pixels[i] = zmk_rgb_hsb_to_rgb(zmk_rgb_hsb_scale_min_max(hsb));
}
state.animation_step += state.animation_speed;
state.animation_step = state.animation_step % HUE_MAX;
*ctx->animation_step += ctx->animation_speed;
*ctx->animation_step = *ctx->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;
static void effect_swirl_render(struct zmk_rgb_effect_ctx *ctx) {
for (int i = 0; i < ctx->num_pixels; i++) {
struct zmk_led_hsb hsb = ctx->base_color;
hsb.h = (HUE_MAX / ctx->num_pixels * i + *ctx->animation_step) % HUE_MAX;
pixels[i] = hsb_to_rgb(hsb_scale_min_max(hsb));
ctx->pixels[i] = zmk_rgb_hsb_to_rgb(zmk_rgb_hsb_scale_min_max(hsb));
}
state.animation_step += state.animation_speed * 2;
state.animation_step = state.animation_step % HUE_MAX;
*ctx->animation_step += ctx->animation_speed * 2;
*ctx->animation_step = *ctx->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;
ZMK_RGB_EFFECT_DEFINE(effect_solid, "Solid", effect_solid_render, 0, NULL, NULL);
ZMK_RGB_EFFECT_DEFINE(effect_breathe, "Breathe", effect_breathe_render, 0, NULL, NULL);
ZMK_RGB_EFFECT_DEFINE(effect_spectrum, "Spectrum", effect_spectrum_render, 0, NULL, NULL);
ZMK_RGB_EFFECT_DEFINE(effect_swirl, "Swirl", effect_swirl_render, 0, NULL, NULL);
/* --- Effect registry helpers --- */
int zmk_rgb_effect_get_count(void) {
int count;
STRUCT_SECTION_COUNT(zmk_rgb_effect, &count);
return count;
}
static struct zmk_rgb_effect *zmk_rgb_effect_get(int index) {
int count = zmk_rgb_effect_get_count();
if (index < 0 || index >= count) {
return NULL;
}
struct zmk_rgb_effect *effect;
STRUCT_SECTION_GET(zmk_rgb_effect, index, &effect);
return effect;
}
/* --- Status indicator overlay (separate from effects) --- */
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();
}
int err = led_strip_update_rgb(led_strip, pixels, STRIP_NUM_PIXELS);
if (blend == 0 && bat0 >= 20) {
led_strip_update_rgb(led_strip, pixels, STRIP_NUM_PIXELS);
return;
}
if (bat0 < 10) {
memset(pixels, 0, sizeof(struct led_rgb) * STRIP_NUM_PIXELS);
#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_EXT_POWER)
if (state.on) {
int c_power = ext_power_get(ext_power);
if (c_power && !state.status_active) {
state.on = false;
reset_ext_power = true;
}
}
#endif
}
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);
}
}
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;
}
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};
}
#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
#endif
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;
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) {
status_pixels[ble_pixel] = white;
} else if (status == 2) {
status_pixels[ble_pixel] = dull_green;
} else if (status == 1) {
status_pixels[ble_pixel] = red;
} else if (status == 0) {
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) {
status_pixels[DT_PROP(UNDERGLOW_INDICATORS, usb_state)] = white;
} else if (usb_state == ZMK_USB_CONN_HID) {
status_pixels[DT_PROP(UNDERGLOW_INDICATORS, usb_state)] = dull_green;
} else if (usb_state == ZMK_USB_CONN_POWERED) {
status_pixels[DT_PROP(UNDERGLOW_INDICATORS, usb_state)] = red;
} else if (usb_state == ZMK_USB_CONN_NONE) {
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
/* --- Tick / animation loop --- */
static void zmk_rgb_underglow_tick(struct k_work *work) {
struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect);
if (effect && effect->render) {
struct zmk_rgb_effect_ctx ctx = {
.pixels = pixels,
.num_pixels = STRIP_NUM_PIXELS,
.base_color = state.color,
.animation_step = &state.animation_step,
.animation_speed = state.animation_speed,
};
effect->render(&ctx);
}
zmk_led_write_pixels();
}
K_WORK_DEFINE(underglow_tick_work, zmk_rgb_underglow_tick);
@ -209,11 +471,16 @@ static void zmk_rgb_underglow_tick_handler(struct k_timer *timer) {
K_TIMER_DEFINE(underglow_tick, zmk_rgb_underglow_tick_handler, NULL);
void zmk_rgb_request_refresh(void) {
k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_tick_work);
}
/* --- Settings persistence --- */
#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;
@ -221,10 +488,13 @@ static int rgb_settings_set(const char *name, size_t len, settings_read_cb read_
rc = read_cb(cb_arg, &state, sizeof(state));
if (rc >= 0) {
int effect_count = zmk_rgb_effect_get_count();
if (state.current_effect >= effect_count) {
state.current_effect = 0;
}
if (state.on) {
k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50));
}
return 0;
}
@ -245,7 +515,6 @@ static struct k_work_delayable underglow_save_work;
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);
@ -273,8 +542,18 @@ static int zmk_rgb_underglow_init(void) {
state.on = zmk_usb_is_powered();
#endif
int effect_count = zmk_rgb_effect_get_count();
if (state.current_effect >= effect_count && effect_count > 0) {
state.current_effect = 0;
}
if (state.on) {
k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(50));
struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect);
if (effect && (effect->flags & ZMK_RGB_EFFECT_STATIC)) {
zmk_rgb_request_refresh();
} else {
k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25));
}
}
return 0;
@ -297,72 +576,136 @@ int zmk_rgb_underglow_get_state(bool *on_off) {
return 0;
}
int zmk_rgb_underglow_on(void) {
if (!led_strip)
return -ENODEV;
void zmk_rgb_set_ext_power(void) {
#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);
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)
if (state.on && !state.status_active) {
if (zmk_battery_state_of_charge() < 10) {
desired_state = false;
}
}
#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};
if (desired_state && !c_power) {
int rc = ext_power_enable(ext_power);
if (rc != 0) {
LOG_ERR("Unable to enable EXT_POWER: %d", rc);
}
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) {
} 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(void) {
zmk_rgb_underglow_transient_on();
return zmk_rgb_underglow_save_state();
}
int zmk_rgb_underglow_transient_on(void) {
if (!led_strip)
return -ENODEV;
state.on = true;
zmk_rgb_set_ext_power();
state.animation_step = 0;
struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect);
if (effect && effect->on_select) {
effect->on_select();
}
if (effect && (effect->flags & ZMK_RGB_EFFECT_STATIC)) {
zmk_rgb_request_refresh();
} else {
k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25));
}
return 0;
}
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) {
zmk_rgb_underglow_transient_off();
return zmk_rgb_underglow_save_state();
}
int zmk_rgb_underglow_transient_off(void) {
if (!led_strip)
return -ENODEV;
struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect);
if (effect && effect->on_deselect) {
effect->on_deselect();
}
k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_off_work);
k_timer_stop(&underglow_tick);
state.on = false;
zmk_rgb_set_ext_power();
return zmk_rgb_underglow_save_state();
return 0;
}
int zmk_rgb_underglow_calc_effect(int direction) {
return (state.current_effect + UNDERGLOW_EFFECT_NUMBER + direction) % UNDERGLOW_EFFECT_NUMBER;
int count = zmk_rgb_effect_get_count();
if (count == 0)
return 0;
return (state.current_effect + count + direction) % count;
}
int zmk_rgb_underglow_select_effect(int effect) {
if (!led_strip)
return -ENODEV;
if (effect < 0 || effect >= UNDERGLOW_EFFECT_NUMBER) {
int count = zmk_rgb_effect_get_count();
if (effect < 0 || effect >= count) {
return -EINVAL;
}
struct zmk_rgb_effect *old_eff = zmk_rgb_effect_get(state.current_effect);
if (old_eff && old_eff->on_deselect) {
old_eff->on_deselect();
}
state.current_effect = effect;
state.animation_step = 0;
struct zmk_rgb_effect *new_eff = zmk_rgb_effect_get(state.current_effect);
if (new_eff && new_eff->on_select) {
new_eff->on_select();
}
if (state.on) {
if (new_eff && (new_eff->flags & ZMK_RGB_EFFECT_STATIC)) {
k_timer_stop(&underglow_tick);
zmk_rgb_request_refresh();
} else {
k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25));
}
}
return zmk_rgb_underglow_save_state();
}
@ -374,6 +717,52 @@ int zmk_rgb_underglow_toggle(void) {
return state.on ? zmk_rgb_underglow_off() : zmk_rgb_underglow_on();
}
/* --- Status overlay timer --- */
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;
}
/* --- HSB control functions --- */
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;
@ -460,6 +849,8 @@ int zmk_rgb_underglow_change_spd(int direction) {
return zmk_rgb_underglow_save_state();
}
/* --- Auto off / idle listeners --- */
#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE) || \
IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_USB)
struct rgb_underglow_sleep_state {
@ -473,7 +864,6 @@ static int rgb_underglow_auto_state(bool target_wake_state) {
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;
}
@ -481,13 +871,13 @@ static int rgb_underglow_auto_state(bool target_wake_state) {
if (sleep_state.is_awake) {
if (sleep_state.rgb_state_before_sleeping) {
return zmk_rgb_underglow_on();
return zmk_rgb_underglow_transient_on();
} else {
return zmk_rgb_underglow_off();
return zmk_rgb_underglow_transient_off();
}
} else {
sleep_state.rgb_state_before_sleeping = state.on;
return zmk_rgb_underglow_off();
return zmk_rgb_underglow_transient_off();
}
}
@ -509,8 +899,7 @@ static int rgb_underglow_event_listener(const zmk_event_t *eh) {
}
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)
#endif
#if IS_ENABLED(CONFIG_ZMK_RGB_UNDERGLOW_AUTO_OFF_IDLE)
ZMK_SUBSCRIPTION(rgb_underglow, zmk_activity_state_changed);

View file

@ -4,9 +4,12 @@
if (NOT CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE service.c)
target_sources(app PRIVATE peripheral.c)
target_sources(app PRIVATE peripheral_layers.c)
endif()
if (CONFIG_ZMK_SPLIT_ROLE_CENTRAL)
target_sources(app PRIVATE central.c)
target_sources_ifdef(CONFIG_ZMK_SPLIT_BLE_PREF_IDLE app PRIVATE central_listener.c)
endif()
if (CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_PROXY)

View file

@ -74,6 +74,26 @@ config ZMK_SPLIT_BLE_PREF_TIMEOUT
int "Supervision timeout to use for split central/peripheral connection"
default 400
config ZMK_SPLIT_BLE_PREF_IDLE
bool "Set slower split peripheral BLE params on idle to save power"
default y
if ZMK_SPLIT_BLE_PREF_IDLE
config ZMK_SPLIT_BLE_PREF_IDLE_INT
int "Peripheral idle connection interval in 1.25ms units"
default 18
config ZMK_SPLIT_BLE_PREF_IDLE_LATENCY
int "Peripheral idle latency in Connection Intervals"
default 10
config ZMK_SPLIT_BLE_PREF_IDLE_TIMEOUT
int "Peripheral idle supervision timeout in 10ms units"
default 400
endif # ZMK_SPLIT_BLE_PREF_IDLE
endif # ZMK_SPLIT_ROLE_CENTRAL
if !ZMK_SPLIT_ROLE_CENTRAL

View file

@ -33,6 +33,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zmk/pointing/input_split.h>
#include <zmk/hid_indicators_types.h>
#include <zmk/physical_layouts.h>
#include <zmk/events/split_peripheral_layer_changed.h>
static int start_scanning(void);
@ -60,6 +61,8 @@ struct peripheral_slot {
uint16_t update_hid_indicators;
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
uint16_t selected_physical_layout_handle;
uint16_t update_layers_handle;
uint8_t position_state[POSITION_STATE_DATA_LEN];
uint8_t changed_positions[POSITION_STATE_DATA_LEN];
};
@ -219,6 +222,7 @@ int release_peripheral_slot(int index) {
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
slot->update_hid_indicators = 0;
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
slot->update_layers_handle = 0;
return 0;
}
@ -620,6 +624,10 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,
LOG_DBG("Found update HID indicators handle");
slot->update_hid_indicators = bt_gatt_attr_value_handle(attr);
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
} else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid,
BT_UUID_DECLARE_128(ZMK_SPLIT_BT_UPDATE_LAYERS_UUID))) {
LOG_DBG("Found update Layers handle");
slot->update_layers_handle = bt_gatt_attr_value_handle(attr);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE_CENTRAL_BATTERY_LEVEL_FETCHING)
} else if (!bt_uuid_cmp(((struct bt_gatt_chrc *)attr->user_data)->uuid,
BT_UUID_BAS_BATTERY_LEVEL)) {
@ -707,6 +715,8 @@ static uint8_t split_central_chrc_discovery_func(struct bt_conn *conn,
}
#endif // IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
subscribed = subscribed && slot->update_layers_handle;
return subscribed ? BT_GATT_ITER_STOP : BT_GATT_ITER_CONTINUE;
}
@ -1145,6 +1155,42 @@ static struct settings_handler ble_central_settings_handler = {
#endif // IS_ENABLED(CONFIG_SETTINGS)
static uint32_t layers_for_peripheral = 0;
static void split_central_update_layers_callback(struct k_work *work) {
uint32_t layers = layers_for_peripheral;
for (int i = 0; i < ZMK_SPLIT_BLE_PERIPHERAL_COUNT; i++) {
if (peripherals[i].state != PERIPHERAL_SLOT_STATE_CONNECTED) {
continue;
}
if (peripherals[i].update_layers_handle == 0) {
continue;
}
int err =
bt_gatt_write_without_response(peripherals[i].conn, peripherals[i].update_layers_handle,
&layers, sizeof(layers), true);
if (err) {
LOG_ERR("Failed to send layers to peripheral (err %d)", err);
} else {
LOG_DBG("Sent Layers over to peripheral");
raise_zmk_split_peripheral_layer_changed(
(struct zmk_split_peripheral_layer_changed){.layers = layers});
}
}
}
static K_WORK_DEFINE(split_central_update_layers, split_central_update_layers_callback);
int zmk_split_central_update_layers(uint32_t new_layers) {
layers_for_peripheral = new_layers;
return k_work_submit_to_queue(&split_central_split_run_q, &split_central_update_layers);
}
// valdur layers done
static int zmk_split_bt_central_init(void) {
k_work_queue_start(&split_central_split_run_q, split_central_split_run_q_stack,
K_THREAD_STACK_SIZEOF(split_central_split_run_q_stack),

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2022 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zmk/event_manager.h>
#include <zmk/events/activity_state_changed.h>
static void set_sleep_params(struct bt_conn *conn, void *data) {
struct bt_conn_info info;
bt_conn_get_info(conn, &info);
if (info.role == BT_CONN_ROLE_CENTRAL) {
int err =
bt_conn_le_param_update(conn, BT_LE_CONN_PARAM(CONFIG_ZMK_SPLIT_BLE_PREF_IDLE_INT,
CONFIG_ZMK_SPLIT_BLE_PREF_IDLE_INT,
CONFIG_ZMK_SPLIT_BLE_PREF_IDLE_LATENCY,
CONFIG_ZMK_SPLIT_BLE_PREF_IDLE_TIMEOUT));
if (err) {
LOG_DBG("Failed to sleep split connection: %d", err);
}
}
}
static void set_wake_params(struct bt_conn *conn, void *data) {
struct bt_conn_info info;
bt_conn_get_info(conn, &info);
if (info.role == BT_CONN_ROLE_CENTRAL) {
int err = bt_conn_le_param_update(
conn,
BT_LE_CONN_PARAM(CONFIG_ZMK_SPLIT_BLE_PREF_INT, CONFIG_ZMK_SPLIT_BLE_PREF_INT,
CONFIG_ZMK_SPLIT_BLE_PREF_LATENCY, CONFIG_ZMK_SPLIT_BLE_PREF_TIMEOUT));
if (err) {
LOG_DBG("Failed to wake up split connection: %d", err);
}
}
}
static void sleep_all() {
LOG_DBG("Setting idle connection parameters on peripherals");
bt_conn_foreach(BT_CONN_TYPE_LE, set_sleep_params, NULL);
}
static void wake_all() {
LOG_DBG("Waking up from idle connection parameters on peripherals");
bt_conn_foreach(BT_CONN_TYPE_LE, set_wake_params, NULL);
}
int central_event_handler(const zmk_event_t *eh) {
struct zmk_activity_state_changed *ev = as_zmk_activity_state_changed(eh);
if (ev == NULL) {
return -ENOTSUP;
}
switch (ev->state) {
case ZMK_ACTIVITY_ACTIVE:
wake_all();
break;
case ZMK_ACTIVITY_IDLE:
sleep_all();
break;
case ZMK_ACTIVITY_SLEEP:
break;
default:
LOG_WRN("Unhandled activity state: %d", ev->state);
return -EINVAL;
}
return 0;
}
ZMK_LISTENER(central, central_event_handler);
ZMK_SUBSCRIPTION(central, zmk_activity_state_changed);

View file

@ -0,0 +1,25 @@
#include <zephyr/types.h>
#include <zephyr/sys/util.h>
#include <zmk/split/bluetooth/peripheral_layers.h>
#include <zmk/keymap.h>
static uint32_t peripheral_layers = 0;
void set_peripheral_layers_state(uint32_t new_layers) { peripheral_layers = new_layers; }
bool peripheral_layer_active(uint8_t layer) {
return (peripheral_layers & (BIT(layer))) == (BIT(layer));
};
uint8_t peripheral_highest_layer_active(void) {
if (peripheral_layers > 0) {
for (uint8_t layer = ZMK_KEYMAP_LAYERS_LEN - 1; layer > 0; layer--) {
if ((peripheral_layers & (BIT(layer))) == (BIT(layer)) || layer == 0) {
return layer;
}
}
}
return 0;
}

View file

@ -31,9 +31,11 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
#if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#include <zmk/events/hid_indicators_changed.h>
#endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS)
#include <zmk/split/bluetooth/peripheral_layers.h>
#include <zmk/events/sensor_event.h>
#include <zmk/sensors.h>
#include <zmk/events/split_peripheral_layer_changed.h>
#if ZMK_KEYMAP_HAS_SENSORS
static struct sensor_event last_sensor_event;
@ -139,6 +141,31 @@ static ssize_t split_svc_get_selected_phys_layout(struct bt_conn *conn,
return bt_gatt_attr_read(conn, attrs, buf, len, offset, &selected, sizeof(selected));
}
static uint32_t layers = 0;
static void split_svc_update_layers_callback(struct k_work *work) {
LOG_DBG("Setting peripheral layers: %x", layers);
// set_peripheral_layers_state(layers);
raise_zmk_split_peripheral_layer_changed(
(struct zmk_split_peripheral_layer_changed){.layers = layers});
}
static K_WORK_DEFINE(split_svc_update_layers_work, split_svc_update_layers_callback);
static ssize_t split_svc_update_layers(struct bt_conn *conn, const struct bt_gatt_attr *attr,
const void *buf, uint16_t len, uint16_t offset,
uint8_t flags) {
if (offset + len > sizeof(uint32_t)) {
return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
}
memcpy((uint8_t *)&layers + offset, buf, len);
k_work_submit(&split_svc_update_layers_work);
return len;
}
#if IS_ENABLED(CONFIG_ZMK_INPUT_SPLIT)
static void split_input_events_ccc(const struct bt_gatt_attr *attr, uint16_t value) {
@ -204,8 +231,11 @@ BT_GATT_SERVICE_DEFINE(
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_SELECT_PHYS_LAYOUT_UUID),
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_READ,
BT_GATT_PERM_WRITE_ENCRYPT | BT_GATT_PERM_READ_ENCRYPT,
split_svc_get_selected_phys_layout, split_svc_select_phys_layout,
NULL), );
split_svc_get_selected_phys_layout, split_svc_select_phys_layout, NULL),
BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_128(ZMK_SPLIT_BT_UPDATE_LAYERS_UUID),
BT_GATT_CHRC_WRITE_WITHOUT_RESP, BT_GATT_PERM_WRITE_ENCRYPT, NULL,
split_svc_update_layers, NULL), );
K_THREAD_STACK_DEFINE(service_q_stack, CONFIG_ZMK_SPLIT_BLE_PERIPHERAL_STACK_SIZE);