forked from kofal.net/zmk
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.
227 lines
8.9 KiB
C
227 lines
8.9 KiB
C
/*
|
|
* 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);
|