/* * Copyright (c) 2025 The ZMK Contributors * * SPDX-License-Identifier: MIT */ #define DT_DRV_COMPAT zmk_indicator_leds #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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);