lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <cf5b6334-7558-4115-92e4-28b4b531490a@tuxedocomputers.com>
Date: Fri, 5 Sep 2025 16:05:09 +0200
From: Werner Sembach <wse@...edocomputers.com>
To: Armin Wolf <W_Armin@....de>, ilpo.jarvinen@...ux.intel.com,
 hdegoede@...hat.com, chumuzero@...il.com, corbet@....net, cs@...edo.de,
 ggo@...edocomputers.com
Cc: linux-doc@...r.kernel.org, linux-kernel@...r.kernel.org,
 platform-driver-x86@...r.kernel.org, rdunlap@...radead.org,
 alok.a.tiwari@...cle.com, linux-leds@...r.kernel.org, lee@...nel.org,
 pobrn@...tonmail.com
Subject: Re: [PATCH v3 1/2] platform/x86: Add Uniwill laptop driver

Hi,

some more things i noticed, i didn't check the lightbar as i don't have a 
lightbar device at hand and I'm not sure if my devices support the battery 
threshold, but i commented on the rest

Am 31.08.25 um 21:27 schrieb Armin Wolf:
> Add a new driver for Uniwill laptops. The driver uses a ACPI
> interface to talk with the embedded controller, but relies on a
> ACPI WMI interface for receiving event notifications.
>
> The driver is reverse-engineered based on the following information:
> - OEM software from intel
> -https://github.com/pobrn/qc71_laptop
> -https://gitlab.com/tuxedocomputers/development/packages/tuxedo-drivers
> -https://github.com/tuxedocomputers/tuxedo-control-center
>
> The underlying EC supports various features, including hwmon sensors,
> battery charge limiting, a RGB lightbar and keyboard-related controls.
>
> Reported-by: cyear<chumuzero@...il.com>
> Closes:https://github.com/lm-sensors/lm-sensors/issues/508
> Closes:https://github.com/Wer-Wolf/uniwill-laptop/issues/3
> Signed-off-by: Armin Wolf<W_Armin@....de>
> ---
>   .../ABI/testing/sysfs-driver-uniwill-laptop   |   53 +
>   Documentation/wmi/devices/uniwill-laptop.rst  |  198 +++
>   MAINTAINERS                                   |   10 +
>   drivers/platform/x86/Kconfig                  |    2 +
>   drivers/platform/x86/Makefile                 |    3 +
>   drivers/platform/x86/uniwill/Kconfig          |   38 +
>   drivers/platform/x86/uniwill/Makefile         |    8 +
>   drivers/platform/x86/uniwill/uniwill-acpi.c   | 1537 +++++++++++++++++
>   drivers/platform/x86/uniwill/uniwill-wmi.c    |   92 +
>   drivers/platform/x86/uniwill/uniwill-wmi.h    |  127 ++
>   10 files changed, 2068 insertions(+)
>   create mode 100644 Documentation/ABI/testing/sysfs-driver-uniwill-laptop
>   create mode 100644 Documentation/wmi/devices/uniwill-laptop.rst
>   create mode 100644 drivers/platform/x86/uniwill/Kconfig
>   create mode 100644 drivers/platform/x86/uniwill/Makefile
>   create mode 100644 drivers/platform/x86/uniwill/uniwill-acpi.c
>   create mode 100644 drivers/platform/x86/uniwill/uniwill-wmi.c
>   create mode 100644 drivers/platform/x86/uniwill/uniwill-wmi.h
>
> diff --git a/Documentation/ABI/testing/sysfs-driver-uniwill-laptop b/Documentation/ABI/testing/sysfs-driver-uniwill-laptop
> new file mode 100644
> index 000000000000..b8d55da113f8
> --- /dev/null
> +++ b/Documentation/ABI/testing/sysfs-driver-uniwill-laptop
> @@ -0,0 +1,53 @@
> +What:		/sys/bus/platform/devices/INOU0000:XX/fn_lock
> +Date:		August 2025
> +KernelVersion:	6.18
> +Contact:	Armin Wolf<W_Armin@....de>
> +Description:
> +		Allows userspace applications to enable/disable the FN lock feature
> +		of the integrated keyboard by writing "enable"/"disable" into this file.
> +
> +		Reading this file returns the current enable status of the FN lock functionality.
> +
> +What:		/sys/bus/platform/devices/INOU0000:XX/super_key_lock
> +Date:		August 2025
> +KernelVersion:	6.18
> +Contact:	Armin Wolf<W_Armin@....de>
> +Description:
> +                Allows userspace applications to enable/disable the super key functionality
> +                of the integrated keyboard by writing "enable"/"disable" into this file.
> +
> +		Reading this file returns the current enable status of the super key functionality.
> +
> +What:		/sys/bus/platform/devices/INOU0000:XX/touchpad_toggle
> +Date:		August 2025
> +KernelVersion:	6.18
> +Contact:	Armin Wolf<W_Armin@....de>
> +Description:
> +		Allows userspace applications to enable/disable the touchpad toggle functionality
> +		of the integrated touchpad by writing "enable"/"disable" into this file.
> +
> +		Reading this file returns the current enable status of the touchpad toggle
> +		functionality.
> +
> +What:		/sys/bus/platform/devices/INOU0000:XX/rainbow_animation
> +Date:		August 2025
> +KernelVersion:	6.18
> +Contact:	Armin Wolf<W_Armin@....de>
> +Description:
> +		Forces the integrated lightbar to display a rainbow animation when the machine
> +		is not suspended. Writing "enable"/"disable" into this file enables/disables
> +		this functionality.
> +
> +		Reading this file returns the current status of the rainbow animation functionality.
> +
> +What:		/sys/bus/platform/devices/INOU0000:XX/breathing_in_suspend
> +Date:		August 2025
> +KernelVersion:	6.18
> +Contact:	Armin Wolf<W_Armin@....de>
> +Description:
> +		Causes the integrated lightbar to display a breathing animation when the machine
> +		has been suspended and is running on AC power. Writing "enable"/"disable" into
> +		this file enables/disables this functionality.
> +
> +		Reading this file returns the current status of the breathing animation
> +		functionality.
> diff --git a/Documentation/wmi/devices/uniwill-laptop.rst b/Documentation/wmi/devices/uniwill-laptop.rst
> new file mode 100644
> index 000000000000..e246bf293450
> --- /dev/null
> +++ b/Documentation/wmi/devices/uniwill-laptop.rst
> @@ -0,0 +1,198 @@
> +.. SPDX-License-Identifier: GPL-2.0-or-later
> +
> +========================================
> +Uniwill Notebook driver (uniwill-laptop)
> +========================================
> +
> +Introduction
> +============
> +
> +Many notebooks manufactured by Uniwill (either directly or as ODM) provide a EC interface
> +for controlling various platform settings like sensors and fan control. This interface is
> +used by the ``uniwill-laptop`` driver to map those features onto standard kernel interfaces.
> +
> +EC WMI interface description
> +============================
> +
> +The EC WMI interface description can be decoded from the embedded binary MOF (bmof)
> +data using the `bmfdec<https://github.com/pali/bmfdec>`_ utility:
> +
> +::
> +
> +  [WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"),
> +   Description("Class used to operate methods on a ULong"),
> +   guid("{ABBC0F6F-8EA1-11d1-00A0-C90629100000}")]
> +  class AcpiTest_MULong {
> +    [key, read] string InstanceName;
> +    [read] boolean Active;
> +
> +    [WmiMethodId(1), Implemented, read, write, Description("Return the contents of a ULong")]
> +    void GetULong([out, Description("Ulong Data")] uint32 Data);
> +
> +    [WmiMethodId(2), Implemented, read, write, Description("Set the contents of a ULong")]
> +    void SetULong([in, Description("Ulong Data")] uint32 Data);
> +
> +    [WmiMethodId(3), Implemented, read, write,
> +     Description("Generate an event containing ULong data")]
> +    void FireULong([in, Description("WMI requires a parameter")] uint32 Hack);
> +
> +    [WmiMethodId(4), Implemented, read, write, Description("Get and Set the contents of a ULong")]
> +    void GetSetULong([in, Description("Ulong Data")] uint64 Data,
> +                     [out, Description("Ulong Data")] uint32 Return);
> +
> +    [WmiMethodId(5), Implemented, read, write,
> +     Description("Get and Set the contents of a ULong for Dollby button")]
> +    void GetButton([in, Description("Ulong Data")] uint64 Data,
> +                   [out, Description("Ulong Data")] uint32 Return);
> +  };
> +
> +Most of the WMI-related code was copied from the Windows driver samples, which unfortunately means
> +that the WMI-GUID is not unique. This makes the WMI-GUID unusable for autoloading.
> +
> +WMI method GetULong()
> +---------------------
> +
> +This WMI method was copied from the Windows driver samples and has no function.
> +
> +WMI method SetULong()
> +---------------------
> +
> +This WMI method was copied from the Windows driver samples and has no function.
> +
> +WMI method FireULong()
> +----------------------
> +
> +This WMI method allows to inject a WMI event with a 32-bit payload. Its primary purpose seems
> +to be debugging.
> +
> +WMI method GetSetULong()
> +------------------------
> +
> +This WMI method is used to communicate with the EC. The ``Data`` argument holds the following
> +information (starting with the least significant byte):
> +
> +1. 16-bit address
> +2. 16-bit data (set to ``0x0000`` when reading)
> +3. 16-bit operation (``0x0100`` for reading and ``0x0000`` for writing)
> +4. 16-bit reserved (set to ``0x0000``)
> +
> +The first 8 bits of the ``Return`` value contain the data returned by the EC when reading.
> +The special value ``0xFEFEFEFE`` is used to indicate a communication failure with the EC.
> +
> +WMI method GetButton()
> +----------------------
> +
> +This WMI method is not implemented on all machines and has an unknown purpose.
> +
> +Reverse-Engineering the EC WMI interface
> +========================================
> +
> +.. warning:: Randomly poking the EC can potentially cause damage to the machine and other unwanted
> +             side effects, please be careful.
> +
> +The EC behind the ``GetSetULong`` method is used by the OEM software supplied by the manufacturer.
> +Reverse-engineering of this software is difficult since it uses an obfuscator, however some parts
> +are not obfuscated. In this case `dnSpy<https://github.com/dnSpy/dnSpy>`_ could also be helpful.
> +
> +The EC can be accessed under Windows using powershell (requires admin privileges):
> +
> +::
> +
> +  > $obj = Get-CimInstance -Namespace root/wmi -ClassName AcpiTest_MULong | Select-Object -First 1
> +  > Invoke-CimMethod -InputObject $obj -MethodName GetSetULong -Arguments @{Data = <input>}
> +
> +WMI event interface description
> +===============================
> +
> +The WMI interface description can also be decoded from the embedded binary MOF (bmof)
> +data:
> +
> +::
> +
> +  [WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"),
> +   Description("Class containing event generated ULong data"),
> +   guid("{ABBC0F72-8EA1-11d1-00A0-C90629100000}")]
> +  class AcpiTest_EventULong : WmiEvent {
> +    [key, read] string InstanceName;
> +    [read] boolean Active;
> +
> +    [WmiDataId(1), read, write, Description("ULong Data")] uint32 ULong;
> +  };
> +
> +Most of the WMI-related code was again copied from the Windows driver samples, causing this WMI
> +interface to suffer from the same restrictions as the EC WMI interface described above.
> +
> +WMI event data
> +--------------
> +
> +The WMI event data contains a single 32-bit value which is used to indicate various platform events.
> +
> +Reverse-Engineering the Uniwill WMI event interface
> +===================================================
> +
> +The driver logs debug messages when receiving a WMI event. Thus enabling debug messages will be
> +useful for finding unknown event codes.
> +
> +EC ACPI interface description
> +=============================
> +
> +The ``INOU0000`` ACPI device is a virtual device used to access various hardware registers
> +available on notebooks manufactured by Uniwill. Reading and writing those registers happens
> +by calling ACPI control methods. The ``uniwill-laptop`` driver uses this device to communicate
> +with the EC because the ACPI control methods are faster than the WMI methods described above.
> +
> +ACPI control methods used for reading registers take a single ACPI integer containing the address
> +of the register to read and return a ACPI integer containing the data inside said register. ACPI
> +control methods used for writing registers however take two ACPI integers, with the additional
> +ACPI integer containing the data to be written into the register. Such ACPI control methods return
> +nothing.
> +
> +System memory
> +-------------
> +
> +System memory can be accessed with a granularity of either a single byte (``MMRB`` for reading and
> +``MMWB`` for writing) or four bytes (``MMRD`` for reading and ``MMWD`` for writing). Those ACPI
> +control methods are unused because they provide no benefit when compared to the native memory
> +access functions provided by the kernel.
> +
> +EC RAM
> +------
> +
> +The internal RAM of the EC can be accessed with a granularity of a single byte using the ``ECRR``
> +(read) and ``ECRW`` (write) ACPI control methods, with the maximum register address being ``0xFFF``.
> +The OEM software waits 6 ms after calling one of those ACPI control methods, likely to avoid
> +overwhelming the EC when being connected over LPC.
> +
> +PCI config space
> +----------------
> +
> +The PCI config space can be accessed with a granularity of four bytes using the ``PCRD`` (read) and
> +``PCWD`` (write) ACPI control methods. The exact address format is unknown, and poking random PCI
> +devices might confuse the PCI subsystem. Because of this those ACPI control methods are not used.
> +
> +IO ports
> +--------
> +
> +IO ports can be accessed with a granularity of four bytes using the ``IORD`` (read) and ``IOWD``
> +(write) ACPI control methods. Those ACPI control methods are unused because they provide no benefit
> +when compared to the native IO port access functions provided by the kernel.
> +
> +CMOS RAM
> +--------
> +
> +The CMOS RAM can be accessed with a granularity of a single byte using the ``RCMS`` (read) and
> +``WCMS`` ACPI control methods. Using those ACPI methods might interfere with the native CMOS RAM
> +access functions provided by the kernel due to the usage of indexed IO, so they are unused.
> +
> +Indexed IO
> +----------
> +
> +Indexed IO with IO ports with a granularity of a single byte can be performed using the ``RIOP``
> +(read) and ``WIOP`` (write) ACPI control methods. Those ACPI methods are unused because they
> +provide no benifit when compared to the native IO port access functions provided by the kernel.
> +
> +Special thanks go to github user `pobrn` which developed the
> +`qc71_laptop<https://github.com/pobrn/qc71_laptop>`_ driver on which this driver is partly based.
> +The same is true for Tuxedo Computers, which developed the
> +`tuxedo-drivers<https://gitlab.com/tuxedocomputers/development/packages/tuxedo-drivers>`_ package
> +which also served as a foundation for this driver.
> diff --git a/MAINTAINERS b/MAINTAINERS
> index fe168477caa4..99624393f271 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -25903,6 +25903,16 @@ L:	linux-scsi@...r.kernel.org
>   S:	Maintained
>   F:	drivers/ufs/host/ufs-renesas.c
>   
> +UNIWILL LAPTOP DRIVER
> +M:	Armin Wolf<W_Armin@....de>
> +L:	platform-driver-x86@...r.kernel.org
> +S:	Maintained
> +F:	Documentation/ABI/testing/sysfs-driver-uniwill-laptop
> +F:	Documentation/wmi/devices/uniwill-laptop.rst
> +F:	drivers/platform/x86/uniwill/uniwill-acpi.c
> +F:	drivers/platform/x86/uniwill/uniwill-wmi.c
> +F:	drivers/platform/x86/uniwill/uniwill-wmi.h
> +
>   UNSORTED BLOCK IMAGES (UBI)
>   M:	Richard Weinberger<richard@....at>
>   R:	Zhihao Cheng<chengzhihao1@...wei.com>
> diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
> index 6d238e120dce..a4c2ea69970d 100644
> --- a/drivers/platform/x86/Kconfig
> +++ b/drivers/platform/x86/Kconfig
> @@ -74,6 +74,8 @@ config HUAWEI_WMI
>   	  To compile this driver as a module, choose M here: the module
>   	  will be called huawei-wmi.
>   
> +source "drivers/platform/x86/uniwill/Kconfig"
> +
>   config UV_SYSFS
>   	tristate "Sysfs structure for UV systems"
>   	depends on X86_UV
> diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile
> index a0c5848513e3..a6271588e05a 100644
> --- a/drivers/platform/x86/Makefile
> +++ b/drivers/platform/x86/Makefile
> @@ -109,6 +109,9 @@ obj-$(CONFIG_TOSHIBA_WMI)	+= toshiba-wmi.o
>   # before toshiba_acpi initializes
>   obj-$(CONFIG_ACPI_TOSHIBA)	+= toshiba_acpi.o
>   
> +# Uniwill
> +obj-y				+= uniwill/
> +
>   # Inspur
>   obj-$(CONFIG_INSPUR_PLATFORM_PROFILE)	+= inspur_platform_profile.o
>   
> diff --git a/drivers/platform/x86/uniwill/Kconfig b/drivers/platform/x86/uniwill/Kconfig
> new file mode 100644
> index 000000000000..d07cc8440188
> --- /dev/null
> +++ b/drivers/platform/x86/uniwill/Kconfig
> @@ -0,0 +1,38 @@
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +#
> +# Uniwill X86 Platform Specific Drivers
> +#
> +
> +menuconfig X86_PLATFORM_DRIVERS_UNIWILL
> +	bool "Uniwill X86 Platform Specific Device Drivers"
> +	depends on X86_PLATFORM_DEVICES
> +	help
> +	  Say Y here to see options for device drivers for various
> +	  Uniwill x86 platforms, including many OEM laptops originally
> +	  manufactured by Uniwill.
> +	  This option alone does not add any kernel code.
> +
> +	  If you say N, all options in this submenu will be skipped and disabled.
> +
> +if X86_PLATFORM_DRIVERS_UNIWILL
> +
> +config UNIWILL_LAPTOP
> +	tristate "Uniwill Laptop Extras"
> +	default m
> +	depends on ACPI
> +	depends on ACPI_WMI
> +	depends on ACPI_BATTERY
> +	depends on HWMON
> +	depends on INPUT
> +	depends on LEDS_CLASS_MULTICOLOR
> +	depends on DMI
> +	select REGMAP
> +	select INPUT_SPARSEKMAP
> +	help
> +	  This driver adds support for various extra features found on Uniwill laptops,
> +	  like the lightbar, hwmon sensors and hotkeys. It also supports many OEM laptops
> +	  originally manufactured by Uniwill.
> +
> +	  If you have such a laptop, say Y or M here.
> +
> +endif
> diff --git a/drivers/platform/x86/uniwill/Makefile b/drivers/platform/x86/uniwill/Makefile
> new file mode 100644
> index 000000000000..05cd1747a240
> --- /dev/null
> +++ b/drivers/platform/x86/uniwill/Makefile
> @@ -0,0 +1,8 @@
> +# SPDX-License-Identifier: GPL-2.0-or-later
> +#
> +# Makefile for linux/drivers/platform/x86/uniwill
> +# Uniwill X86 Platform Specific Drivers
> +#
> +
> +obj-$(CONFIG_UNIWILL_LAPTOP)	+= uniwill-laptop.o
> +uniwill-laptop-y		:= uniwill-acpi.o uniwill-wmi.o
> diff --git a/drivers/platform/x86/uniwill/uniwill-acpi.c b/drivers/platform/x86/uniwill/uniwill-acpi.c
> new file mode 100644
> index 000000000000..3467677cc99f
> --- /dev/null
> +++ b/drivers/platform/x86/uniwill/uniwill-acpi.c
> @@ -0,0 +1,1537 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Linux driver for Uniwill notebooks.
> + *
> + * Special thanks go to Pőcze Barnabás, Christoffer Sandberg and Werner Sembach
> + * for supporting the development of this driver either through prior work or
> + * by answering questions regarding the underlying ACPI and WMI interfaces.
> + *
> + * Copyright (C) 2025 Armin Wolf<W_Armin@....de>
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/acpi.h>
> +#include <linux/bits.h>
> +#include <linux/bitfield.h>
> +#include <linux/cleanup.h>
> +#include <linux/debugfs.h>
> +#include <linux/delay.h>
> +#include <linux/device.h>
> +#include <linux/device/driver.h>
> +#include <linux/dmi.h>
> +#include <linux/errno.h>
> +#include <linux/fixp-arith.h>
> +#include <linux/hwmon.h>
> +#include <linux/hwmon-sysfs.h>
> +#include <linux/init.h>
> +#include <linux/input.h>
> +#include <linux/input/sparse-keymap.h>
> +#include <linux/kernel.h>
> +#include <linux/kstrtox.h>
> +#include <linux/leds.h>
> +#include <linux/led-class-multicolor.h>
> +#include <linux/limits.h>
> +#include <linux/list.h>
> +#include <linux/minmax.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/notifier.h>
> +#include <linux/platform_device.h>
> +#include <linux/pm.h>
> +#include <linux/printk.h>
> +#include <linux/regmap.h>
> +#include <linux/string.h>
> +#include <linux/string_choices.h>
> +#include <linux/sysfs.h>
> +#include <linux/types.h>
> +#include <linux/units.h>
> +
> +#include <acpi/battery.h>
> +
> +#include "uniwill-wmi.h"
> +
> +#define EC_ADDR_BAT_POWER_UNIT_1	0x0400
> +
> +#define EC_ADDR_BAT_POWER_UNIT_2	0x0401
> +
> +#define EC_ADDR_BAT_DESIGN_CAPACITY_1	0x0402
> +
> +#define EC_ADDR_BAT_DESIGN_CAPACITY_2	0x0403
> +
> +#define EC_ADDR_BAT_FULL_CAPACITY_1	0x0404
> +
> +#define EC_ADDR_BAT_FULL_CAPACITY_2	0x0405
> +
> +#define EC_ADDR_BAT_DESIGN_VOLTAGE_1	0x0408
> +
> +#define EC_ADDR_BAT_DESIGN_VOLTAGE_2	0x0409
> +
> +#define EC_ADDR_BAT_STATUS_1		0x0432
> +#define BAT_DISCHARGING			BIT(0)
> +
> +#define EC_ADDR_BAT_STATUS_2		0x0433
> +
> +#define EC_ADDR_BAT_CURRENT_1		0x0434
> +
> +#define EC_ADDR_BAT_CURRENT_2		0x0435
> +
> +#define EC_ADDR_BAT_REMAIN_CAPACITY_1	0x0436
> +
> +#define EC_ADDR_BAT_REMAIN_CAPACITY_2	0x0437
> +
> +#define EC_ADDR_BAT_VOLTAGE_1		0x0438
> +
> +#define EC_ADDR_BAT_VOLTAGE_2		0x0439
> +
> +#define EC_ADDR_CPU_TEMP		0x043E
> +
> +#define EC_ADDR_GPU_TEMP		0x044F
> +
> +#define EC_ADDR_MAIN_FAN_RPM_1		0x0464
> +
> +#define EC_ADDR_MAIN_FAN_RPM_2		0x0465
> +
> +#define EC_ADDR_SECOND_FAN_RPM_1	0x046C
> +
> +#define EC_ADDR_SECOND_FAN_RPM_2	0x046D
> +
> +#define EC_ADDR_DEVICE_STATUS		0x047B
> +#define WIFI_STATUS_ON			BIT(7)
> +/* BIT(5) is also unset depending on the rfkill state (bluetooth?) */
> +
> +#define EC_ADDR_BAT_ALERT		0x0494
> +
> +#define EC_ADDR_BAT_CYCLE_COUNT_1	0x04A6
> +
> +#define EC_ADDR_BAT_CYCLE_COUNT_2	0x04A7
> +
> +#define EC_ADDR_PROJECT_ID		0x0740
> +
> +#define EC_ADDR_AP_OEM			0x0741
> +#define	ENABLE_MANUAL_CTRL		BIT(0)
> +#define ITE_KBD_EFFECT_REACTIVE		BIT(3)
> +#define FAN_ABNORMAL			BIT(5)
> +
> +#define EC_ADDR_SUPPORT_5		0x0742
> +#define FAN_TURBO_SUPPORTED		BIT(4)
> +#define FAN_SUPPORT			BIT(5)
> +
> +#define EC_ADDR_CTGP_DB_CTRL		0x0743
> +#define CTGP_DB_GENERAL_ENABLE		BIT(0)
> +#define CTGP_DB_DB_ENABLE		BIT(1)
> +#define CTGP_DB_CTGP_ENABLE		BIT(2)
> +
> +#define EC_ADDR_CTGP_OFFSET		0x0744
> +
> +#define EC_ADDR_TPP_OFFSET		0x0745
> +
> +#define EC_ADDR_MAX_TGP			0x0746
> +
> +#define EC_ADDR_LIGHTBAR_AC_CTRL	0x0748
> +#define LIGHTBAR_APP_EXISTS		BIT(0)
> +#define LIGHTBAR_POWER_SAVE		BIT(1)
> +#define LIGHTBAR_S0_OFF			BIT(2)
> +#define LIGHTBAR_S3_OFF			BIT(3)	// Breathing animation when suspended
> +#define LIGHTBAR_WELCOME		BIT(7)	// Rainbow animation
> +
> +#define EC_ADDR_LIGHTBAR_AC_RED		0x0749
> +
> +#define EC_ADDR_LIGHTBAR_AC_GREEN	0x074A
> +
> +#define EC_ADDR_LIGHTBAR_AC_BLUE	0x074B
> +
> +#define EC_ADDR_BIOS_OEM		0x074E
> +#define FN_LOCK_STATUS			BIT(4)
> +
> +#define EC_ADDR_MANUAL_FAN_CTRL		0x0751
> +#define FAN_LEVEL_MASK			GENMASK(2, 0)
> +#define FAN_MODE_TURBO			BIT(4)
> +#define FAN_MODE_HIGH			BIT(5)
> +#define FAN_MODE_BOOST			BIT(6)
> +#define FAN_MODE_USER			BIT(7)
> +
> +#define EC_ADDR_PWM_1			0x075B
> +
> +#define EC_ADDR_PWM_2			0x075C
> +
> +/* Unreliable */
> +#define EC_ADDR_SUPPORT_1		0x0765
> +#define AIRPLANE_MODE			BIT(0)
> +#define GPS_SWITCH			BIT(1)
> +#define OVERCLOCK			BIT(2)
> +#define MACRO_KEY			BIT(3)
> +#define SHORTCUT_KEY			BIT(4)
> +#define SUPER_KEY_LOCK			BIT(5)
> +#define LIGHTBAR			BIT(6)
> +#define FAN_BOOST			BIT(7)
> +
> +#define EC_ADDR_SUPPORT_2		0x0766
> +#define SILENT_MODE			BIT(0)
> +#define USB_CHARGING			BIT(1)
> +#define RGB_KEYBOARD			BIT(2)
> +#define CHINA_MODE			BIT(5)
> +#define MY_BATTERY			BIT(6)
> +
> +#define EC_ADDR_TRIGGER			0x0767
> +#define TRIGGER_SUPER_KEY_LOCK		BIT(0)
> +#define TRIGGER_LIGHTBAR		BIT(1)
> +#define TRIGGER_FAN_BOOST		BIT(2)
> +#define TRIGGER_SILENT_MODE		BIT(3)
> +#define TRIGGER_USB_CHARGING		BIT(4)
> +#define RGB_APPLY_COLOR			BIT(5)
> +#define RGB_LOGO_EFFECT			BIT(6)
> +#define RGB_RAINBOW_EFFECT		BIT(7)
> +
> +#define EC_ADDR_SWITCH_STATUS		0x0768
> +#define SUPER_KEY_LOCK_STATUS		BIT(0)
> +#define LIGHTBAR_STATUS			BIT(1)
> +#define FAN_BOOST_STATUS		BIT(2)
> +#define MACRO_KEY_STATUS		BIT(3)
> +#define MY_BAT_POWER_BAT_STATUS		BIT(4)
> +
> +#define EC_ADDR_RGB_RED			0x0769
> +
> +#define EC_ADDR_RGB_GREEN		0x076A
> +
> +#define EC_ADDR_RGB_BLUE		0x076B
> +
> +#define EC_ADDR_ROMID_START		0x0770
> +#define ROMID_LENGTH			14
> +
> +#define EC_ADDR_ROMID_EXTRA_1		0x077E
> +
> +#define EC_ADDR_ROMID_EXTRA_2		0x077F
> +
> +#define EC_ADDR_BIOS_OEM_2		0x0782
> +#define FAN_V2_NEW			BIT(0)
> +#define FAN_QKEY			BIT(1)
> +#define FAN_TABLE_OFFICE_MODE		BIT(2)
> +#define FAN_V3				BIT(3)
> +#define DEFAULT_MODE			BIT(4)
> +
> +#define EC_ADDR_PL1_SETTING		0x0783
> +
> +#define EC_ADDR_PL2_SETTING		0x0784
> +
> +#define EC_ADDR_PL4_SETTING		0x0785
> +
> +#define EC_ADDR_FAN_DEFAULT		0x0786
> +#define FAN_CURVE_LENGTH		5
> +
> +#define EC_ADDR_KBD_STATUS		0x078C
> +#define KBD_WHITE_ONLY			BIT(0)	// ~single color
> +#define KBD_SINGLE_COLOR_OFF		BIT(1)
> +#define KBD_TURBO_LEVEL_MASK		GENMASK(3, 2)
> +#define KBD_APPLY			BIT(4)
> +#define KBD_BRIGHTNESS			GENMASK(7, 5)
> +
> +#define EC_ADDR_FAN_CTRL		0x078E
> +#define FAN3P5				BIT(1)
> +#define CHARGING_PROFILE		BIT(3)
> +#define UNIVERSAL_FAN_CTRL		BIT(6)
> +
> +#define EC_ADDR_BIOS_OEM_3		0x07A3
> +#define FAN_REDUCED_DURY_CYCLE		BIT(5)
> +#define FAN_ALWAYS_ON			BIT(6)
> +
> +#define EC_ADDR_BIOS_BYTE		0x07A4
> +#define FN_LOCK_SWITCH			BIT(3)
> +
> +#define EC_ADDR_OEM_3			0x07A5
> +#define POWER_LED_MASK			GENMASK(1, 0)
> +#define POWER_LED_LEFT			0x00
> +#define POWER_LED_BOTH			0x01
> +#define POWER_LED_NONE			0x02
> +#define FAN_QUIET			BIT(2)
> +#define OVERBOOST			BIT(4)
> +#define HIGH_POWER			BIT(7)
> +
> +#define EC_ADDR_OEM_4			0x07A6
> +#define OVERBOOST_DYN_TEMP_OFF		BIT(1)
> +#define TOUCHPAD_TOGGLE_OFF		BIT(6)
> +
> +#define EC_ADDR_CHARGE_CTRL		0x07B9
> +#define CHARGE_CTRL_MASK		GENMASK(6, 0)
> +#define CHARGE_CTRL_REACHED		BIT(7)
> +
> +#define EC_ADDR_UNIVERSAL_FAN_CTRL	0x07C5
> +#define SPLIT_TABLES			BIT(7)
> +
> +#define EC_ADDR_AP_OEM_6		0x07C6
> +#define ENABLE_UNIVERSAL_FAN_CTRL	BIT(2)
> +#define BATTERY_CHARGE_FULL_OVER_24H	BIT(3)
> +#define BATTERY_ERM_STATUS_REACHED	BIT(4)
> +
> +#define EC_ADDR_CHARGE_PRIO		0x07CC
> +#define CHARGING_PERFORMANCE		BIT(7)
> +
> +/* Same bits as EC_ADDR_LIGHTBAR_AC_CTRL except LIGHTBAR_S3_OFF */
> +#define EC_ADDR_LIGHTBAR_BAT_CTRL	0x07E2
> +
> +#define EC_ADDR_LIGHTBAR_BAT_RED	0x07E3
> +
> +#define EC_ADDR_LIGHTBAR_BAT_GREEN	0x07E4
> +
> +#define EC_ADDR_LIGHTBAR_BAT_BLUE	0x07E5
> +
> +#define EC_ADDR_CPU_TEMP_END_TABLE	0x0F00
> +
> +#define EC_ADDR_CPU_TEMP_START_TABLE	0x0F10
> +
> +#define EC_ADDR_CPU_FAN_SPEED_TABLE	0x0F20
> +
> +#define EC_ADDR_GPU_TEMP_END_TABLE	0x0F30
> +
> +#define EC_ADDR_GPU_TEMP_START_TABLE	0x0F40
> +
> +#define EC_ADDR_GPU_FAN_SPEED_TABLE	0x0F50
> +
> +/*
> + * Those two registers technically allow for manual fan control,
> + * but are unstable on some models and are likely not meant to
> + * be used by applications as they are only accessible when using
> + * the WMI interface.
> + */
> +#define EC_ADDR_PWM_1_WRITEABLE		0x1804
> +
> +#define EC_ADDR_PWM_2_WRITEABLE		0x1809
> +
> +#define DRIVER_NAME	"uniwill"
> +
> +/*
> + * The OEM software always sleeps up to 6 ms after reading/writing EC
> + * registers, so we emulate this behaviour for maximum compatibility.
> + */
> +#define UNIWILL_EC_DELAY_US	6000
> +
> +#define PWM_MAX			200
> +#define FAN_TABLE_LENGTH	16
> +
> +#define LED_CHANNELS		3
> +#define LED_MAX_BRIGHTNESS	200
> +
> +#define UNIWILL_FEATURE_FN_LOCK		BIT(0)
> +#define UNIWILL_FEATURE_SUPER_KEY_LOCK	BIT(1)
> +#define UNIWILL_FEATURE_TOUCHPAD_TOGGLE BIT(2)
> +#define UNIWILL_FEATURE_LIGHTBAR	BIT(3)
> +#define UNIWILL_FEATURE_BATTERY		BIT(4)
> +#define UNIWILL_FEATURE_HWMON		BIT(5)
> +
> +struct uniwill_data {
> +	struct device *dev;
> +	acpi_handle handle;
> +	struct regmap *regmap;
> +	struct acpi_battery_hook hook;
> +	unsigned int last_charge_ctrl;
> +	struct mutex battery_lock;	/* Protects the list of currently registered batteries */
> +	unsigned int last_switch_status;
> +	struct mutex super_key_lock;	/* Protects the toggling of the super key lock state */
> +	struct list_head batteries;
> +	struct led_classdev_mc led_mc_cdev;
> +	struct mc_subled led_mc_subled_info[LED_CHANNELS];
> +	struct mutex input_lock;	/* Protects input sequence during notify */
> +	struct input_dev *input_device;
> +	struct notifier_block nb;
> +};
> +
> +struct uniwill_battery_entry {
> +	struct list_head head;
> +	struct power_supply *battery;
> +};
> +
> +static bool force;
> +module_param_unsafe(force, bool, 0);
> +MODULE_PARM_DESC(force, "Force loading without checking for supported devices\n");
> +
> +/* Feature bitmask since the associated registers are not reliable */
> +static unsigned int supported_features;
> +
> +/*
> + * "disable" is placed on index 0 so that the return value of sysfs_match_string()
> + * directly translates into a boolean value.
> + */
> +static const char * const uniwill_enable_disable_strings[] = {
> +	[0] = "disable",
> +	[1] = "enable",
> +};
> +
> +static const char * const uniwill_temp_labels[] = {
> +	"CPU",
> +	"GPU",
> +};
> +
> +static const char * const uniwill_fan_labels[] = {
> +	"Main",
> +	"Secondary",
> +};
> +
> +static const struct key_entry uniwill_keymap[] = {
> +	/* Reported via keyboard controller */
> +	{ KE_IGNORE,    UNIWILL_OSD_CAPSLOCK,                   { KEY_CAPSLOCK }},
> +	{ KE_IGNORE,    UNIWILL_OSD_NUMLOCK,                    { KEY_NUMLOCK }},
> +
> +	/* Reported when the user locks/unlocks the super key */
> +	{ KE_IGNORE,    UNIWILL_OSD_SUPER_KEY_LOCK_ENABLE,      { KEY_UNKNOWN }},
> +	{ KE_IGNORE,    UNIWILL_OSD_SUPER_KEY_LOCK_DISABLE,     { KEY_UNKNOWN }},
> +
> +	/* Reported in manual mode when toggling the airplane mode status */
> +	{ KE_KEY,       UNIWILL_OSD_RFKILL,                     { KEY_RFKILL }},
> +
> +	/* Reported when user wants to cycle the platform profile */
> +	{ KE_IGNORE,    UNIWILL_OSD_PERFORMANCE_MODE_TOGGLE,    { KEY_UNKNOWN }},
> +
> +	/* Reported when the user wants to adjust the brightness of the keyboard */
> +	{ KE_KEY,       UNIWILL_OSD_KBDILLUMDOWN,               { KEY_KBDILLUMDOWN }},
> +	{ KE_KEY,       UNIWILL_OSD_KBDILLUMUP,                 { KEY_KBDILLUMUP }},
> +
> +	/* Reported when the user wants to toggle the microphone mute status */
> +	{ KE_KEY,       UNIWILL_OSD_MIC_MUTE,                   { KEY_MICMUTE }},
> +
> +	/* Reported when the user locks/unlocks the Fn key */
> +	{ KE_IGNORE,    UNIWILL_OSD_FN_LOCK,                    { KEY_FN_ESC }},
> +
> +	/* Reported when the user wants to toggle the brightness of the keyboard */
> +	{ KE_KEY,       UNIWILL_OSD_KBDILLUMTOGGLE,             { KEY_KBDILLUMTOGGLE }},
> +
> +	/* FIXME: find out the exact meaning of those events */
> +	{ KE_IGNORE,    UNIWILL_OSD_BAT_CHARGE_FULL_24_H,       { KEY_UNKNOWN }},
> +	{ KE_IGNORE,    UNIWILL_OSD_BAT_ERM_UPDATE,             { KEY_UNKNOWN }},
> +
> +	/* Reported when the user wants to toggle the benchmark mode status */
> +	{ KE_IGNORE,    UNIWILL_OSD_BENCHMARK_MODE_TOGGLE,      { KEY_UNKNOWN }},
> +
> +	{ KE_END }
> +};
> +
> +static int uniwill_ec_reg_write(void *context, unsigned int reg, unsigned int val)
> +{
> +	union acpi_object params[2] = {
> +		{
> +			.integer = {
> +				.type = ACPI_TYPE_INTEGER,
> +				.value = reg,
> +			},
> +		},
> +		{
> +			.integer = {
> +				.type = ACPI_TYPE_INTEGER,
> +				.value = val,
> +			},
> +		},
> +	};
> +	struct uniwill_data *data = context;
> +	struct acpi_object_list input = {
> +		.count = ARRAY_SIZE(params),
> +		.pointer = params,
> +	};
> +	acpi_status status;
> +
> +	status = acpi_evaluate_object(data->handle, "ECRW", &input, NULL);
> +	if (ACPI_FAILURE(status))
> +		return -EIO;
> +
> +	usleep_range(UNIWILL_EC_DELAY_US, UNIWILL_EC_DELAY_US * 2);
> +
> +	return 0;
> +}
> +
> +static int uniwill_ec_reg_read(void *context, unsigned int reg, unsigned int *val)
> +{
> +	union acpi_object params[1] = {
> +		{
> +			.integer = {
> +				.type = ACPI_TYPE_INTEGER,
> +				.value = reg,
> +			},
> +		},
> +	};
> +	struct uniwill_data *data = context;
> +	struct acpi_object_list input = {
> +		.count = ARRAY_SIZE(params),
> +		.pointer = params,
> +	};
> +	unsigned long long output;
> +	acpi_status status;
> +
> +	status = acpi_evaluate_integer(data->handle, "ECRR", &input, &output);
> +	if (ACPI_FAILURE(status))
> +		return -EIO;
> +
> +	if (output > U8_MAX)
> +		return -ENXIO;
> +
> +	usleep_range(UNIWILL_EC_DELAY_US, UNIWILL_EC_DELAY_US * 2);
> +
> +	*val = output;
> +
> +	return 0;
> +}
> +
> +static const struct regmap_bus uniwill_ec_bus = {
> +	.reg_write = uniwill_ec_reg_write,
> +	.reg_read = uniwill_ec_reg_read,
> +	.reg_format_endian_default = REGMAP_ENDIAN_LITTLE,
> +	.val_format_endian_default = REGMAP_ENDIAN_LITTLE,
> +};
> +
> +static bool uniwill_writeable_reg(struct device *dev, unsigned int reg)
> +{
> +	switch (reg) {
> +	case EC_ADDR_AP_OEM:
> +	case EC_ADDR_LIGHTBAR_AC_CTRL:
> +	case EC_ADDR_LIGHTBAR_AC_RED:
> +	case EC_ADDR_LIGHTBAR_AC_GREEN:
> +	case EC_ADDR_LIGHTBAR_AC_BLUE:
> +	case EC_ADDR_BIOS_OEM:
> +	case EC_ADDR_TRIGGER:
> +	case EC_ADDR_OEM_4:
> +	case EC_ADDR_CHARGE_CTRL:
> +	case EC_ADDR_LIGHTBAR_BAT_CTRL:
> +	case EC_ADDR_LIGHTBAR_BAT_RED:
> +	case EC_ADDR_LIGHTBAR_BAT_GREEN:
> +	case EC_ADDR_LIGHTBAR_BAT_BLUE:
> +		return true;
> +	default:
> +		return false;
> +	}
> +}
> +
> +static bool uniwill_readable_reg(struct device *dev, unsigned int reg)
> +{
> +	switch (reg) {
> +	case EC_ADDR_CPU_TEMP:
> +	case EC_ADDR_GPU_TEMP:
> +	case EC_ADDR_MAIN_FAN_RPM_1:
> +	case EC_ADDR_MAIN_FAN_RPM_2:
> +	case EC_ADDR_SECOND_FAN_RPM_1:
> +	case EC_ADDR_SECOND_FAN_RPM_2:
> +	case EC_ADDR_BAT_ALERT:
> +	case EC_ADDR_PROJECT_ID:
> +	case EC_ADDR_AP_OEM:
> +	case EC_ADDR_LIGHTBAR_AC_CTRL:
> +	case EC_ADDR_LIGHTBAR_AC_RED:
> +	case EC_ADDR_LIGHTBAR_AC_GREEN:
> +	case EC_ADDR_LIGHTBAR_AC_BLUE:
> +	case EC_ADDR_BIOS_OEM:
> +	case EC_ADDR_PWM_1:
> +	case EC_ADDR_PWM_2:
> +	case EC_ADDR_TRIGGER:
> +	case EC_ADDR_SWITCH_STATUS:
> +	case EC_ADDR_OEM_4:
> +	case EC_ADDR_CHARGE_CTRL:
> +	case EC_ADDR_LIGHTBAR_BAT_CTRL:
> +	case EC_ADDR_LIGHTBAR_BAT_RED:
> +	case EC_ADDR_LIGHTBAR_BAT_GREEN:
> +	case EC_ADDR_LIGHTBAR_BAT_BLUE:
> +		return true;
> +	default:
> +		return false;
> +	}
> +}
> +
> +static bool uniwill_volatile_reg(struct device *dev, unsigned int reg)
> +{
> +	switch (reg) {
> +	case EC_ADDR_CPU_TEMP:
> +	case EC_ADDR_GPU_TEMP:
> +	case EC_ADDR_MAIN_FAN_RPM_1:
> +	case EC_ADDR_MAIN_FAN_RPM_2:
> +	case EC_ADDR_SECOND_FAN_RPM_1:
> +	case EC_ADDR_SECOND_FAN_RPM_2:
> +	case EC_ADDR_BAT_ALERT:
> +	case EC_ADDR_PWM_1:
> +	case EC_ADDR_PWM_2:
> +	case EC_ADDR_TRIGGER:
> +	case EC_ADDR_SWITCH_STATUS:
> +	case EC_ADDR_CHARGE_CTRL:
> +		return true;
> +	default:
> +		return false;
> +	}
> +}
> +
> +static const struct regmap_config uniwill_ec_config = {
> +	.reg_bits = 16,
> +	.val_bits = 8,
> +	.writeable_reg = uniwill_writeable_reg,
> +	.readable_reg = uniwill_readable_reg,
> +	.volatile_reg = uniwill_volatile_reg,
> +	.can_sleep = true,
> +	.max_register = 0xFFF,
> +	.cache_type = REGCACHE_MAPLE,
> +	.use_single_read = true,
> +	.use_single_write = true,
> +};
> +
> +static ssize_t fn_lock_store(struct device *dev, struct device_attribute *attr, const char *buf,
> +			     size_t count)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	int ret;
> +
> +	ret = sysfs_match_string(uniwill_enable_disable_strings, buf);
> +	if (ret < 0)
> +		return ret;
> +
> +	if (ret)
> +		value = FN_LOCK_STATUS;
> +	else
> +		value = 0;
> +
> +	ret = regmap_update_bits(data->regmap, EC_ADDR_BIOS_OEM, FN_LOCK_STATUS, value);
> +	if (ret < 0)
> +		return ret;
> +
> +	return count;
> +}
> +
> +static ssize_t fn_lock_show(struct device *dev, struct device_attribute *attr, char *buf)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	int ret;
> +
> +	ret = regmap_read(data->regmap, EC_ADDR_BIOS_OEM, &value);
> +	if (ret < 0)
> +		return ret;
> +
> +	return sysfs_emit(buf, "%s\n", str_enable_disable(value & FN_LOCK_STATUS));
> +}
> +
> +static DEVICE_ATTR_RW(fn_lock);

The fn_lock register value does not automatically get updated by pressing the 
fn+esc key (unlicke the super_key_lock), so the driver needs to do that manually.

Another posibility is: uniwill sometimes have a "config" and an "immediate" 
value for a setting, waybe we have the config value here (and have the immediate 
value for the super_key_lock)

Also I realized: The value here is preserved on hot, but not on cold reboots, 
maybe this should be initialized by the driver for consistency?

> +
> +static ssize_t super_key_lock_store(struct device *dev, struct device_attribute *attr,
> +				    const char *buf, size_t count)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	int state, ret;
> +
> +	state = sysfs_match_string(uniwill_enable_disable_strings, buf);
> +	if (state < 0)
> +		return state;
> +
> +	guard(mutex)(&data->super_key_lock);
> +
> +	ret = regmap_read(data->regmap, EC_ADDR_SWITCH_STATUS, &value);
> +	if (ret < 0)
> +		return ret;
> +
> +	/*
> +	 * We can only toggle the super key lock, so we return early if the setting
> +	 * is already in the correct state.
> +	 */
> +	if (state == !(value & SUPER_KEY_LOCK_STATUS))
> +		return count;
> +
> +	ret = regmap_write_bits(data->regmap, EC_ADDR_TRIGGER, TRIGGER_SUPER_KEY_LOCK,
> +				TRIGGER_SUPER_KEY_LOCK);
> +	if (ret < 0)
> +		return ret;
> +
> +	return count;
> +}
> +
> +static ssize_t super_key_lock_show(struct device *dev, struct device_attribute *attr, char *buf)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	int ret;
> +
> +	ret = regmap_read(data->regmap, EC_ADDR_SWITCH_STATUS, &value);
> +	if (ret < 0)
> +		return ret;
> +
> +	return sysfs_emit(buf, "%s\n", str_enable_disable(!(value & SUPER_KEY_LOCK_STATUS)));
> +}
> +
> +static DEVICE_ATTR_RW(super_key_lock);

I did not know what "super_key_lock" was supposed to mean at first, a more 
fitting name would be super_key_enable imho.

Cold vs hot reboot volatility not tested, but wouldn't hurt to initialize imho 
as i don't trust uniwill to be consistent in this point across multiple device 
generations.

> +
> +static ssize_t touchpad_toggle_store(struct device *dev, struct device_attribute *attr,
> +				     const char *buf, size_t count)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	int ret;
> +
> +	ret = sysfs_match_string(uniwill_enable_disable_strings, buf);
> +	if (ret < 0)
> +		return ret;
> +
> +	if (ret)
> +		value = 0;
> +	else
> +		value = TOUCHPAD_TOGGLE_OFF;
> +
> +	ret = regmap_update_bits(data->regmap, EC_ADDR_OEM_4, TOUCHPAD_TOGGLE_OFF, value);
> +	if (ret < 0)
> +		return ret;
> +
> +	return count;
> +}
> +
> +static ssize_t touchpad_toggle_show(struct device *dev, struct device_attribute *attr, char *buf)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	int ret;
> +
> +	ret = regmap_read(data->regmap, EC_ADDR_OEM_4, &value);
> +	if (ret < 0)
> +		return ret;
> +
> +	return sysfs_emit(buf, "%s\n", str_enable_disable(!(value & TOUCHPAD_TOGGLE_OFF)));
> +}
> +
> +static DEVICE_ATTR_RW(touchpad_toggle);
What exactly does this do? Seems like a noop on my testing devices. Also is 
touchpad disable not already handled by userspace?
> +
> +static ssize_t rainbow_animation_store(struct device *dev, struct device_attribute *attr,
> +				       const char *buf, size_t count)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	int ret;
> +
> +	ret = sysfs_match_string(uniwill_enable_disable_strings, buf);
> +	if (ret < 0)
> +		return ret;
> +
> +	if (ret)
> +		value = LIGHTBAR_WELCOME;
> +	else
> +		value = 0;
> +
> +	ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, LIGHTBAR_WELCOME, value);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_BAT_CTRL, LIGHTBAR_WELCOME, value);
> +	if (ret < 0)
> +		return ret;
> +
> +	return count;
> +}
> +
> +static ssize_t rainbow_animation_show(struct device *dev, struct device_attribute *attr, char *buf)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	int ret;
> +
> +	ret = regmap_read(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, &value);
> +	if (ret < 0)
> +		return ret;
> +
> +	return sysfs_emit(buf, "%s\n", str_enable_disable(value & LIGHTBAR_WELCOME));
> +}
> +
> +static DEVICE_ATTR_RW(rainbow_animation);
> +
> +static ssize_t breathing_in_suspend_store(struct device *dev, struct device_attribute *attr,
> +					  const char *buf, size_t count)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	int ret;
> +
> +	ret = sysfs_match_string(uniwill_enable_disable_strings, buf);
> +	if (ret < 0)
> +		return ret;
> +
> +	if (ret)
> +		value = 0;
> +	else
> +		value = LIGHTBAR_S3_OFF;
> +
> +	ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, LIGHTBAR_S3_OFF, value);
> +	if (ret < 0)
> +		return ret;
> +
> +	return count;
> +}
> +
> +static ssize_t breathing_in_suspend_show(struct device *dev, struct device_attribute *attr,
> +					 char *buf)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	int ret;
> +
> +	ret = regmap_read(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, &value);
> +	if (ret < 0)
> +		return ret;
> +
> +	return sysfs_emit(buf, "%s\n", str_enable_disable(!(value & LIGHTBAR_S3_OFF)));
> +}
> +
> +static DEVICE_ATTR_RW(breathing_in_suspend);
> +
> +static struct attribute *uniwill_attrs[] = {
> +	/* Keyboard-related */
> +	&dev_attr_fn_lock.attr,
> +	&dev_attr_super_key_lock.attr,
> +	&dev_attr_touchpad_toggle.attr,
> +	/* Lightbar-related */
> +	&dev_attr_rainbow_animation.attr,
> +	&dev_attr_breathing_in_suspend.attr,
> +	NULL
> +};
> +
> +static umode_t uniwill_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n)
> +{
> +	if (attr == &dev_attr_fn_lock.attr) {
> +		if (supported_features & UNIWILL_FEATURE_FN_LOCK)
> +			return attr->mode;
> +	}
> +
> +	if (attr == &dev_attr_super_key_lock.attr) {
> +		if (supported_features & UNIWILL_FEATURE_SUPER_KEY_LOCK)
> +			return attr->mode;
> +	}
> +
> +	if (attr == &dev_attr_touchpad_toggle.attr) {
> +		if (supported_features & UNIWILL_FEATURE_TOUCHPAD_TOGGLE)
> +			return attr->mode;
> +	}
> +
> +	if (attr == &dev_attr_rainbow_animation.attr ||
> +	    attr == &dev_attr_breathing_in_suspend.attr) {
> +		if (supported_features & UNIWILL_FEATURE_LIGHTBAR)
> +			return attr->mode;
> +	}
> +
> +	return 0;
> +}
> +
> +static const struct attribute_group uniwill_group = {
> +	.is_visible = uniwill_attr_is_visible,
> +	.attrs = uniwill_attrs,
> +};
> +
> +static const struct attribute_group *uniwill_groups[] = {
> +	&uniwill_group,
> +	NULL
> +};
> +
> +static int uniwill_read(struct device *dev, enum hwmon_sensor_types type, u32 attr, int channel,
> +			long *val)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	unsigned int value;
> +	__be16 rpm;
> +	int ret;
> +
> +	switch (type) {
> +	case hwmon_temp:
> +		switch (channel) {
> +		case 0:
> +			ret = regmap_read(data->regmap, EC_ADDR_CPU_TEMP, &value);
> +			break;
> +		case 1:
> +			ret = regmap_read(data->regmap, EC_ADDR_GPU_TEMP, &value);
> +			break;
> +		default:
> +			return -EOPNOTSUPP;
> +		}
> +
> +		if (ret < 0)
> +			return ret;
> +
> +		*val = value * MILLIDEGREE_PER_DEGREE;
> +		return 0;
> +	case hwmon_fan:
> +		switch (channel) {
> +		case 0:
> +			ret = regmap_bulk_read(data->regmap, EC_ADDR_MAIN_FAN_RPM_1, &rpm,
> +					       sizeof(rpm));
> +			break;
> +		case 1:
> +			ret = regmap_bulk_read(data->regmap, EC_ADDR_SECOND_FAN_RPM_1, &rpm,
> +					       sizeof(rpm));
> +			break;
> +		default:
> +			return -EOPNOTSUPP;
> +		}
> +
> +		if (ret < 0)
> +			return ret;
> +
> +		*val = be16_to_cpu(rpm);
> +		return 0;
> +	case hwmon_pwm:
> +		switch (channel) {
> +		case 0:
> +			ret = regmap_read(data->regmap, EC_ADDR_PWM_1, &value);
> +			break;
> +		case 1:
> +			ret = regmap_read(data->regmap, EC_ADDR_PWM_2, &value);
> +			break;
> +		default:
> +			return -EOPNOTSUPP;
> +		}
> +
> +		if (ret < 0)
> +			return ret;
> +
> +		*val = fixp_linear_interpolate(0, 0, PWM_MAX, U8_MAX, value);
> +		return 0;
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +}
> +
> +static int uniwill_read_string(struct device *dev, enum hwmon_sensor_types type, u32 attr,
> +			       int channel, const char **str)
> +{
> +	switch (type) {
> +	case hwmon_temp:
> +		*str = uniwill_temp_labels[channel];
> +		return 0;
> +	case hwmon_fan:
> +		*str = uniwill_fan_labels[channel];
> +		return 0;
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +}
> +
> +static const struct hwmon_ops uniwill_ops = {
> +	.visible = 0444,
> +	.read = uniwill_read,
> +	.read_string = uniwill_read_string,
> +};

.visible should hide gpu temp sensor on devices that don't have a dgpu and 
therefore not gpu temp sensor (the value is stuck at 0 on these devices)

also the number of fan might also not always be exactly 2

> +
> +static const struct hwmon_channel_info * const uniwill_info[] = {
> +	HWMON_CHANNEL_INFO(chip, HWMON_C_REGISTER_TZ),
> +	HWMON_CHANNEL_INFO(temp,
> +			   HWMON_T_INPUT | HWMON_T_LABEL,
> +			   HWMON_T_INPUT | HWMON_T_LABEL),
> +	HWMON_CHANNEL_INFO(fan,
> +			   HWMON_F_INPUT | HWMON_F_LABEL,
> +			   HWMON_F_INPUT | HWMON_F_LABEL),
> +	HWMON_CHANNEL_INFO(pwm,
> +			   HWMON_PWM_INPUT,
> +			   HWMON_PWM_INPUT),
> +	NULL
> +};
> +
> +static const struct hwmon_chip_info uniwill_chip_info = {
> +	.ops = &uniwill_ops,
> +	.info = uniwill_info,
> +};
> +
> +static int uniwill_hwmon_init(struct uniwill_data *data)
> +{
> +	struct device *hdev;
> +
> +	if (!(supported_features & UNIWILL_FEATURE_HWMON))
> +		return 0;
> +
> +	hdev = devm_hwmon_device_register_with_info(data->dev, "uniwill", data,
> +						    &uniwill_chip_info, NULL);
> +
> +	return PTR_ERR_OR_ZERO(hdev);
> +}
> +
> +static const unsigned int uniwill_led_channel_to_bat_reg[LED_CHANNELS] = {
> +	EC_ADDR_LIGHTBAR_BAT_RED,
> +	EC_ADDR_LIGHTBAR_BAT_GREEN,
> +	EC_ADDR_LIGHTBAR_BAT_BLUE,
> +};
> +
> +static const unsigned int uniwill_led_channel_to_ac_reg[LED_CHANNELS] = {
> +	EC_ADDR_LIGHTBAR_AC_RED,
> +	EC_ADDR_LIGHTBAR_AC_GREEN,
> +	EC_ADDR_LIGHTBAR_AC_BLUE,
> +};
> +
> +static int uniwill_led_brightness_set(struct led_classdev *led_cdev, enum led_brightness brightness)
> +{
> +	struct led_classdev_mc *led_mc_cdev = lcdev_to_mccdev(led_cdev);
> +	struct uniwill_data *data = container_of(led_mc_cdev, struct uniwill_data, led_mc_cdev);
> +	unsigned int value;
> +	int ret;
> +
> +	ret = led_mc_calc_color_components(led_mc_cdev, brightness);
> +	if (ret < 0)
> +		return ret;
> +
> +	for (int i = 0; i < LED_CHANNELS; i++) {
> +		/* Prevent the brightness values from overflowing */
> +		value = min(LED_MAX_BRIGHTNESS, data->led_mc_subled_info[i].brightness);
> +		ret = regmap_write(data->regmap, uniwill_led_channel_to_ac_reg[i], value);
> +		if (ret < 0)
> +			return ret;
> +
> +		ret = regmap_write(data->regmap, uniwill_led_channel_to_bat_reg[i], value);
> +		if (ret < 0)
> +			return ret;
> +	}
> +
> +	if (brightness)
> +		value = 0;
> +	else
> +		value = LIGHTBAR_S0_OFF;
> +
> +	ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, LIGHTBAR_S0_OFF, value);
> +	if (ret < 0)
> +		return ret;
> +
> +	return regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_BAT_CTRL, LIGHTBAR_S0_OFF, value);
> +}
> +
> +#define LIGHTBAR_MASK	(LIGHTBAR_APP_EXISTS | LIGHTBAR_S0_OFF | LIGHTBAR_S3_OFF | LIGHTBAR_WELCOME)
> +
> +static int uniwill_led_init(struct uniwill_data *data)
> +{
> +	struct led_init_data init_data = {
> +		.devicename = DRIVER_NAME,
> +		.default_label = "multicolor:" LED_FUNCTION_STATUS,
> +		.devname_mandatory = true,
> +	};
> +	unsigned int color_indices[3] = {
> +		LED_COLOR_ID_RED,
> +		LED_COLOR_ID_GREEN,
> +		LED_COLOR_ID_BLUE,
> +	};
> +	unsigned int value;
> +	int ret;
> +
> +	if (!(supported_features & UNIWILL_FEATURE_LIGHTBAR))
> +		return 0;
> +
> +	/*
> +	 * The EC has separate lightbar settings for AC and battery mode,
> +	 * so we have to ensure that both settings are the same.
> +	 */
> +	ret = regmap_read(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, &value);
> +	if (ret < 0)
> +		return ret;
> +
> +	value |= LIGHTBAR_APP_EXISTS;
> +	ret = regmap_write(data->regmap, EC_ADDR_LIGHTBAR_AC_CTRL, value);
> +	if (ret < 0)
> +		return ret;
> +
> +	/*
> +	 * The breathing animation during suspend is not supported when
> +	 * running on battery power.
> +	 */
> +	value |= LIGHTBAR_S3_OFF;
> +	ret = regmap_update_bits(data->regmap, EC_ADDR_LIGHTBAR_BAT_CTRL, LIGHTBAR_MASK, value);
> +	if (ret < 0)
> +		return ret;
> +
> +	data->led_mc_cdev.led_cdev.color = LED_COLOR_ID_MULTI;
> +	data->led_mc_cdev.led_cdev.max_brightness = LED_MAX_BRIGHTNESS;
> +	data->led_mc_cdev.led_cdev.flags = LED_REJECT_NAME_CONFLICT;
> +	data->led_mc_cdev.led_cdev.brightness_set_blocking = uniwill_led_brightness_set;
> +
> +	if (value & LIGHTBAR_S0_OFF)
> +		data->led_mc_cdev.led_cdev.brightness = 0;
> +	else
> +		data->led_mc_cdev.led_cdev.brightness = LED_MAX_BRIGHTNESS;
> +
> +	for (int i = 0; i < LED_CHANNELS; i++) {
> +		data->led_mc_subled_info[i].color_index = color_indices[i];
> +
> +		ret = regmap_read(data->regmap, uniwill_led_channel_to_ac_reg[i], &value);
> +		if (ret < 0)
> +			return ret;
> +
> +		/*
> +		 * Make sure that the initial intensity value is not greater than
> +		 * the maximum brightness.
> +		 */
> +		value = min(LED_MAX_BRIGHTNESS, value);
> +		ret = regmap_write(data->regmap, uniwill_led_channel_to_ac_reg[i], value);
> +		if (ret < 0)
> +			return ret;
> +
> +		ret = regmap_write(data->regmap, uniwill_led_channel_to_bat_reg[i], value);
> +		if (ret < 0)
> +			return ret;
> +
> +		data->led_mc_subled_info[i].intensity = value;
> +		data->led_mc_subled_info[i].channel = i;
> +	}
> +
> +	data->led_mc_cdev.subled_info = data->led_mc_subled_info;
> +	data->led_mc_cdev.num_colors = LED_CHANNELS;
> +
> +	return devm_led_classdev_multicolor_register_ext(data->dev, &data->led_mc_cdev,
> +							 &init_data);
> +}
> +
> +static int uniwill_get_property(struct power_supply *psy, const struct power_supply_ext *ext,
> +				void *drvdata, enum power_supply_property psp,
> +				union power_supply_propval *val)
> +{
> +	struct uniwill_data *data = drvdata;
> +	union power_supply_propval prop;
> +	unsigned int regval;
> +	int ret;
> +
> +	switch (psp) {
> +	case POWER_SUPPLY_PROP_HEALTH:
> +		ret = power_supply_get_property_direct(psy, POWER_SUPPLY_PROP_PRESENT, &prop);
> +		if (ret < 0)
> +			return ret;
> +
> +		if (!prop.intval) {
> +			val->intval = POWER_SUPPLY_HEALTH_NO_BATTERY;
> +			return 0;
> +		}
> +
> +		ret = power_supply_get_property_direct(psy, POWER_SUPPLY_PROP_STATUS, &prop);
> +		if (ret < 0)
> +			return ret;
> +
> +		if (prop.intval == POWER_SUPPLY_STATUS_UNKNOWN) {
> +			val->intval = POWER_SUPPLY_HEALTH_UNKNOWN;
> +			return 0;
> +		}
> +
> +		ret = regmap_read(data->regmap, EC_ADDR_BAT_ALERT, &regval);
> +		if (ret < 0)
> +			return ret;
> +
> +		if (regval) {
> +			/* Charging issue */
> +			val->intval = POWER_SUPPLY_HEALTH_UNSPEC_FAILURE;
> +			return 0;
> +		}
> +
> +		val->intval = POWER_SUPPLY_HEALTH_GOOD;
> +		return 0;
> +	case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD:
> +		ret = regmap_read(data->regmap, EC_ADDR_CHARGE_CTRL, &regval);
> +		if (ret < 0)
> +			return ret;
> +
> +		val->intval = clamp_val(FIELD_GET(CHARGE_CTRL_MASK, regval), 0, 100);
> +		return 0;
> +	default:
> +		return -EINVAL;
> +	}
> +}
> +
> +static int uniwill_set_property(struct power_supply *psy, const struct power_supply_ext *ext,
> +				void *drvdata, enum power_supply_property psp,
> +				const union power_supply_propval *val)
> +{
> +	struct uniwill_data *data = drvdata;
> +
> +	switch (psp) {
> +	case POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD:
> +		if (val->intval < 1 || val->intval > 100)
> +			return -EINVAL;
> +
> +		return regmap_update_bits(data->regmap, EC_ADDR_CHARGE_CTRL, CHARGE_CTRL_MASK,
> +					  val->intval);
> +	default:
> +		return -EINVAL;
> +	}
> +}
> +
> +static int uniwill_property_is_writeable(struct power_supply *psy,
> +					 const struct power_supply_ext *ext, void *drvdata,
> +					 enum power_supply_property psp)
> +{
> +	if (psp == POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD)
> +		return true;
> +
> +	return false;
> +}
> +
> +static const enum power_supply_property uniwill_properties[] = {
> +	POWER_SUPPLY_PROP_HEALTH,
> +	POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD,
> +};
> +
> +static const struct power_supply_ext uniwill_extension = {
> +	.name = DRIVER_NAME,
> +	.properties = uniwill_properties,
> +	.num_properties = ARRAY_SIZE(uniwill_properties),
> +	.get_property = uniwill_get_property,
> +	.set_property = uniwill_set_property,
> +	.property_is_writeable = uniwill_property_is_writeable,
> +};
> +
> +static int uniwill_add_battery(struct power_supply *battery, struct acpi_battery_hook *hook)
> +{
> +	struct uniwill_data *data = container_of(hook, struct uniwill_data, hook);
> +	struct uniwill_battery_entry *entry;
> +	int ret;
> +
> +	entry = kzalloc(sizeof(*entry), GFP_KERNEL);
> +	if (!entry)
> +		return -ENOMEM;
> +
> +	ret = power_supply_register_extension(battery, &uniwill_extension, data->dev, data);
> +	if (ret < 0) {
> +		kfree(entry);
> +		return ret;
> +	}
> +
> +	guard(mutex)(&data->battery_lock);
> +
> +	entry->battery = battery;
> +	list_add(&entry->head, &data->batteries);
> +
> +	return 0;
> +}
> +
> +static int uniwill_remove_battery(struct power_supply *battery, struct acpi_battery_hook *hook)
> +{
> +	struct uniwill_data *data = container_of(hook, struct uniwill_data, hook);
> +	struct uniwill_battery_entry *entry, *tmp;
> +
> +	scoped_guard(mutex, &data->battery_lock) {
> +		list_for_each_entry_safe(entry, tmp, &data->batteries, head) {
> +			if (entry->battery == battery) {
> +				list_del(&entry->head);
> +				kfree(entry);
> +				break;
> +			}
> +		}
> +	}
> +
> +	power_supply_unregister_extension(battery, &uniwill_extension);
> +
> +	return 0;
> +}
> +
> +static int uniwill_battery_init(struct uniwill_data *data)
> +{
> +	int ret;
> +
> +	if (!(supported_features & UNIWILL_FEATURE_BATTERY))
> +		return 0;
> +
> +	ret = devm_mutex_init(data->dev, &data->battery_lock);
> +	if (ret < 0)
> +		return ret;
> +
> +	INIT_LIST_HEAD(&data->batteries);
> +	data->hook.name = "Uniwill Battery Extension";
> +	data->hook.add_battery = uniwill_add_battery;
> +	data->hook.remove_battery = uniwill_remove_battery;
> +
> +	return devm_battery_hook_register(data->dev, &data->hook);
> +}
> +
> +static int uniwill_notifier_call(struct notifier_block *nb, unsigned long action, void *dummy)
> +{
> +	struct uniwill_data *data = container_of(nb, struct uniwill_data, nb);
> +	struct uniwill_battery_entry *entry;
> +
> +	switch (action) {
> +	case UNIWILL_OSD_BATTERY_ALERT:
> +		guard(mutex)(&data->battery_lock);
> +		list_for_each_entry(entry, &data->batteries, head) {
> +			power_supply_changed(entry->battery);
> +		}
> +
> +		return NOTIFY_OK;
> +	default:
> +		guard(mutex)(&data->input_lock);
> +		sparse_keymap_report_event(data->input_device, action, 1, true);
> +
> +		return NOTIFY_OK;
> +	}
> +}
> +
> +static int uniwill_input_init(struct uniwill_data *data)
> +{
> +	int ret;
> +
> +	ret = devm_mutex_init(data->dev, &data->input_lock);
> +	if (ret < 0)
> +		return ret;
> +
> +	data->input_device = devm_input_allocate_device(data->dev);
> +	if (!data->input_device)
> +		return -ENOMEM;
> +
> +	ret = sparse_keymap_setup(data->input_device, uniwill_keymap, NULL);
> +	if (ret < 0)
> +		return ret;
> +
> +	data->input_device->name = "Uniwill WMI hotkeys";
> +	data->input_device->phys = "wmi/input0";
> +	data->input_device->id.bustype = BUS_HOST;
> +	ret = input_register_device(data->input_device);
> +	if (ret < 0)
> +		return ret;
> +
> +	data->nb.notifier_call = uniwill_notifier_call;
> +
> +	return devm_uniwill_wmi_register_notifier(data->dev, &data->nb);
> +}
> +
> +static void uniwill_disable_manual_control(void *context)
> +{
> +	struct uniwill_data *data = context;
> +
> +	regmap_clear_bits(data->regmap, EC_ADDR_AP_OEM, ENABLE_MANUAL_CTRL);
> +}
> +
> +static int uniwill_ec_init(struct uniwill_data *data)
> +{
> +	unsigned int value;
> +	int ret;
> +
> +	ret = regmap_read(data->regmap, EC_ADDR_PROJECT_ID, &value);
> +	if (ret < 0)
> +		return ret;
> +
> +	dev_dbg(data->dev, "Project ID: %u\n", value);
> +
> +	ret = regmap_set_bits(data->regmap, EC_ADDR_AP_OEM, ENABLE_MANUAL_CTRL);
> +	if (ret < 0)
> +		return ret;
> +
> +	return devm_add_action_or_reset(data->dev, uniwill_disable_manual_control, data);
> +}
> +
> +static int uniwill_probe(struct platform_device *pdev)
> +{
> +	struct uniwill_data *data;
> +	struct regmap *regmap;
> +	acpi_handle handle;
> +	int ret;
> +
> +	handle = ACPI_HANDLE(&pdev->dev);
> +	if (!handle)
> +		return -ENODEV;
> +
> +	data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);
> +	if (!data)
> +		return -ENOMEM;
> +
> +	data->dev = &pdev->dev;
> +	data->handle = handle;
> +	platform_set_drvdata(pdev, data);
> +
> +	regmap = devm_regmap_init(&pdev->dev, &uniwill_ec_bus, data, &uniwill_ec_config);
> +	if (IS_ERR(regmap))
> +		return PTR_ERR(regmap);
> +
> +	data->regmap = regmap;
> +	ret = devm_mutex_init(&pdev->dev, &data->super_key_lock);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = uniwill_ec_init(data);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = uniwill_battery_init(data);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = uniwill_led_init(data);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = uniwill_hwmon_init(data);
> +	if (ret < 0)
> +		return ret;
> +
> +	return uniwill_input_init(data);
> +}
> +
> +static void uniwill_shutdown(struct platform_device *pdev)
> +{
> +	struct uniwill_data *data = platform_get_drvdata(pdev);
> +
> +	regmap_clear_bits(data->regmap, EC_ADDR_AP_OEM, ENABLE_MANUAL_CTRL);
> +}
> +
> +static int uniwill_suspend_keyboard(struct uniwill_data *data)
> +{
> +	if (!(supported_features & UNIWILL_FEATURE_SUPER_KEY_LOCK))
> +		return 0;
> +
> +	/*
> +	 * The EC_ADDR_SWITCH_STATUS is marked as volatile, so we have to restore it
> +	 * ourselves.
> +	 */
> +	return regmap_read(data->regmap, EC_ADDR_SWITCH_STATUS, &data->last_switch_status);
> +}
> +
> +static int uniwill_suspend_battery(struct uniwill_data *data)
> +{
> +	if (!(supported_features & UNIWILL_FEATURE_BATTERY))
> +		return 0;
> +
> +	/*
> +	 * Save the current charge limit in order to restore it during resume.
> +	 * We cannot use the regmap code for that since this register needs to
> +	 * be declared as volatile due to CHARGE_CTRL_REACHED.
> +	 */
> +	return regmap_read(data->regmap, EC_ADDR_CHARGE_CTRL, &data->last_charge_ctrl);
> +}
> +
> +static int uniwill_suspend(struct device *dev)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	int ret;
> +
> +	ret = uniwill_suspend_keyboard(data);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = uniwill_suspend_battery(data);
> +	if (ret < 0)
> +		return ret;
> +
> +	regcache_cache_only(data->regmap, true);
> +	regcache_mark_dirty(data->regmap);
> +
> +	return 0;
> +}
> +
> +static int uniwill_resume_keyboard(struct uniwill_data *data)
> +{
> +	unsigned int value;
> +	int ret;
> +
> +	if (!(supported_features & UNIWILL_FEATURE_SUPER_KEY_LOCK))
> +		return 0;
> +
> +	ret = regmap_read(data->regmap, EC_ADDR_SWITCH_STATUS, &value);
> +	if (ret < 0)
> +		return ret;
> +
> +	if ((data->last_switch_status & SUPER_KEY_LOCK_STATUS) == (value & SUPER_KEY_LOCK_STATUS))
> +		return 0;
> +
> +	return regmap_write_bits(data->regmap, EC_ADDR_TRIGGER, TRIGGER_SUPER_KEY_LOCK,
> +				 TRIGGER_SUPER_KEY_LOCK);
> +}
> +
> +static int uniwill_resume_battery(struct uniwill_data *data)
> +{
> +	if (!(supported_features & UNIWILL_FEATURE_BATTERY))
> +		return 0;
> +
> +	return regmap_update_bits(data->regmap, EC_ADDR_CHARGE_CTRL, CHARGE_CTRL_MASK,
> +				  data->last_charge_ctrl);
> +}
> +
> +static int uniwill_resume(struct device *dev)
> +{
> +	struct uniwill_data *data = dev_get_drvdata(dev);
> +	int ret;
> +
> +	regcache_cache_only(data->regmap, false);
> +
> +	ret = regcache_sync(data->regmap);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = uniwill_resume_keyboard(data);
> +	if (ret < 0)
> +		return ret;
> +
> +	return uniwill_resume_battery(data);
> +}
> +
> +static DEFINE_SIMPLE_DEV_PM_OPS(uniwill_pm_ops, uniwill_suspend, uniwill_resume);
> +
> +/*
> + * We only use the DMI table for auoloading because the ACPI device itself
> + * does not guarantee that the underlying EC implementation is supported.
> + */
> +static const struct acpi_device_id uniwill_id_table[] = {
> +	{ "INOU0000" },
> +	{ },
> +};
> +
> +static struct platform_driver uniwill_driver = {
> +	.driver = {
> +		.name = DRIVER_NAME,
> +		.dev_groups = uniwill_groups,
> +		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
> +		.acpi_match_table = uniwill_id_table,
> +		.pm = pm_sleep_ptr(&uniwill_pm_ops),
> +	},
> +	.probe = uniwill_probe,
> +	.shutdown = uniwill_shutdown,
> +};
> +
> +static const struct dmi_system_id uniwill_dmi_table[] __initconst = {
> +	{
> +		.ident = "Intel NUC x15",
> +		.matches = {
> +			DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Intel(R) Client Systems"),
> +			DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "LAPAC71H"),
> +		},
> +		.driver_data = (void *)(UNIWILL_FEATURE_FN_LOCK |
> +					UNIWILL_FEATURE_SUPER_KEY_LOCK |
> +					UNIWILL_FEATURE_TOUCHPAD_TOGGLE |
> +					UNIWILL_FEATURE_BATTERY |
> +					UNIWILL_FEATURE_HWMON),
> +	},
> +	{
> +		.ident = "Intel NUC x15",
> +		.matches = {
> +			DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Intel(R) Client Systems"),
> +			DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "LAPKC71F"),
> +		},
> +		.driver_data = (void *)(UNIWILL_FEATURE_FN_LOCK |
> +					UNIWILL_FEATURE_SUPER_KEY_LOCK |
> +					UNIWILL_FEATURE_TOUCHPAD_TOGGLE |
> +					UNIWILL_FEATURE_LIGHTBAR |
> +					UNIWILL_FEATURE_BATTERY |
> +					UNIWILL_FEATURE_HWMON),
> +	},
> +	{ }
> +};
> +MODULE_DEVICE_TABLE(dmi, uniwill_dmi_table);
> +
> +static int __init uniwill_init(void)
> +{
> +	const struct dmi_system_id *id;
> +	int ret;
> +
> +	id = dmi_first_match(uniwill_dmi_table);
> +	if (!id) {
> +		if (!force)
> +			return -ENODEV;
> +
> +		/* Assume that the device supports all features */
> +		supported_features = UINT_MAX;

in the future there might be mutually exclusive feature (for example when Uniwil 
repurposes EC registers)

my suggestion would be to have a "force_supported_features" in addition that 
overwrites the supported_features list (also for devices that are in the list)

so something like:

if (!id && !force)

     return -ENODEV

if (force)

     supported_features = force_supported_features

else

     supported_features = (uintptr_t)id->driver_data;

> +		pr_warn("Loading on a potentially unsupported device\n");
> +	} else {
> +		supported_features = (uintptr_t)id->driver_data;
> +	}
> +
> +	ret = platform_driver_register(&uniwill_driver);
> +	if (ret < 0)
> +		return ret;
> +
> +	ret = uniwill_wmi_register_driver();
> +	if (ret < 0) {
> +		platform_driver_unregister(&uniwill_driver);
> +		return ret;
> +	}
> +
> +	return 0;
> +}
> +module_init(uniwill_init);
> +
> +static void __exit uniwill_exit(void)
> +{
> +	uniwill_wmi_unregister_driver();
> +	platform_driver_unregister(&uniwill_driver);
> +}
> +module_exit(uniwill_exit);
> +
> +MODULE_AUTHOR("Armin Wolf<W_Armin@....de>");
> +MODULE_DESCRIPTION("Uniwill notebook driver");
> +MODULE_LICENSE("GPL");
> diff --git a/drivers/platform/x86/uniwill/uniwill-wmi.c b/drivers/platform/x86/uniwill/uniwill-wmi.c
> new file mode 100644
> index 000000000000..31d9c39f14ab
> --- /dev/null
> +++ b/drivers/platform/x86/uniwill/uniwill-wmi.c
> @@ -0,0 +1,92 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Linux hotkey driver for Uniwill notebooks.
> + *
> + * Special thanks go to Pőcze Barnabás, Christoffer Sandberg and Werner Sembach
> + * for supporting the development of this driver either through prior work or
> + * by answering questions regarding the underlying WMI interface.
> + *
> + * Copyright (C) 2025 Armin Wolf<W_Armin@....de>
> + */
> +
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
> +#include <linux/acpi.h>
> +#include <linux/device.h>
> +#include <linux/init.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/notifier.h>
> +#include <linux/printk.h>
> +#include <linux/types.h>
> +#include <linux/wmi.h>
> +
> +#include "uniwill-wmi.h"
> +
> +#define DRIVER_NAME		"uniwill-wmi"
> +#define UNIWILL_EVENT_GUID	"ABBC0F72-8EA1-11D1-00A0-C90629100000"
> +
> +static BLOCKING_NOTIFIER_HEAD(uniwill_wmi_chain_head);
> +
> +static void devm_uniwill_wmi_unregister_notifier(void *data)
> +{
> +	struct notifier_block *nb = data;
> +
> +	blocking_notifier_chain_unregister(&uniwill_wmi_chain_head, nb);
> +}
> +
> +int devm_uniwill_wmi_register_notifier(struct device *dev, struct notifier_block *nb)
> +{
> +	int ret;
> +
> +	ret = blocking_notifier_chain_register(&uniwill_wmi_chain_head, nb);
> +	if (ret < 0)
> +		return ret;
> +
> +	return devm_add_action_or_reset(dev, devm_uniwill_wmi_unregister_notifier, nb);
> +}
> +
> +static void uniwill_wmi_notify(struct wmi_device *wdev, union acpi_object *obj)
> +{
> +	u32 value;
> +
> +	if (obj->type != ACPI_TYPE_INTEGER)
> +		return;
> +
> +	value = obj->integer.value;
> +
> +	dev_dbg(&wdev->dev, "Received WMI event %u\n", value);
> +
> +	blocking_notifier_call_chain(&uniwill_wmi_chain_head, value, NULL);
> +}
> +
> +/*
> + * We cannot fully trust this GUID since Uniwill just copied the WMI GUID
> + * from the Windows driver example, and others probably did the same.
> + *
> + * Because of this we cannot use this WMI GUID for autoloading. Instead the
> + * associated driver will be registered manually after matching a DMI table.
> + */
> +static const struct wmi_device_id uniwill_wmi_id_table[] = {
> +	{ UNIWILL_EVENT_GUID, NULL },
> +	{ }
> +};
> +
> +static struct wmi_driver uniwill_wmi_driver = {
> +	.driver = {
> +		.name = DRIVER_NAME,
> +		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
> +	},
> +	.id_table = uniwill_wmi_id_table,
> +	.notify = uniwill_wmi_notify,
> +	.no_singleton = true,
> +};
> +
> +int __init uniwill_wmi_register_driver(void)
> +{
> +	return wmi_driver_register(&uniwill_wmi_driver);
> +}
> +
> +void __exit uniwill_wmi_unregister_driver(void)
> +{
> +	wmi_driver_unregister(&uniwill_wmi_driver);
> +}
> diff --git a/drivers/platform/x86/uniwill/uniwill-wmi.h b/drivers/platform/x86/uniwill/uniwill-wmi.h
> new file mode 100644
> index 000000000000..2bf69f2d8038
> --- /dev/null
> +++ b/drivers/platform/x86/uniwill/uniwill-wmi.h
> @@ -0,0 +1,127 @@
> +/* SPDX-License-Identifier: GPL-2.0-or-later */
> +/*
> + * Linux hotkey driver for Uniwill notebooks.
> + *
> + * Copyright (C) 2025 Armin Wolf<W_Armin@....de>
> + */
> +
> +#ifndef UNIWILL_WMI_H
> +#define UNIWILL_WMI_H
> +
> +#include <linux/init.h>
> +
> +#define UNIWILL_OSD_CAPSLOCK			0x01
> +#define UNIWILL_OSD_NUMLOCK			0x02
> +#define UNIWILL_OSD_SCROLLLOCK			0x03
> +
> +#define UNIWILL_OSD_TOUCHPAD_ON			0x04
> +#define UNIWILL_OSD_TOUCHPAD_OFF		0x05
> +
> +#define UNIWILL_OSD_SILENT_MODE_ON		0x06
> +#define UNIWILL_OSD_SILENT_MODE_OFF		0x07
> +
> +#define UNIWILL_OSD_WLAN_ON			0x08
> +#define UNIWILL_OSD_WLAN_OFF			0x09
> +
> +#define UNIWILL_OSD_WIMAX_ON			0x0A
> +#define UNIWILL_OSD_WIMAX_OFF			0x0B
> +
> +#define UNIWILL_OSD_BLUETOOTH_ON		0x0C
> +#define UNIWILL_OSD_BLUETOOTH_OFF		0x0D
> +
> +#define UNIWILL_OSD_RF_ON			0x0E
> +#define UNIWILL_OSD_RF_OFF			0x0F
> +
> +#define UNIWILL_OSD_3G_ON			0x10
> +#define UNIWILL_OSD_3G_OFF			0x11
> +
> +#define UNIWILL_OSD_WEBCAM_ON			0x12
> +#define UNIWILL_OSD_WEBCAM_OFF			0x13
> +
> +#define UNIWILL_OSD_BRIGHTNESSUP		0x14
> +#define UNIWILL_OSD_BRIGHTNESSDOWN		0x15
> +
> +#define UNIWILL_OSD_RADIOON			0x1A
> +#define UNIWILL_OSD_RADIOOFF			0x1B
> +
> +#define UNIWILL_OSD_POWERSAVE_ON		0x31
> +#define UNIWILL_OSD_POWERSAVE_OFF		0x32
> +
> +#define UNIWILL_OSD_MENU			0x34
> +
> +#define UNIWILL_OSD_MUTE			0x35
> +#define UNIWILL_OSD_VOLUMEDOWN			0x36
> +#define UNIWILL_OSD_VOLUMEUP			0x37
> +
> +#define UNIWILL_OSD_MENU_2			0x38
> +
> +#define UNIWILL_OSD_LIGHTBAR_ON			0x39
> +#define UNIWILL_OSD_LIGHTBAR_OFF		0x3A
> +
> +#define UNIWILL_OSD_KB_LED_LEVEL0		0x3B
> +#define UNIWILL_OSD_KB_LED_LEVEL1		0x3C
> +#define UNIWILL_OSD_KB_LED_LEVEL2		0x3D
> +#define UNIWILL_OSD_KB_LED_LEVEL3		0x3E
> +#define UNIWILL_OSD_KB_LED_LEVEL4		0x3F
> +
> +#define UNIWILL_OSD_SUPER_KEY_LOCK_ENABLE	0x40
> +#define UNIWILL_OSD_SUPER_KEY_LOCK_DISABLE	0x41
> +
> +#define UNIWILL_OSD_MENU_JP			0x42
> +
> +#define UNIWILL_OSD_CAMERA_ON			0x90
> +#define UNIWILL_OSD_CAMERA_OFF			0x91
> +
> +#define UNIWILL_OSD_RFKILL			0xA4
> +
> +#define UNIWILL_OSD_SUPER_KEY_LOCK_CHANGED	0xA5
> +
> +#define UNIWILL_OSD_LIGHTBAR_STATE_CHANGED	0xA6
> +
> +#define UNIWILL_OSD_FAN_BOOST_STATE_CHANGED	0xA7
> +
> +#define UNIWILL_OSD_LCD_SW			0xA9
> +
> +#define UNIWILL_OSD_FAN_OVERTEMP		0xAA
> +
> +#define UNIWILL_OSD_DC_ADAPTER_CHANGED		0xAB
> +
> +#define UNIWILL_OSD_BAT_HP_OFF			0xAC
> +
> +#define UNIWILL_OSD_FAN_DOWN_TEMP		0xAD
> +
> +#define UNIWILL_OSD_BATTERY_ALERT		0xAE
> +
> +#define UNIWILL_OSD_TIMAP_HAIERLB_SW		0xAF
> +
> +#define UNIWILL_OSD_PERFORMANCE_MODE_TOGGLE	0xB0
> +
> +#define UNIWILL_OSD_KBDILLUMDOWN		0xB1
> +#define UNIWILL_OSD_KBDILLUMUP			0xB2
> +
> +#define UNIWILL_OSD_BACKLIGHT_LEVEL_CHANGE	0xB3
> +#define UNIWILL_OSD_BACKLIGHT_POWER_CHANGE	0xB4
> +
> +#define UNIWILL_OSD_MIC_MUTE			0xB7
> +
> +#define UNIWILL_OSD_FN_LOCK			0xB8
> +#define UNIWILL_OSD_KBDILLUMTOGGLE		0xB9
> +
> +#define UNIWILL_OSD_BAT_CHARGE_FULL_24_H	0xBE
> +
> +#define UNIWILL_OSD_BAT_ERM_UPDATE		0xBF
> +
> +#define UNIWILL_OSD_BENCHMARK_MODE_TOGGLE	0xC0
> +
> +#define UNIWILL_OSD_KBD_BACKLIGHT_CHANGED	0xF0
> +
> +struct device;
> +struct notifier_block;
> +
> +int devm_uniwill_wmi_register_notifier(struct device *dev, struct notifier_block *nb);
> +
> +int __init uniwill_wmi_register_driver(void);
> +
> +void __exit uniwill_wmi_unregister_driver(void);
> +
> +#endif /* UNIWILL_WMI_H */

Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ