feat: Add LED indicator support (#3239)

This adds a new zmk,indicator-leds device, which maps HID indicator
states onto any devices that implement the LED driver API. This adds
support for things like a caps lock LED.

The name was chosen so that more drivers could be added later as
zmk,indicator-*, for example a version that uses the LED strip API.
This commit is contained in:
Joel Spadin
2026-03-23 01:35:35 -05:00
committed by GitHub
parent a1aca03aa0
commit 197f899777
12 changed files with 540 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# Copyright (c) 2025 The ZMK Contributors
# SPDX-License-Identifier: MIT
target_sources_ifdef(CONFIG_ZMK_INDICATOR_LEDS app PRIVATE indicator_leds.c)

View File

@@ -0,0 +1,4 @@
# Copyright (c) 2026 The ZMK Contributors
# SPDX-License-Identifier: MIT
rsource "Kconfig.indicator_leds"

View File

@@ -0,0 +1,19 @@
# Copyright (c) 2025 The ZMK Contributors
# SPDX-License-Identifier: MIT
config ZMK_INDICATOR_LEDS
bool
default y
depends on DT_HAS_ZMK_INDICATOR_LEDS_ENABLED
select LED
select ZMK_HID_INDICATORS
if ZMK_INDICATOR_LEDS
config ZMK_INDICATOR_LEDS_INIT_PRIORITY
int "ZMK indicator LED initialization priority"
default 91
help
System initialization priority for ZMK indicator LEDs.
endif # ZMK_INDICATOR_LEDS

View File

@@ -0,0 +1,226 @@
/*
* Copyright (c) 2025 The ZMK Contributors
*
* SPDX-License-Identifier: MIT
*/
#define DT_DRV_COMPAT zmk_indicator_leds
#include <errno.h>
#include <zephyr/device.h>
#include <zephyr/kernel.h>
#include <zephyr/drivers/led.h>
#include <zephyr/pm/device.h>
#include <zmk/endpoints.h>
#include <zmk/event_manager.h>
#include <zmk/hid_indicators.h>
#include <zmk/usb.h>
#include <zmk/events/activity_state_changed.h>
#include <zmk/events/endpoint_changed.h>
#include <zmk/events/hid_indicators_changed.h>
#include <zmk/events/usb_conn_state_changed.h>
#include <zephyr/logging/log.h>
LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
struct indicator_led_child_config {
size_t leds_len;
const struct led_dt_spec *leds;
zmk_hid_indicators_t indicator;
uint8_t active_brightness;
uint8_t inactive_brightness;
uint8_t disconnected_brightness;
bool on_while_idle;
};
struct indicator_led_config {
size_t indicators_len;
const struct indicator_led_child_config *indicators;
};
struct indicator_led_data {
enum zmk_activity_state activity_state;
zmk_hid_indicators_t indicators;
bool usb_powered;
bool pm_suspended;
bool endpoint_connected;
};
static bool is_led_disabled(const struct indicator_led_child_config *config,
const struct indicator_led_data *data) {
// LEDs should always be off if the device is suspended.
if (data->pm_suspended) {
return true;
}
// If the keyboard is powered, LEDs don't need to be disabled to save power.
if (data->usb_powered) {
return false;
}
switch (data->activity_state) {
case ZMK_ACTIVITY_ACTIVE:
return false;
case ZMK_ACTIVITY_IDLE:
return !config->on_while_idle;
case ZMK_ACTIVITY_SLEEP:
return true;
}
LOG_ERR("Unhandled activity state %d", data->activity_state);
return false;
}
static uint8_t get_brightness(const struct indicator_led_child_config *config,
const struct indicator_led_data *data) {
if (is_led_disabled(config, data)) {
return 0;
}
if (!data->endpoint_connected) {
return config->disconnected_brightness;
}
const bool active = data->indicators & config->indicator;
return active ? config->active_brightness : config->inactive_brightness;
}
static int update_indicator(const struct indicator_led_child_config *config,
const struct indicator_led_data *data) {
const uint8_t value = get_brightness(config, data);
for (int i = 0; i < config->leds_len; i++) {
const struct led_dt_spec *spec = &config->leds[i];
const int err = led_set_brightness_dt(spec, value);
if (err) {
LOG_ERR("Failed to set %s %u to %u%%: %d", spec->dev->name, spec->index, value, err);
return err;
}
LOG_DBG("Set %s %u to %u%%", spec->dev->name, spec->index, value);
}
return 0;
}
static int update_device(const struct device *dev) {
const struct indicator_led_config *config = dev->config;
struct indicator_led_data *data = dev->data;
data->activity_state = zmk_activity_get_state();
data->indicators = zmk_hid_indicators_get_current_profile();
data->usb_powered = zmk_usb_is_powered();
data->endpoint_connected = zmk_endpoint_is_connected();
for (int i = 0; i < config->indicators_len; i++) {
const int err = update_indicator(&config->indicators[i], data);
if (err) {
return err;
}
}
return 0;
}
#define INST_DEV(n) DEVICE_DT_GET(DT_DRV_INST(n)),
static const struct device *all_instances[] = {DT_INST_FOREACH_STATUS_OKAY(INST_DEV)};
static void update_all_indicators(struct k_work *work) {
LOG_DBG("Updating indicator LEDs");
for (int i = 0; i < ARRAY_SIZE(all_instances); i++) {
if (device_is_ready(all_instances[i])) {
update_device(all_instances[i]);
}
}
}
// We may get multiple events at the same time (e.g. endpoint changed will
// also trigger HID indicators changed), but we only need to update the LEDs
// once per batch of events, so defer the updates with a work item.
static K_WORK_DEFINE(update_all_indicators_work, update_all_indicators);
static int indicator_led_event_listener(const zmk_event_t *eh) {
k_work_submit(&update_all_indicators_work);
return ZMK_EV_EVENT_BUBBLE;
}
static int indicator_led_init(const struct device *dev) { return update_device(dev); }
ZMK_LISTENER(indicator_led, indicator_led_event_listener);
ZMK_SUBSCRIPTION(indicator_led, zmk_activity_state_changed);
ZMK_SUBSCRIPTION(indicator_led, zmk_hid_indicators_changed);
ZMK_SUBSCRIPTION(indicator_led, zmk_usb_conn_state_changed);
ZMK_SUBSCRIPTION(indicator_led, zmk_endpoint_changed);
#if IS_ENABLED(CONFIG_PM_DEVICE)
static int indicator_led_init_pm_action(const struct device *dev, enum pm_device_action action) {
struct indicator_led_data *data = dev->data;
switch (action) {
case PM_DEVICE_ACTION_SUSPEND:
data->pm_suspended = true;
return update_device(dev);
case PM_DEVICE_ACTION_RESUME:
data->pm_suspended = false;
return update_device(dev);
default:
return -ENOTSUP;
}
}
#endif // IS_ENABLED(CONFIG_PM_DEVICE)
#define LED_DT_SPEC_GET_BY_IDX(node_id, prop, idx) \
LED_DT_SPEC_GET(DT_PHANDLE_BY_IDX(node_id, prop, idx))
#define CHILD_LEDS_ARRAY(inst) DT_CAT(indicator_led_dt_spec_, inst)
#define DEFINE_CHILD_LEDS(inst) \
static const struct led_dt_spec CHILD_LEDS_ARRAY(inst)[] = { \
DT_FOREACH_PROP_ELEM_SEP(inst, leds, LED_DT_SPEC_GET_BY_IDX, (, )), \
};
#define CHILD_CONFIG(inst) \
{ \
.leds_len = ARRAY_SIZE(CHILD_LEDS_ARRAY(inst)), \
.leds = CHILD_LEDS_ARRAY(inst), \
.indicator = DT_PROP(inst, indicator), \
.active_brightness = DT_PROP_OR(inst, active_brightness, 100), \
.inactive_brightness = DT_PROP_OR(inst, inactive_brightness, 0), \
.disconnected_brightness = DT_PROP_OR(inst, disconnected_brightness, 0), \
.on_while_idle = DT_PROP_OR(inst, on_while_idle, false), \
},
#define INDICATOR_LED_DEVICE(n) \
DT_INST_FOREACH_CHILD(n, DEFINE_CHILD_LEDS) \
\
static const struct indicator_led_child_config indicator_led_children_##n[] = { \
DT_INST_FOREACH_CHILD(n, CHILD_CONFIG)}; \
\
static const struct indicator_led_config indicator_led_config_##n = { \
.indicators_len = ARRAY_SIZE(indicator_led_children_##n), \
.indicators = indicator_led_children_##n, \
}; \
\
static struct indicator_led_data indicator_led_data_##n = { \
.activity_state = ZMK_ACTIVITY_ACTIVE, \
.indicators = 0, \
.usb_powered = true, \
.pm_suspended = false, \
}; \
\
PM_DEVICE_DT_INST_DEFINE(n, indicator_led_init_pm_action); \
\
DEVICE_DT_INST_DEFINE(n, &indicator_led_init, PM_DEVICE_DT_INST_GET(n), \
&indicator_led_data_##n, &indicator_led_config_##n, POST_KERNEL, \
CONFIG_ZMK_INDICATOR_LEDS_INIT_PRIORITY, NULL);
DT_INST_FOREACH_STATUS_OKAY(INDICATOR_LED_DEVICE);