mirror of
https://github.com/zmkfirmware/zmk.git
synced 2026-03-29 17:35:18 -05:00
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:
4
app/src/indicators/CMakeLists.txt
Normal file
4
app/src/indicators/CMakeLists.txt
Normal 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)
|
||||
4
app/src/indicators/Kconfig
Normal file
4
app/src/indicators/Kconfig
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) 2026 The ZMK Contributors
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
rsource "Kconfig.indicator_leds"
|
||||
19
app/src/indicators/Kconfig.indicator_leds
Normal file
19
app/src/indicators/Kconfig.indicator_leds
Normal 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
|
||||
226
app/src/indicators/indicator_leds.c
Normal file
226
app/src/indicators/indicator_leds.c
Normal 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);
|
||||
Reference in New Issue
Block a user