From 85b2d30bd521424686ba7f1c5ac4fe492f5f2ca5 Mon Sep 17 00:00:00 2001
From: Alessandro Bortolin <bortolin.alessandro@outlook.it>
Date: Sat, 9 Oct 2021 18:24:37 +0200
Subject: [PATCH] feat(lighting): add backlight behavior

---
 app/CMakeLists.txt                            |   2 +
 app/Kconfig                                   |  37 ++-
 app/dts/behaviors.dtsi                        |   1 +
 app/dts/behaviors/backlight.dtsi              |  15 ++
 .../behaviors/zmk,behavior-backlight.yaml     |   8 +
 app/include/dt-bindings/zmk/backlight.h       |  11 +
 app/include/zmk/backlight.h                   |  19 ++
 app/src/backlight.c                           | 224 ++++++++++++++++++
 app/src/behaviors/behavior_backlight.c        |  56 +++++
 9 files changed, 371 insertions(+), 2 deletions(-)
 create mode 100644 app/dts/behaviors/backlight.dtsi
 create mode 100644 app/dts/bindings/behaviors/zmk,behavior-backlight.yaml
 create mode 100644 app/include/dt-bindings/zmk/backlight.h
 create mode 100644 app/include/zmk/backlight.h
 create mode 100644 app/src/backlight.c
 create mode 100644 app/src/behaviors/behavior_backlight.c

diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index 7681efab..25f6c6cd 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -64,6 +64,7 @@ if ((NOT CONFIG_ZMK_SPLIT) OR CONFIG_ZMK_SPLIT_BLE_ROLE_CENTRAL)
   target_sources(app PRIVATE src/keymap.c)
 endif()
 target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/behaviors/behavior_rgb_underglow.c)
+target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/behaviors/behavior_backlight.c)
 target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/behaviors/behavior_bt.c)
 target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/ble.c)
 target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/battery.c)
@@ -77,6 +78,7 @@ endif()
 target_sources_ifdef(CONFIG_USB app PRIVATE src/usb.c)
 target_sources_ifdef(CONFIG_ZMK_BLE app PRIVATE src/hog.c)
 target_sources_ifdef(CONFIG_ZMK_RGB_UNDERGLOW app PRIVATE src/rgb_underglow.c)
+target_sources_ifdef(CONFIG_ZMK_BACKLIGHT app PRIVATE src/backlight.c)
 target_sources(app PRIVATE src/endpoints.c)
 target_sources(app PRIVATE src/hid_listener.c)
 target_sources(app PRIVATE src/main.c)
diff --git a/app/Kconfig b/app/Kconfig
index 76035147..49eec835 100644
--- a/app/Kconfig
+++ b/app/Kconfig
@@ -257,7 +257,7 @@ menu "Display/LED Options"
 
 rsource "src/display/Kconfig"
 
-config ZMK_RGB_UNDERGLOW
+menuconfig ZMK_RGB_UNDERGLOW
 	bool "RGB Adressable LED Underglow"
 	select LED_STRIP
 
@@ -328,6 +328,39 @@ config ZMK_RGB_UNDERGLOW_ON_START
 #ZMK_RGB_UNDERGLOW
 endif
 
+menuconfig ZMK_BACKLIGHT
+	bool "LED backlight"
+	select PWM
+	select LED
+	select ZMK_LED_PWM
+
+if ZMK_BACKLIGHT
+
+config ZMK_BACKLIGHT_BRT_STEP
+	int "Brightness step in percent"
+	range 1 100
+	default 20
+
+config ZMK_BACKLIGHT_BRT_START
+	int "Default brightness in percent"
+	range 1 100
+	default 40
+
+config ZMK_BACKLIGHT_ON_START
+	bool "Default backlight state"
+	default y
+
+config ZMK_BACKLIGHT_AUTO_OFF_IDLE
+	bool "Turn off backlight when keyboard goes into idle state"
+	default y
+
+config ZMK_BACKLIGHT_AUTO_OFF_USB
+	bool "Turn off backlight when USB is disconnected"
+	default n
+
+#ZMK_BACKLIGHT
+endif
+
 #Display/LED Options
 endmenu
 
@@ -378,7 +411,7 @@ config ZMK_COMBO_MAX_KEYS_PER_COMBO
 	int "Maximum number of keys per combo"
 	default 4
 
-#Display/LED Options
+#Combo options
 endmenu
 
 menu "Advanced"
diff --git a/app/dts/behaviors.dtsi b/app/dts/behaviors.dtsi
index 06489616..3e797cc9 100644
--- a/app/dts/behaviors.dtsi
+++ b/app/dts/behaviors.dtsi
@@ -16,3 +16,4 @@
 #include <behaviors/outputs.dtsi>
 #include <behaviors/caps_word.dtsi>
 #include <behaviors/key_repeat.dtsi>
+#include <behaviors/backlight.dtsi>
diff --git a/app/dts/behaviors/backlight.dtsi b/app/dts/behaviors/backlight.dtsi
new file mode 100644
index 00000000..b05d97ae
--- /dev/null
+++ b/app/dts/behaviors/backlight.dtsi
@@ -0,0 +1,15 @@
+/*
+ * Copyright (c) 2021 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+ / {
+	behaviors {
+		/omit-if-no-ref/ bl: behavior_backlight {
+			compatible = "zmk,behavior-backlight";
+			label = "BACKLIGHT";
+			#binding-cells = <1>;
+		};
+	};
+};
diff --git a/app/dts/bindings/behaviors/zmk,behavior-backlight.yaml b/app/dts/bindings/behaviors/zmk,behavior-backlight.yaml
new file mode 100644
index 00000000..e035e15e
--- /dev/null
+++ b/app/dts/bindings/behaviors/zmk,behavior-backlight.yaml
@@ -0,0 +1,8 @@
+# Copyright (c) 2021 The ZMK Contributors
+# SPDX-License-Identifier: MIT
+
+description: Backlight behavior
+
+compatible: "zmk,behavior-backlight"
+
+include: one_param.yaml
diff --git a/app/include/dt-bindings/zmk/backlight.h b/app/include/dt-bindings/zmk/backlight.h
new file mode 100644
index 00000000..c33e4344
--- /dev/null
+++ b/app/include/dt-bindings/zmk/backlight.h
@@ -0,0 +1,11 @@
+/*
+ * Copyright (c) 2021 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#define BL_TOG 0
+#define BL_ON 1
+#define BL_OFF 2
+#define BL_INC 3
+#define BL_DEC 4
diff --git a/app/include/zmk/backlight.h b/app/include/zmk/backlight.h
new file mode 100644
index 00000000..817efe7a
--- /dev/null
+++ b/app/include/zmk/backlight.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2021 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#pragma once
+
+int zmk_backlight_set_on(bool on);
+bool zmk_backlight_is_on();
+
+int zmk_backlight_set_brt(int brt);
+int zmk_backlight_get_brt();
+
+int zmk_backlight_toggle();
+int zmk_backlight_on();
+int zmk_backlight_off();
+int zmk_backlight_inc();
+int zmk_backlight_dec();
diff --git a/app/src/backlight.c b/app/src/backlight.c
new file mode 100644
index 00000000..517e6133
--- /dev/null
+++ b/app/src/backlight.c
@@ -0,0 +1,224 @@
+/*
+ * Copyright (c) 2021 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <device.h>
+#include <devicetree.h>
+#include <init.h>
+#include <kernel.h>
+
+#include <drivers/led.h>
+#include <logging/log.h>
+#include <settings/settings.h>
+
+#include <zmk/activity.h>
+#include <zmk/backlight.h>
+#include <zmk/usb.h>
+#include <zmk/event_manager.h>
+#include <zmk/events/activity_state_changed.h>
+#include <zmk/events/usb_conn_state_changed.h>
+
+LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
+
+BUILD_ASSERT(DT_HAS_CHOSEN(zmk_backlight),
+             "CONFIG_ZMK_BACKLIGHT is enabled but no zmk,backlight chosen node found");
+
+static const struct device *const backlight_dev = DEVICE_DT_GET(DT_CHOSEN(zmk_backlight));
+
+#define CHILD_COUNT(...) +1
+#define DT_NUM_CHILD(node_id) (DT_FOREACH_CHILD(node_id, CHILD_COUNT))
+
+#define BACKLIGHT_NUM_LEDS (DT_NUM_CHILD(DT_CHOSEN(zmk_backlight)))
+
+struct backlight_state {
+    uint8_t brightness;
+    bool on;
+};
+
+static struct backlight_state state = {.brightness = CONFIG_ZMK_BACKLIGHT_BRT_START,
+                                       .on = IS_ENABLED(CONFIG_ZMK_BACKLIGHT_ON_START)};
+
+static int zmk_backlight_update() {
+    uint8_t brt = state.on ? state.brightness : 0;
+    for (int i = 0; i < BACKLIGHT_NUM_LEDS; i++) {
+        int rc = led_set_brightness(backlight_dev, i, brt);
+        if (rc != 0) {
+            return rc;
+        }
+    }
+    return 0;
+}
+
+#if IS_ENABLED(CONFIG_SETTINGS)
+static int backlight_settings_set(const char *name, size_t len, settings_read_cb read_cb,
+                                  void *cb_arg) {
+    const char *next;
+
+    if (settings_name_steq(name, "state", &next) && !next) {
+        if (len != sizeof(state)) {
+            return -EINVAL;
+        }
+
+        int rc = read_cb(cb_arg, &state, sizeof(state));
+        if (rc < 0) {
+            return rc;
+        }
+
+        return zmk_backlight_update();
+    }
+
+    return -ENOENT;
+}
+
+static struct settings_handler backlight_conf = {.name = "backlight",
+                                                 .h_set = backlight_settings_set};
+
+static void zmk_backlight_save_state_work() {
+    settings_save_one("backlight/state", &state, sizeof(state));
+}
+
+static struct k_delayed_work backlight_save_work;
+#endif // IS_ENABLED(CONFIG_SETTINGS)
+
+static int zmk_backlight_save_state() {
+#if IS_ENABLED(CONFIG_SETTINGS)
+    k_delayed_work_cancel(&backlight_save_work);
+    return k_delayed_work_submit(&backlight_save_work, K_MSEC(CONFIG_ZMK_SETTINGS_SAVE_DEBOUNCE));
+#else
+    return 0;
+#endif
+}
+
+static int zmk_backlight_init(const struct device *_arg) {
+    if (!device_is_ready(backlight_dev)) {
+        LOG_ERR("Backlight device \"%s\" is not ready", backlight_dev->name);
+        return -ENODEV;
+    }
+
+#if IS_ENABLED(CONFIG_SETTINGS)
+    settings_subsys_init();
+
+    int err = settings_register(&backlight_conf);
+    if (err) {
+        LOG_ERR("Failed to register the backlight settings handler (err %d)", err);
+        return err;
+    }
+
+    k_delayed_work_init(&backlight_save_work, zmk_backlight_save_state_work);
+
+    settings_load_subtree("backlight");
+#endif
+
+    return zmk_backlight_update();
+}
+
+int zmk_backlight_set_on(bool on) {
+    if (!state.on && state.brightness == 0) {
+        state.brightness = CONFIG_ZMK_BACKLIGHT_BRT_STEP;
+    }
+    state.on = on;
+
+    int rc = zmk_backlight_update();
+    if (rc != 0) {
+        return rc;
+    }
+
+    return zmk_backlight_save_state();
+}
+
+bool zmk_backlight_is_on() { return state.on; }
+
+int zmk_backlight_set_brt(int brt) {
+    state.on = (brt > 0);
+    state.brightness = CLAMP(brt, 0, 100);
+
+    int rc = zmk_backlight_update();
+    if (rc != 0) {
+        return rc;
+    }
+
+    return zmk_backlight_save_state();
+}
+
+int zmk_backlight_get_brt() { return state.on ? state.brightness : 0; }
+
+int zmk_backlight_toggle() { return zmk_backlight_set_on(!state.on); }
+
+int zmk_backlight_on() { return zmk_backlight_set_on(true); }
+
+int zmk_backlight_off() { return zmk_backlight_set_on(false); }
+
+int zmk_backlight_inc() {
+    if (!state.on) {
+        return zmk_backlight_set_brt(MAX(state.brightness, CONFIG_ZMK_BACKLIGHT_BRT_STEP));
+    }
+    return zmk_backlight_set_brt(state.brightness + CONFIG_ZMK_BACKLIGHT_BRT_STEP);
+}
+
+int zmk_backlight_dec() {
+    return zmk_backlight_set_brt(state.brightness - CONFIG_ZMK_BACKLIGHT_BRT_STEP);
+}
+
+#if IS_ENABLED(CONFIG_ZMK_BACKLIGHT_AUTO_OFF_IDLE)
+static bool auto_off_idle_prev_state = false;
+#endif
+
+#if IS_ENABLED(CONFIG_ZMK_BACKLIGHT_AUTO_OFF_USB)
+static bool auto_off_usb_prev_state = false;
+#endif
+
+#if IS_ENABLED(CONFIG_ZMK_BACKLIGHT_AUTO_OFF_IDLE) || IS_ENABLED(CONFIG_ZMK_BACKLIGHT_AUTO_OFF_USB)
+static int backlight_event_listener(const zmk_event_t *eh) {
+
+#if IS_ENABLED(CONFIG_ZMK_BACKLIGHT_AUTO_OFF_IDLE)
+    if (as_zmk_activity_state_changed(eh)) {
+        bool new_state = (zmk_activity_get_state() == ZMK_ACTIVITY_ACTIVE);
+        if (state.on == new_state) {
+            return 0;
+        }
+        if (new_state) {
+            state.on = auto_off_idle_prev_state;
+            auto_off_idle_prev_state = false;
+        } else {
+            state.on = false;
+            auto_off_idle_prev_state = true;
+        }
+        return zmk_backlight_update();
+    }
+#endif
+
+#if IS_ENABLED(CONFIG_ZMK_BACKLIGHT_AUTO_OFF_USB)
+    if (as_zmk_usb_conn_state_changed(eh)) {
+        bool new_state = zmk_usb_is_powered();
+        if (state.on == new_state) {
+            return 0;
+        }
+        if (new_state) {
+            state.on = auto_off_usb_prev_state;
+            auto_off_usb_prev_state = false;
+        } else {
+            state.on = false;
+            auto_off_usb_prev_state = true;
+        }
+        return zmk_backlight_update();
+    }
+#endif
+
+    return -ENOTSUP;
+}
+
+ZMK_LISTENER(backlight, backlight_event_listener);
+#endif // IS_ENABLED(CONFIG_ZMK_BACKLIGHT_AUTO_OFF_IDLE) ||
+       // IS_ENABLED(CONFIG_ZMK_BACKLIGHT_AUTO_OFF_USB)
+
+#if IS_ENABLED(CONFIG_ZMK_BACKLIGHT_AUTO_OFF_IDLE)
+ZMK_SUBSCRIPTION(backlight, zmk_activity_state_changed);
+#endif
+
+#if IS_ENABLED(CONFIG_ZMK_BACKLIGHT_AUTO_OFF_USB)
+ZMK_SUBSCRIPTION(backlight, zmk_usb_conn_state_changed);
+#endif
+
+SYS_INIT(zmk_backlight_init, APPLICATION, CONFIG_APPLICATION_INIT_PRIORITY);
diff --git a/app/src/behaviors/behavior_backlight.c b/app/src/behaviors/behavior_backlight.c
new file mode 100644
index 00000000..8d921f45
--- /dev/null
+++ b/app/src/behaviors/behavior_backlight.c
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2021 The ZMK Contributors
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#define DT_DRV_COMPAT zmk_behavior_backlight
+
+#include <device.h>
+#include <drivers/behavior.h>
+#include <logging/log.h>
+
+#include <dt-bindings/zmk/backlight.h>
+#include <zmk/backlight.h>
+#include <zmk/keymap.h>
+
+LOG_MODULE_DECLARE(zmk, CONFIG_ZMK_LOG_LEVEL);
+
+#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
+
+static int behavior_backlight_init(const struct device *dev) { return 0; }
+
+static int on_keymap_binding_pressed(struct zmk_behavior_binding *binding,
+                                     struct zmk_behavior_binding_event event) {
+    switch (binding->param1) {
+    case BL_TOG:
+        return zmk_backlight_toggle();
+    case BL_ON:
+        return zmk_backlight_on();
+    case BL_OFF:
+        return zmk_backlight_off();
+    case BL_INC:
+        return zmk_backlight_inc();
+    case BL_DEC:
+        return zmk_backlight_dec();
+    default:
+        LOG_ERR("Unknown backlight command: %d", binding->param1);
+    }
+
+    return -ENOTSUP;
+}
+
+static int on_keymap_binding_released(struct zmk_behavior_binding *binding,
+                                      struct zmk_behavior_binding_event event) {
+    return ZMK_BEHAVIOR_OPAQUE;
+}
+
+static const struct behavior_driver_api behavior_backlight_driver_api = {
+    .binding_pressed = on_keymap_binding_pressed,
+    .binding_released = on_keymap_binding_released,
+};
+
+DEVICE_DT_INST_DEFINE(0, behavior_backlight_init, device_pm_control_nop, NULL, NULL, APPLICATION,
+                      CONFIG_KERNEL_INIT_PRIORITY_DEFAULT, &behavior_backlight_driver_api);
+
+#endif /* DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT) */