diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 60c502fc..478c325b 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -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) diff --git a/app/Kconfig b/app/Kconfig index 6b4e3509..fc49414f 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -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" diff --git a/app/boards/arm/glove80/glove80_lh.dts b/app/boards/arm/glove80/glove80_lh.dts index 2ed56688..7bd57742 100644 --- a/app/boards/arm/glove80/glove80_lh.dts +++ b/app/boards/arm/glove80/glove80_lh.dts @@ -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 { diff --git a/app/boards/arm/glove80/glove80_lh_defconfig b/app/boards/arm/glove80/glove80_lh_defconfig index a93f27cd..d36173bb 100644 --- a/app/boards/arm/glove80/glove80_lh_defconfig +++ b/app/boards/arm/glove80/glove80_lh_defconfig @@ -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 diff --git a/app/boards/arm/glove80/glove80_rh.dts b/app/boards/arm/glove80/glove80_rh.dts index 7b54f62c..0cb16518 100644 --- a/app/boards/arm/glove80/glove80_rh.dts +++ b/app/boards/arm/glove80/glove80_rh.dts @@ -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 { diff --git a/app/boards/arm/glove80/glove80_rh_defconfig b/app/boards/arm/glove80/glove80_rh_defconfig index ef29d682..b6795f5d 100644 --- a/app/boards/arm/glove80/glove80_rh_defconfig +++ b/app/boards/arm/glove80/glove80_rh_defconfig @@ -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 diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi index 653b085d..1f87ef1a 100644 --- a/app/dts/behaviors.dtsi +++ b/app/dts/behaviors.dtsi @@ -28,3 +28,6 @@ #include #include #include +#include +#include +#include diff --git a/app/dts/behaviors/ug_battery.dtsi b/app/dts/behaviors/ug_battery.dtsi new file mode 100644 index 00000000..3e5f75cc --- /dev/null +++ b/app/dts/behaviors/ug_battery.dtsi @@ -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%"; + }; + }; +}; diff --git a/app/dts/behaviors/ug_color.dtsi b/app/dts/behaviors/ug_color.dtsi new file mode 100644 index 00000000..15d6c863 --- /dev/null +++ b/app/dts/behaviors/ug_color.dtsi @@ -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"; + }; + }; +}; diff --git a/app/dts/behaviors/ug_indicators.dtsi b/app/dts/behaviors/ug_indicators.dtsi new file mode 100644 index 00000000..b62c79b7 --- /dev/null +++ b/app/dts/behaviors/ug_indicators.dtsi @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +/ { + behaviors { + ug_nl: ugnumlk { + compatible = "zmk,behavior-underglow-indicators"; + indicator = ; + #binding-cells = <2>; + display-name = "Underglow NumLock indicator"; + }; + + ug_cl: ugcapslk { + compatible = "zmk,behavior-underglow-indicators"; + indicator = ; + #binding-cells = <2>; + display-name = "Underglow CapsLock indicator"; + }; + + ug_sl: ugscrllk { + compatible = "zmk,behavior-underglow-indicators"; + indicator = ; + #binding-cells = <2>; + display-name = "Underglow ScrollLock indicator"; + }; + + }; +}; 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/hid_indicators.h b/app/include/dt-bindings/zmk/hid_indicators.h new file mode 100644 index 00000000..860c81db --- /dev/null +++ b/app/include/dt-bindings/zmk/hid_indicators.h @@ -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 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/dt-bindings/zmk/rgb_colors.h b/app/include/dt-bindings/zmk/rgb_colors.h new file mode 100644 index 00000000..73e9cc1f --- /dev/null +++ b/app/include/dt-bindings/zmk/rgb_colors.h @@ -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 \ No newline at end of file diff --git a/app/include/linker/zmk-rgb-effects.ld b/app/include/linker/zmk-rgb-effects.ld new file mode 100644 index 00000000..28d519be --- /dev/null +++ b/app/include/linker/zmk-rgb-effects.ld @@ -0,0 +1,9 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +ITERABLE_SECTION_ROM(zmk_rgb_effect, 4) diff --git a/app/include/zmk/ble.h b/app/include/zmk/ble.h index 92b2107d..c65addde 100644 --- a/app/include/zmk/ble.h +++ b/app/include/zmk/ble.h @@ -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); diff --git a/app/include/zmk/endpoints.h b/app/include/zmk/endpoints.h index a2ef3181..aabdb302 100644 --- a/app/include/zmk/endpoints.h +++ b/app/include/zmk/endpoints.h @@ -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) diff --git a/app/include/zmk/events/split_peripheral_layer_changed.h b/app/include/zmk/events/split_peripheral_layer_changed.h new file mode 100644 index 00000000..2445c164 --- /dev/null +++ b/app/include/zmk/events/split_peripheral_layer_changed.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +struct zmk_split_peripheral_layer_changed { + uint32_t layers; +}; + +ZMK_EVENT_DECLARE(zmk_split_peripheral_layer_changed); diff --git a/app/include/zmk/rgb_effect.h b/app/include/zmk/rgb_effect.h new file mode 100644 index 00000000..d70a9160 --- /dev/null +++ b/app/include/zmk/rgb_effect.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +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); +typedef void (*zmk_rgb_effect_idle_fn)(bool awake); +typedef bool (*zmk_rgb_effect_is_active_fn)(void); + +#define ZMK_RGB_EFFECT_STATIC BIT(0) +#define ZMK_RGB_EFFECT_PERSISTENT BIT(1) + +struct zmk_rgb_effect { + const char *name; + zmk_rgb_effect_render_fn render; + zmk_rgb_effect_event_fn on_select; + zmk_rgb_effect_event_fn on_deselect; + zmk_rgb_effect_idle_fn on_idle; + zmk_rgb_effect_is_active_fn is_active; + uint8_t flags; +}; + +#define ZMK_RGB_EFFECT_DEFINE(_sym, _name_str, _render, _flags, _on_select, _on_deselect, \ + _on_idle, _is_active) \ + STRUCT_SECTION_ITERABLE(zmk_rgb_effect, _sym) = { \ + .name = _name_str, \ + .render = _render, \ + .flags = _flags, \ + .on_select = _on_select, \ + .on_deselect = _on_deselect, \ + .on_idle = _on_idle, \ + .is_active = _is_active, \ + } + +void zmk_rgb_request_refresh(void); +void zmk_rgb_request_refresh_wakeup(bool wakeup); +int zmk_rgb_effect_get_count(void); +bool zmk_rgb_is_on(void); +void zmk_rgb_set_tick_delay(int delay_seconds); + +struct led_rgb zmk_rgb_hsb_to_rgb(struct zmk_led_hsb hsb); +struct zmk_led_hsb zmk_rgb_hsb_scale_min_max(struct zmk_led_hsb hsb); +struct zmk_led_hsb zmk_rgb_hsb_scale_zero_max(struct zmk_led_hsb hsb); diff --git a/app/include/zmk/rgb_underglow.h b/app/include/zmk/rgb_underglow.h index be0ef252..f00dcd5e 100644 --- a/app/include/zmk/rgb_underglow.h +++ b/app/include/zmk/rgb_underglow.h @@ -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); diff --git a/app/include/zmk/split/bluetooth/peripheral_layers.h b/app/include/zmk/split/bluetooth/peripheral_layers.h new file mode 100644 index 00000000..974a322c --- /dev/null +++ b/app/include/zmk/split/bluetooth/peripheral_layers.h @@ -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); \ No newline at end of file diff --git a/app/include/zmk/split/bluetooth/uuid.h b/app/include/zmk/split/bluetooth/uuid.h index c9a63efa..6380e08f 100644 --- a/app/include/zmk/split/bluetooth/uuid.h +++ b/app/include/zmk/split/bluetooth/uuid.h @@ -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) diff --git a/app/include/zmk/split/central.h b/app/include/zmk/split/central.h index ff971bfc..3fcf17f2 100644 --- a/app/include/zmk/split/central.h +++ b/app/include/zmk/split/central.h @@ -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); diff --git a/app/src/activity.c b/app/src/activity.c index b109d46d..570b1462 100644 --- a/app/src/activity.c +++ b/app/src/activity.c @@ -16,6 +16,9 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#if IS_ENABLED(CONFIG_ZMK_SPLIT) +#include +#endif #include #include @@ -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) diff --git a/app/src/behaviors/behavior_rgb_underglow.c b/app/src/behaviors/behavior_rgb_underglow.c index 80cd5182..2576486a 100644 --- a/app/src/behaviors/behavior_rgb_underglow.c +++ b/app/src/behaviors/behavior_rgb_underglow.c @@ -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; diff --git a/app/src/ble.c b/app/src/ble.c index 2611eee5..f14df787 100644 --- a/app/src/ble.c +++ b/app/src/ble.c @@ -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; \ diff --git a/app/src/endpoints.c b/app/src/endpoints.c index ae0e5e7f..e96ce796 100644 --- a/app/src/endpoints.c +++ b/app/src/endpoints.c @@ -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); diff --git a/app/src/events/split_peripheral_layer_changed.c b/app/src/events/split_peripheral_layer_changed.c new file mode 100644 index 00000000..81f2ab8d --- /dev/null +++ b/app/src/events/split_peripheral_layer_changed.c @@ -0,0 +1,10 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +ZMK_EVENT_IMPL(zmk_split_peripheral_layer_changed); \ No newline at end of file diff --git a/app/src/ext_power_generic.c b/app/src/ext_power_generic.c index 3975e038..facbbc9a 100644 --- a/app/src/ext_power_generic.c +++ b/app/src/ext_power_generic.c @@ -136,7 +136,11 @@ static int ext_power_settings_commit() { data->status = true; k_work_schedule(&ext_power_save_work, K_NO_WAIT); - ext_power_enable(dev); + 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. - ext_power_enable(dev); + // 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); diff --git a/app/src/keymap.c b/app/src/keymap.c index 762dd4f4..02c9e392 100644 --- a/app/src/keymap.c +++ b/app/src/keymap.c @@ -17,6 +17,9 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#if IS_ENABLED(CONFIG_ZMK_SPLIT_BLE) +#include +#endif #include #include @@ -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; diff --git a/app/src/rgb_underglow.c b/app/src/rgb_underglow.c index 3453fb44..14a3a3ce 100644 --- a/app/src/rgb_underglow.c +++ b/app/src/rgb_underglow.c @@ -12,20 +12,36 @@ #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 + LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if !DT_HAS_CHOSEN(zmk_underglow) @@ -44,25 +60,21 @@ 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; + bool layer_enabled; + 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 +82,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,77 +146,330 @@ 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, NULL, NULL); +ZMK_RGB_EFFECT_DEFINE(effect_breathe, "Breathe", effect_breathe_render, 0, NULL, NULL, NULL, NULL); +ZMK_RGB_EFFECT_DEFINE(effect_spectrum, "Spectrum", effect_spectrum_render, 0, NULL, NULL, NULL, + NULL); +ZMK_RGB_EFFECT_DEFINE(effect_swirl, "Swirl", effect_swirl_render, 0, NULL, NULL, NULL, NULL); + +/* --- Effect registry helpers --- */ + +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; +} + +static bool zmk_rgb_effect_is_persistent(void) { + struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); + return effect && (effect->flags & ZMK_RGB_EFFECT_PERSISTENT); +} + +/* --- Status indicator overlay (separate from effects) --- */ + +static int zmk_led_generate_status(void); + +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); static void zmk_rgb_underglow_tick_handler(struct k_timer *timer) { - if (!state.on) { + if (!state.on && !state.layer_enabled) { return; } @@ -209,11 +478,39 @@ static void zmk_rgb_underglow_tick_handler(struct k_timer *timer) { K_TIMER_DEFINE(underglow_tick, zmk_rgb_underglow_tick_handler, NULL); +bool zmk_rgb_is_on(void) { return state.on; } + +void zmk_rgb_request_refresh(void) { + k_work_submit_to_queue(zmk_workqueue_lowprio_work_q(), &underglow_tick_work); +} + +void zmk_rgb_request_refresh_wakeup(bool wakeup) { + if (!state.on && !state.layer_enabled) { + return; + } + if (!state.on && state.layer_enabled) { + if (!wakeup) { + return; + } + zmk_rgb_underglow_transient_on(); + } + zmk_rgb_request_refresh(); +} + +void zmk_rgb_set_tick_delay(int delay_seconds) { + k_timer_stop(&underglow_tick); + state.animation_step = 0; + if (delay_seconds >= 0) { + k_timer_start(&underglow_tick, K_SECONDS(delay_seconds), K_MSEC(50)); + } +} + +/* --- Settings persistence --- */ + #if IS_ENABLED(CONFIG_SETTINGS) 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 +518,19 @@ 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)); } - + if (state.layer_enabled) { + struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); + if (effect && effect->on_select) { + effect->on_select(); + } + } return 0; } @@ -245,7 +551,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 +578,24 @@ 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)); + } + } + if (state.layer_enabled) { + struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); + if (effect && effect->on_select) { + effect->on_select(); + } } return 0; @@ -293,76 +614,152 @@ int zmk_rgb_underglow_get_state(bool *on_off) { if (!led_strip) return -ENODEV; - *on_off = state.on; + *on_off = state.on || state.layer_enabled; return 0; } -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}; - } - - 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) { + 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(void) { + zmk_rgb_underglow_transient_on(); + if (zmk_rgb_effect_is_persistent()) { + state.layer_enabled = true; + memset(pixels, 0, sizeof(struct led_rgb) * STRIP_NUM_PIXELS); + } + return zmk_rgb_underglow_save_state(); +} + +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(); + state.layer_enabled = false; + return zmk_rgb_underglow_save_state(); +} + +int zmk_rgb_underglow_transient_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(); + 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; } + bool was_persistent = zmk_rgb_effect_is_persistent(); + + struct zmk_rgb_effect *old_eff = zmk_rgb_effect_get(state.current_effect); + if (old_eff && old_eff->on_deselect) { + old_eff->on_deselect(); + } + state.current_effect = effect; state.animation_step = 0; + struct zmk_rgb_effect *new_eff = zmk_rgb_effect_get(state.current_effect); + + state.layer_enabled = (new_eff && (new_eff->flags & ZMK_RGB_EFFECT_PERSISTENT)); + + if (new_eff && new_eff->on_select) { + new_eff->on_select(); + } + + if (state.on) { + if (state.layer_enabled) { + memset(pixels, 0, sizeof(struct led_rgb) * STRIP_NUM_PIXELS); + zmk_rgb_request_refresh(); + } else if (new_eff && (new_eff->flags & ZMK_RGB_EFFECT_STATIC)) { + k_timer_stop(&underglow_tick); + zmk_rgb_request_refresh(); + } else { + if (was_persistent && !state.layer_enabled) { + k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25)); + } else { + k_timer_start(&underglow_tick, K_NO_WAIT, K_MSEC(25)); + } + } + } + return zmk_rgb_underglow_save_state(); } @@ -374,6 +771,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 +903,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,21 +918,30 @@ 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; } sleep_state.is_awake = target_wake_state; if (sleep_state.is_awake) { + struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); + if (effect && effect->on_idle) { + effect->on_idle(true); + return 0; + } if (sleep_state.rgb_state_before_sleeping) { - return zmk_rgb_underglow_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(); + struct zmk_rgb_effect *effect = zmk_rgb_effect_get(state.current_effect); + if (effect && effect->on_idle) { + effect->on_idle(false); + return 0; + } + return zmk_rgb_underglow_transient_off(); } } @@ -509,8 +963,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); diff --git a/app/src/split/bluetooth/CMakeLists.txt b/app/src/split/bluetooth/CMakeLists.txt index f4e12a9d..4258b209 100644 --- a/app/src/split/bluetooth/CMakeLists.txt +++ b/app/src/split/bluetooth/CMakeLists.txt @@ -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) diff --git a/app/src/split/bluetooth/Kconfig b/app/src/split/bluetooth/Kconfig index 5f4a782f..c5f11945 100644 --- a/app/src/split/bluetooth/Kconfig +++ b/app/src/split/bluetooth/Kconfig @@ -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 diff --git a/app/src/split/bluetooth/central.c b/app/src/split/bluetooth/central.c index 685deb51..382c7bad 100644 --- a/app/src/split/bluetooth/central.c +++ b/app/src/split/bluetooth/central.c @@ -33,6 +33,7 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #include #include #include +#include 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), diff --git a/app/src/split/bluetooth/central_listener.c b/app/src/split/bluetooth/central_listener.c new file mode 100644 index 00000000..87e471cb --- /dev/null +++ b/app/src/split/bluetooth/central_listener.c @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022 The ZMK Contributors + * + * SPDX-License-Identifier: MIT + */ + +#include + +LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); + +#include +#include + +#include +#include + +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); diff --git a/app/src/split/bluetooth/peripheral_layers.c b/app/src/split/bluetooth/peripheral_layers.c new file mode 100644 index 00000000..c5e8c68d --- /dev/null +++ b/app/src/split/bluetooth/peripheral_layers.c @@ -0,0 +1,25 @@ + +#include +#include + +#include +#include + +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; +} \ No newline at end of file diff --git a/app/src/split/bluetooth/service.c b/app/src/split/bluetooth/service.c index 5bbed137..9f02a4ca 100644 --- a/app/src/split/bluetooth/service.c +++ b/app/src/split/bluetooth/service.c @@ -31,9 +31,11 @@ LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL); #if IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) #include #endif // IS_ENABLED(CONFIG_ZMK_SPLIT_PERIPHERAL_HID_INDICATORS) +#include #include #include +#include #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);