Some Intel Next Unit of Computing (NUC) machines have
software-configured LEDs that can be used to display a
variety of events:

        - Power State
        - HDD Activity
        - Ethernet
        - WiFi
        - Power Limit

They can even be controlled directly via software, without
any hardware-specific indicator connected into them.

Some devices have mono-colored LEDs, but the more advanced
ones have RGB leds that can show any color.

Different color and 4 blink states can be programmed for
thee system states:

        - powered on (S0);
        - S3;
        - Standby.

The NUC BIOSes allow to partially set them for S0, but doesn't
provide any control for the other states, nor does allow
changing the blinking logic.

They all use a WMI interface using GUID:
        8C5DA44C-CDC3-46b3-8619-4E26D34390B7

But there are 3 different revisions of the spec, all using
the same GUID, but two different APIs:

- the original one, for NUC6 and to NUCi7:
        - 
https://www.intel.com/content/www/us/en/support/articles/000023426/intel-nuc/intel-nuc-kits.html

- a new one, starting with NUCi8, with two revisions:
        - 
https://raw.githubusercontent.com/nomego/intel_nuc_led/master/specs/INTEL_WMI_LED_0.64.pdf
        - 
https://www.intel.com/content/dam/support/us/en/documents/intel-nuc/WMI-Spec-Intel-NUC-NUC10ixFNx.pdf

There are some OOT drivers for them, but they use procfs
and use a messy interface to setup it. Also, there are
different drivers with the same name, each with support
for each NUC family.

Let's start a new driver from scratch, using the x86 platform
WMI core and the LED class.

This initial version is compatible with NUCi8 and above, and it
was tested with a Hades Canyon NUC (NUC8i7HNK).

Signed-off-by: Mauro Carvalho Chehab <mchehab+hua...@kernel.org>
---
 MAINTAINERS                       |   6 +
 drivers/staging/Kconfig           |   2 +
 drivers/staging/Makefile          |   1 +
 drivers/staging/nuc-led/Kconfig   |  11 +
 drivers/staging/nuc-led/Makefile  |   3 +
 drivers/staging/nuc-led/TODO      |   6 +
 drivers/staging/nuc-led/nuc-wmi.c | 489 ++++++++++++++++++++++++++++++
 7 files changed, 518 insertions(+)
 create mode 100644 drivers/staging/nuc-led/Kconfig
 create mode 100644 drivers/staging/nuc-led/Makefile
 create mode 100644 drivers/staging/nuc-led/TODO
 create mode 100644 drivers/staging/nuc-led/nuc-wmi.c

diff --git a/MAINTAINERS b/MAINTAINERS
index bd7aff0c120f..50d181e1d745 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13063,6 +13063,12 @@ T:     git 
git://git.kernel.org/pub/scm/linux/kernel/git/aia21/ntfs.git
 F:     Documentation/filesystems/ntfs.rst
 F:     fs/ntfs/
 
+NUC LED DRIVER
+M:      Mauro Carvalho Chehab <mche...@kernel.org>
+L:      de...@driverdev.osuosl.org
+S:      Maintained
+F:      drivers/staging/nuc-led
+
 NUBUS SUBSYSTEM
 M:     Finn Thain <fth...@telegraphics.com.au>
 L:     linux-m...@lists.linux-m68k.org
diff --git a/drivers/staging/Kconfig b/drivers/staging/Kconfig
index b7ae5bdc4eb5..d1a8e3e08d00 100644
--- a/drivers/staging/Kconfig
+++ b/drivers/staging/Kconfig
@@ -84,6 +84,8 @@ source "drivers/staging/greybus/Kconfig"
 
 source "drivers/staging/vc04_services/Kconfig"
 
+source "drivers/staging/nuc-led/Kconfig"
+
 source "drivers/staging/pi433/Kconfig"
 
 source "drivers/staging/mt7621-pci/Kconfig"
diff --git a/drivers/staging/Makefile b/drivers/staging/Makefile
index 075c979bfe7c..de937f947edb 100644
--- a/drivers/staging/Makefile
+++ b/drivers/staging/Makefile
@@ -29,6 +29,7 @@ obj-$(CONFIG_UNISYSSPAR)      += unisys/
 obj-$(CONFIG_COMMON_CLK_XLNX_CLKWZRD)  += clocking-wizard/
 obj-$(CONFIG_FB_TFT)           += fbtft/
 obj-$(CONFIG_MOST)             += most/
+obj-$(CONFIG_LEDS_NUC_WMI)     += nuc-led/
 obj-$(CONFIG_KS7010)           += ks7010/
 obj-$(CONFIG_GREYBUS)          += greybus/
 obj-$(CONFIG_BCM2835_VCHIQ)    += vc04_services/
diff --git a/drivers/staging/nuc-led/Kconfig b/drivers/staging/nuc-led/Kconfig
new file mode 100644
index 000000000000..0f870f45bf44
--- /dev/null
+++ b/drivers/staging/nuc-led/Kconfig
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0
+
+config LEDS_NUC_WMI
+       tristate "Intel NUC WMI support for LEDs"
+       depends on LEDS_CLASS
+       depends on ACPI_WMI
+       help
+         Enable this to support the Intel NUC WMI support for
+         LEDs, starting from NUCi8 and upper devices.
+
+         To compile this driver as a module, choose M here.
diff --git a/drivers/staging/nuc-led/Makefile b/drivers/staging/nuc-led/Makefile
new file mode 100644
index 000000000000..abba9e305fa1
--- /dev/null
+++ b/drivers/staging/nuc-led/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0
+
+obj-$(CONFIG_LEDS_NUC_WMI)             += nuc-wmi.o
diff --git a/drivers/staging/nuc-led/TODO b/drivers/staging/nuc-led/TODO
new file mode 100644
index 000000000000..d5296d7186a7
--- /dev/null
+++ b/drivers/staging/nuc-led/TODO
@@ -0,0 +1,6 @@
+- Add support for 6th gen NUCs, like Skull Canyon
+- Improve LED core support to avoid it to try to manage the
+  LED brightness directly;
+- Test it with 8th gen NUCs;
+- Add more functionality to the driver;
+- Stabilize and document its sysfs interface.
diff --git a/drivers/staging/nuc-led/nuc-wmi.c 
b/drivers/staging/nuc-led/nuc-wmi.c
new file mode 100644
index 000000000000..15d956ad8556
--- /dev/null
+++ b/drivers/staging/nuc-led/nuc-wmi.c
@@ -0,0 +1,489 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Intel NUC WMI Control WMI Driver
+ *
+ * Currently, it implements only the LED support
+ *
+ * Copyright(c) 2021 Mauro Carvalho Chehab
+ *
+ * Inspired on WMI from https://github.com/nomego/intel_nuc_led
+ *
+ * It follows this spec:
+ *     
https://www.intel.com/content/dam/support/us/en/documents/intel-nuc/WMI-Spec-Intel-NUC-NUC10ixFNx.pdf
+ */
+
+#include <linux/acpi.h>
+#include <linux/bits.h>
+#include <linux/kernel.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/wmi.h>
+
+#define NUC_LED_WMI_GUID       "8C5DA44C-CDC3-46B3-8619-4E26D34390B7"
+
+#define MAX_LEDS               7
+#define NUM_INPUT_ARGS         4
+#define NUM_OUTPUT_ARGS                3
+
+enum led_cmds {
+       LED_QUERY                       = 0x03,
+       LED_NEW_GET_STATUS              = 0x04,
+       LED_SET_INDICATOR               = 0x05,
+       LED_SET_VALUE                   = 0x06,
+       LED_NOTIFICATION                = 0x07,
+       LED_SWITCH_TYPE                 = 0x08,
+};
+
+enum led_query_subcmd {
+       LED_QUERY_LIST_ALL              = 0x00,
+       LED_QUERY_COLOR_TYPE            = 0x01,
+       LED_QUERY_INDICATOR_OPTIONS     = 0x02,
+       LED_QUERY_CONTROL_ITEMS         = 0x03,
+};
+
+enum led_new_get_subcmd {
+       LED_NEW_GET_CURRENT_INDICATOR   = 0x00,
+       LED_NEW_GET_CONTROL_ITEM        = 0x01,
+};
+
+/* LED color indicator */
+#define LED_BLUE_AMBER         BIT(0)
+#define LED_BLUE_WHITE         BIT(1)
+#define LED_RGB                        BIT(2)
+#define        LED_SINGLE_COLOR        BIT(3)
+
+/* LED indicator options */
+#define LED_IND_POWER_STATE    BIT(0)
+#define LED_IND_HDD_ACTIVITY   BIT(1)
+#define LED_IND_ETHERNET       BIT(2)
+#define LED_IND_WIFI           BIT(3)
+#define LED_IND_SOFTWARE       BIT(4)
+#define LED_IND_POWER_LIMIT    BIT(5)
+#define LED_IND_DISABLE                BIT(6)
+
+static const char * const led_names[] = {
+       "nuc::power",
+       "nuc::hdd",
+       "nuc::skull",
+       "nuc::eyes",
+       "nuc::front1",
+       "nuc::front2",
+       "nuc::front3",
+};
+
+struct nuc_nmi_led {
+       struct led_classdev cdev;
+       struct device *dev;
+       u8 id;
+       u8 indicator;
+       u32 color_type;
+       u32 avail_indicators;
+       u32 control_items;
+};
+
+struct nuc_wmi {
+       struct nuc_nmi_led led[MAX_LEDS * 3];   /* Worse case: RGB LEDs */
+       int num_leds;
+
+       /* Avoid concurrent access to WMI */
+       struct mutex wmi_lock;
+};
+
+static int nuc_nmi_led_error(u8 error_code)
+{
+       switch (error_code) {
+       case 0:
+               return 0;
+       case 0xe1:      /* Function not support */
+               return -ENOENT;
+       case 0xe2:      /* Undefined device */
+               return -ENODEV;
+       case 0xe3:      /* EC no respond */
+               return -EIO;
+       case 0xe4:      /* Invalid Parameter */
+               return -EINVAL;
+       case 0xef:      /* Unexpected error */
+               return -EFAULT;
+
+       /* Revision 1.0 Errors */
+       case 0xe5:      /* Node busy */
+               return -EBUSY;
+       case 0xe6:      /* Destination device is disabled or unavailable */
+               return -EACCES;
+       case 0xe7:      /* Invalid CEC Opcode */
+               return -ENOENT;
+       case 0xe8:      /* Data Buffer size is not enough */
+               return -ENOSPC;
+
+       default:        /* Reserved */
+               return -EPROTO;
+       }
+}
+
+static int nuc_nmi_cmd(struct device *dev,
+                      u8 cmd,
+                      u8 input_args[NUM_INPUT_ARGS],
+                      u8 output_args[NUM_OUTPUT_ARGS])
+{
+       struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL };
+       struct nuc_wmi *priv = dev_get_drvdata(dev);
+       struct acpi_buffer input;
+       union acpi_object *obj;
+       acpi_status status;
+       int size, ret;
+       u8 *p;
+
+       input.length = NUM_INPUT_ARGS;
+       input.pointer = input_args;
+
+       mutex_lock(&priv->wmi_lock);
+       status = wmi_evaluate_method(NUC_LED_WMI_GUID, 0, cmd,
+                                    &input, &output);
+       mutex_unlock(&priv->wmi_lock);
+       if (ACPI_FAILURE(status)) {
+               dev_warn(dev, "cmd %02x (%*ph): ACPI failure: %d\n",
+                        cmd, (int)input.length, input_args, ret);
+               return status;
+       }
+
+       obj = output.pointer;
+       if (!obj) {
+               dev_warn(dev, "cmd %02x (%*ph): no output\n",
+                        cmd, (int)input.length, input_args);
+               return -EINVAL;
+       }
+
+       if (obj->type == ACPI_TYPE_BUFFER) {
+               if (obj->buffer.length < NUM_OUTPUT_ARGS + 1) {
+                       ret = -EINVAL;
+                       goto err;
+               }
+               p = (u8 *)obj->buffer.pointer;
+       } else if (obj->type == ACPI_TYPE_INTEGER) {
+               p = (u8 *)&obj->integer.value;
+       } else {
+               return -EINVAL;
+       }
+
+       ret = nuc_nmi_led_error(p[0]);
+       if (ret) {
+               dev_warn(dev, "cmd %02x (%*ph): WMI error code: %02x\n",
+                        cmd, (int)input.length, input_args, p[0]);
+               goto err;
+       }
+
+       size = NUM_OUTPUT_ARGS + 1;
+
+       if (output_args) {
+               memcpy(output_args, p + 1, NUM_OUTPUT_ARGS);
+
+               dev_info(dev, "cmd %02x (%*ph), return: %*ph\n",
+                        cmd, (int)input.length, input_args, NUM_OUTPUT_ARGS, 
output_args);
+       } else {
+               dev_info(dev, "cmd %02x (%*ph)\n",
+                        cmd, (int)input.length, input_args);
+       }
+
+err:
+       kfree(obj);
+       return ret;
+}
+
+static int nuc_wmi_query_leds(struct device *dev)
+{
+       struct nuc_wmi *priv = dev_get_drvdata(dev);
+       u8 cmd, input[NUM_INPUT_ARGS] = { 0 };
+       u8 output[NUM_OUTPUT_ARGS];
+       int i, id, ret;
+       u8 leds;
+
+       /*
+        * List all LED types support in the platform
+        *
+        * Should work with both NUC8iXXX and NUC10iXXX
+        *
+        * FIXME: Should add a fallback code for it to work with older NUCs,
+        * as LED_QUERY returns an error on older devices like Skull Canyon.
+        */
+       cmd = LED_QUERY;
+       input[0] = LED_QUERY_LIST_ALL;
+       ret = nuc_nmi_cmd(dev, cmd, input, output);
+       if (ret) {
+               dev_warn(dev, "error %d while listing all LEDs\n", ret);
+               return ret;
+       }
+
+       leds = output[0];
+       if (!leds) {
+               dev_warn(dev, "No LEDs found\n");
+               return -ENODEV;
+       }
+
+       for (id = 0; id < MAX_LEDS; id++) {
+               struct nuc_nmi_led *led = &priv->led[priv->num_leds];
+
+               if (!(leds & BIT(id)))
+                       continue;
+
+               led->id = id;
+
+               cmd = LED_QUERY;
+               input[0] = LED_QUERY_COLOR_TYPE;
+               input[1] = id;
+               ret = nuc_nmi_cmd(dev, cmd, input, output);
+               if (ret) {
+                       dev_warn(dev, "error %d on led %i\n", ret, i);
+                       return ret;
+               }
+
+               led->color_type = output[0]      |
+                                 output[1] << 8 |
+                                 output[2] << 16;
+
+               cmd = LED_NEW_GET_STATUS;
+               input[0] = LED_NEW_GET_CURRENT_INDICATOR;
+               input[1] = i;
+               ret = nuc_nmi_cmd(dev, cmd, input, output);
+               if (ret) {
+                       dev_warn(dev, "error %d on led %i\n", ret, i);
+                       return ret;
+               }
+
+               led->indicator = output[0];
+
+               cmd = LED_QUERY;
+               input[0] = LED_QUERY_INDICATOR_OPTIONS;
+               input[1] = i;
+               ret = nuc_nmi_cmd(dev, cmd, input, output);
+               if (ret) {
+                       dev_warn(dev, "error %d on led %i\n", ret, i);
+                       return ret;
+               }
+
+               led->avail_indicators = output[0]      |
+                                       output[1] << 8 |
+                                       output[2] << 16;
+
+               cmd = LED_QUERY;
+               input[0] = LED_QUERY_CONTROL_ITEMS;
+               input[1] = i;
+               input[2] = led->indicator;
+               ret = nuc_nmi_cmd(dev, cmd, input, output);
+               if (ret) {
+                       dev_warn(dev, "error %d on led %i\n", ret, i);
+                       return ret;
+               }
+
+               led->control_items = output[0]      |
+                                    output[1] << 8 |
+                                    output[2] << 16;
+
+               dev_dbg(dev, "%s: id: %02x, color type: %06x, indicator: %06x, 
control items: %06x\n",
+                       led_names[led->id], led->id,
+                       led->color_type, led->indicator, led->control_items);
+
+               priv->num_leds++;
+       }
+
+       return 0;
+}
+
+/*
+ * LED show/store routines
+ */
+
+#define LED_ATTR_RW(_name) \
+       DEVICE_ATTR(_name, 0644, show_##_name, store_##_name)
+
+static const char * const led_indicators[] = {
+       "Power State",
+       "HDD Activity",
+       "Ethernet",
+       "WiFi",
+       "Software",
+       "Power Limit",
+       "Disable"
+};
+
+static ssize_t show_indicator(struct device *dev,
+                             struct device_attribute *attr,
+                             char *buf)
+{
+       struct led_classdev *cdev = dev_get_drvdata(dev);
+       struct nuc_nmi_led *led = container_of(cdev, struct nuc_nmi_led, cdev);
+       int size = PAGE_SIZE;
+       char *p = buf;
+       int i, n;
+
+       for (i = 0; i < fls(led->avail_indicators); i++) {
+               if (!(led->avail_indicators & BIT(i)))
+                       continue;
+               if (i == led->indicator)
+                       n = scnprintf(p, size, "[%s]  ", led_indicators[i]);
+               else
+                       n = scnprintf(p, size, "%s  ", led_indicators[i]);
+               p += n;
+               size -= n;
+       }
+       size -= scnprintf(p, size, "\n");
+
+       return PAGE_SIZE - size;
+}
+
+static ssize_t store_indicator(struct device *dev,
+                              struct device_attribute *attr,
+                              const char *buf, size_t len)
+{
+       struct led_classdev *cdev = dev_get_drvdata(dev);
+       struct nuc_nmi_led *led = container_of(cdev, struct nuc_nmi_led, cdev);
+       u8 cmd, input[NUM_INPUT_ARGS] = { 0 };
+       const char *tmp;
+       int ret, i;
+
+       tmp = strsep((char **)&buf, "\n");
+
+       for (i = 0; i < fls(led->avail_indicators); i++) {
+               if (!(led->avail_indicators & BIT(i)))
+                       continue;
+
+               if (!strcasecmp(tmp, led_indicators[i])) {
+                       cmd = LED_SET_INDICATOR;
+                       input[0] = led->id;
+                       input[1] = i;
+
+                       dev_dbg(dev, "set led %s indicator to %s\n",
+                               cdev->name, led_indicators[i]);
+
+                       ret = nuc_nmi_cmd(dev, cmd, input, NULL);
+                       if (ret)
+                               return ret;
+
+                       led->indicator = i;
+
+                       return len;
+               }
+       }
+
+       return -EINVAL;
+}
+
+static LED_ATTR_RW(indicator);
+
+/*
+ * Attributes for multicolor LEDs
+ */
+
+static struct attribute *nuc_wmi_multicolor_led_attr[] = {
+       &dev_attr_indicator.attr,
+       NULL,
+};
+
+static const struct attribute_group nuc_wmi_led_attribute_group = {
+       .attrs          = nuc_wmi_multicolor_led_attr,
+};
+
+static const struct attribute_group *nuc_wmi_led_attribute_groups[] = {
+       &nuc_wmi_led_attribute_group,
+       NULL
+};
+
+static int nuc_wmi_led_register(struct device *dev, struct nuc_nmi_led *led)
+{
+       led->cdev.name = led_names[led->id];
+
+       led->dev = dev;
+       led->cdev.groups = nuc_wmi_led_attribute_groups;
+
+       /*
+        * It can't let the classdev to manage the brightness due to several
+        * reasons:
+        *
+        * 1) classdev has some internal logic to manage the brightness,
+        *    at set_brightness_delayed(), which starts disabling the LEDs;
+        *    While this makes sense on most cases, here, it would appear
+        *    that the NUC was powered off, which is not what happens;
+        * 2) classdev unconditionally tries to set brightness for all
+        *    leds, including the ones that were software-disabled or
+        *    disabled disabled via BIOS menu;
+        * 3) There are 3 types of brightness values for each LED, depending
+        *    on the CPU power state: S0, S3 and S5.
+        *
+        * So, the best seems to export everything via sysfs attributes
+        * directly. This would require some further changes at the
+        * LED class, though, or we would need to create our own LED
+        * class, which seems wrong.
+        */
+
+       return devm_led_classdev_register(dev, &led->cdev);
+}
+
+static int nuc_wmi_leds_setup(struct device *dev)
+{
+       struct nuc_wmi *priv = dev_get_drvdata(dev);
+       int ret, i;
+
+       ret = nuc_wmi_query_leds(dev);
+       if (ret)
+               return ret;
+
+       for (i = 0; i < priv->num_leds; i++) {
+               ret = nuc_wmi_led_register(dev, &priv->led[i]);
+               if (ret) {
+                       dev_err(dev, "Failed to register led %d: %s\n",
+                               i, led_names[priv->led[i].id]);
+                       while (--i >= 0)
+                               devm_led_classdev_unregister(dev, 
&priv->led[i].cdev);
+
+                       return ret;
+               }
+       }
+       return 0;
+}
+
+static int nuc_wmi_probe(struct wmi_device *wdev, const void *context)
+{
+       struct device *dev = &wdev->dev;
+       struct nuc_wmi *priv;
+       int ret;
+
+       priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+       mutex_init(&priv->wmi_lock);
+
+       dev_set_drvdata(dev, priv);
+
+       ret = nuc_wmi_leds_setup(dev);
+       if (ret)
+               return ret;
+
+       dev_info(dev, "NUC WMI driver initialized.\n");
+       return 0;
+}
+
+static void nuc_wmi_remove(struct wmi_device *wdev)
+{
+       struct device *dev = &wdev->dev;
+
+       dev_info(dev, "NUC WMI driver removed.\n");
+}
+
+static const struct wmi_device_id nuc_wmi_descriptor_id_table[] = {
+       { .guid_string = NUC_LED_WMI_GUID },
+       { },
+};
+
+static struct wmi_driver nuc_wmi_driver = {
+       .driver = {
+               .name = "nuc-wmi",
+       },
+       .probe = nuc_wmi_probe,
+       .remove = nuc_wmi_remove,
+       .id_table = nuc_wmi_descriptor_id_table,
+};
+
+module_wmi_driver(nuc_wmi_driver);
+
+MODULE_DEVICE_TABLE(wmi, nuc_wmi_descriptor_id_table);
+MODULE_AUTHOR("Mauro Carvalho Chehab <mchehab+hua...@kernel.org>");
+MODULE_DESCRIPTION("Intel NUC WMI driver");
+MODULE_LICENSE("GPL");
-- 
2.31.1

_______________________________________________
devel mailing list
de...@linuxdriverproject.org
http://driverdev.linuxdriverproject.org/mailman/listinfo/driverdev-devel

Reply via email to