linux-hwmon.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH v6 0/7] platform/x86: lenovo-wmi-{capdata,other}: Add HWMON for fan speed
@ 2025-11-22 18:44 Rong Zhang
  2025-11-22 18:44 ` [PATCH v6 1/7] platform/x86: lenovo-wmi-helpers: Convert returned buffer into u32 Rong Zhang
                   ` (6 more replies)
  0 siblings, 7 replies; 22+ messages in thread
From: Rong Zhang @ 2025-11-22 18:44 UTC (permalink / raw)
  To: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Ilpo Järvinen
  Cc: Rong Zhang, Guenter Roeck, platform-driver-x86, linux-kernel,
	linux-hwmon

Lenovo WMI Other Mode interface also supports querying or setting fan
speed RPM. This capability is decribed by LENOVO_CAPABILITY_DATA_00.
Besides, LENOVO_FAN_TEST_DATA provides reference data for self-test of
cooling fans, including minimum and maximum fan speed RPM.

This patchset turns lenovo-wmi-capdata01 into a unified driver (now
named lenovo-wmi-capdata) for LENOVO_CAPABILITY_DATA_{00,01} and
LENOVO_FAN_TEST_DATA; then adds HWMON support for lenovo-wmi-other:

 - fanX_enable: enable/disable the fan (tunable)
 - fanX_input: current RPM
 - fanX_max: maximum RPM
 - fanX_min: minimum RPM
 - fanX_target: target RPM (tunable)

LENOVO_CAPABILITY_DATA_{00,01} presents on all devices, so
both binds to lenovo-wmi-other. However, some device does not have
LENOVO_FAN_TEST_DATA and its presence is described by
LENOVO_CAPABILITY_DATA_00; hence, the former binds to the latter and a
callback is used to pass the data to lenovo-wmi-other.

Summarizing this scheme:

        lenovo-wmi-other <-> capdata00 <-> capdata_fan
        |- master            |- component
                             |- sub-master
                                           |- sub-component

The callback will be called once both the master and the sub-component
are bound to the sub-master (component).

This scheme is essential to solve these issues:
- The component framework only supports one aggregation per master
- A binding is only established until all components are found
- The Fan Test Data interface may be missing on some devices
- To get rid of queries for the presense of WMI GUIDs

capdata00 is registered as a component and a sub-master on probe,
instead of chaining the registerations in one's bind callback. This is
because calling (un)registration methods of the component framework
causes deadlock in (un)bind callbacks, i.e., it's impossible to register
capdata00 as a sub-master/component in its component/sub-master bind
callback, and vice versa.

The implementation does not rely on a specific binding sequence. This
has been fuzz-tested using:

	#!/bin/bash

	DRV_DIR=/sys/bus/wmi/drivers/lenovo_wmi_cd
	CAPDATA_GUIDS=(
		$(find "$DRV_DIR"/ -name '*-*-*-*-*-*' -printf "%f ")
	)

	b() { sudo tee "$DRV_DIR"/bind <<<"$1"; }
	u() { sudo tee "$DRV_DIR"/unbind <<<"$1"; }

	for guid in "${CAPDATA_GUIDS[@]}"; do
		u "$guid"
	done

	while read -rsa perm; do
		for guid in "${perm[@]}"; do
			b "$guid"
		done
		sensors | grep -A3 lenovo_wmi_other || true
		for guid in "${perm[@]}"; do
			u "$guid"
		done
	done < <(python3 -c "
	from itertools import permutations
	ps = permutations('${CAPDATA_GUIDS[*]}'.split())
	for p in ps: print(' '.join(p))")

	for guid in "${CAPDATA_GUIDS[@]}"; do
		b "$guid"
	done

Tested on ThinkBook 14 G7+ ASP.

Changes in v6:
- Fix mistaken error paths
- Link to v5: https://lore.kernel.org/r/20251114175927.52533-1-i@rong.moe/

Changes in v5:
- Do not cast pointer to non-pointer or vice versa (thanks kernel test
  robot)
- Fix missing include (ditto)
- Link to v4: https://lore.kernel.org/r/20251113191152.96076-1-i@rong.moe/

Changes in v4:
- Get rid of wmi_has_guid() (thanks Armin Wolf's inspiration)
  - Add [PATCH v4 6/7], please review & test
    - Check 0x04050000.supported and bind capdata_fan to capdata00
  - Rework HWMON registration
    - Collect fan info from capdata00 and capdata_fan separately
    - Use a callback to collect fan info from capdata_fan
    - Trigger HWMON registration only if all fan info is collected
    - Do not check 0x04050000.supported, implied by the presense of
      capdata_fan
- Drop Reviewed-by & Tested-by from [PATCH v4 7/7] due to the changes,
  please review & test
- Link to v3: https://lore.kernel.org/r/20251031155349.24693-1-i@rong.moe/

Changes in v3:
- Fix grammar (thanks Derek J. Clark)
- Link to v2: https://lore.kernel.org/r/20251030193955.107148-1-i@rong.moe/

Changes in v2:
- Add a workaround for ACPI methods that return a 4B buffer for u32
  (thanks Armin Wolf)
- Fix function documentation (thanks kernel test bot)
- Reword documentation (thanks Derek J. Clark)
- Squash min/max reporting patch into the initial HWMON one (ditto)
- Query 0x04050000 for interface availability (ditto)
  - New parameter "expose_all_fans" to skip this check
- Enforce min/max RPM constraint on set (ditto)
  - New parameter "relax_fan_constraint" to disable this behavior
  - Drop parameter "ignore_fan_cap", superseded by the next one
  - New parameter "expose_all_fans" to expose fans w/o such data
- Assume auto mode on probe (ditto)
- Do not register HWMON device if no fan can be exposed
- fanX_target: Return -EBUSY instead of raw target value when fan stops
- Link to v1: https://lore.kernel.org/r/20251019210450.88830-1-i@rong.moe/

Rong Zhang (7):
  platform/x86: lenovo-wmi-helpers: Convert returned buffer into u32
  platform/x86: Rename lenovo-wmi-capdata01 to lenovo-wmi-capdata
  platform/x86: lenovo-wmi-{capdata,other}: Support multiple Capability
    Data
  platform/x86: lenovo-wmi-capdata: Add support for Capability Data 00
  platform/x86: lenovo-wmi-capdata: Add support for Fan Test Data
  platform/x86: lenovo-wmi-capdata: Wire up Fan Test Data
  platform/x86: lenovo-wmi-other: Add HWMON for fan reporting/tuning

 .../wmi/devices/lenovo-wmi-other.rst          |  43 +-
 drivers/platform/x86/lenovo/Kconfig           |   5 +-
 drivers/platform/x86/lenovo/Makefile          |   2 +-
 drivers/platform/x86/lenovo/wmi-capdata.c     | 811 ++++++++++++++++++
 drivers/platform/x86/lenovo/wmi-capdata.h     |  65 ++
 drivers/platform/x86/lenovo/wmi-capdata01.c   | 302 -------
 drivers/platform/x86/lenovo/wmi-capdata01.h   |  25 -
 drivers/platform/x86/lenovo/wmi-helpers.c     |  22 +-
 drivers/platform/x86/lenovo/wmi-other.c       | 511 ++++++++++-
 9 files changed, 1429 insertions(+), 357 deletions(-)
 create mode 100644 drivers/platform/x86/lenovo/wmi-capdata.c
 create mode 100644 drivers/platform/x86/lenovo/wmi-capdata.h
 delete mode 100644 drivers/platform/x86/lenovo/wmi-capdata01.c
 delete mode 100644 drivers/platform/x86/lenovo/wmi-capdata01.h


base-commit: 2eba5e05d9bcf4cdea995ed51b0f07ba0275794a
-- 
2.51.0


^ permalink raw reply	[flat|nested] 22+ messages in thread

* [PATCH v6 1/7] platform/x86: lenovo-wmi-helpers: Convert returned buffer into u32
  2025-11-22 18:44 [PATCH v6 0/7] platform/x86: lenovo-wmi-{capdata,other}: Add HWMON for fan speed Rong Zhang
@ 2025-11-22 18:44 ` Rong Zhang
  2025-11-22 18:44 ` [PATCH v6 2/7] platform/x86: Rename lenovo-wmi-capdata01 to lenovo-wmi-capdata Rong Zhang
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 22+ messages in thread
From: Rong Zhang @ 2025-11-22 18:44 UTC (permalink / raw)
  To: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Ilpo Järvinen
  Cc: Rong Zhang, Guenter Roeck, platform-driver-x86, linux-kernel,
	linux-hwmon

The Windows WMI-ACPI driver converts all ACPI objects into a common
buffer format, so returning a buffer with four bytes will look like an
integer for WMI consumers under Windows.

Therefore, some devices may simply implement the corresponding ACPI
methods to always return a buffer. While lwmi_dev_evaluate_int() expects
an integer (u32), convert returned >=4B buffer into u32 to support these
devices.

Suggested-by: Armin Wolf <W_Armin@gmx.de>
Link: https://lore.kernel.org/r/f1787927-b655-4321-b9d9-bc12353c72db@gmx.de/
Signed-off-by: Rong Zhang <i@rong.moe>
Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
Reviewed-by: Armin Wolf <W_Armin@gmx.de>
---
Changes in v4:
- Accept oversized buffer (thanks Armin Wolf)
- Use get_unaligned_le32() (ditto)

Changes in v2:
- New patch (thanks Armin Wolf)
---
 drivers/platform/x86/lenovo/wmi-helpers.c | 22 +++++++++++++++++++---
 1 file changed, 19 insertions(+), 3 deletions(-)

diff --git a/drivers/platform/x86/lenovo/wmi-helpers.c b/drivers/platform/x86/lenovo/wmi-helpers.c
index f6fef6296251..ea0b75334c9e 100644
--- a/drivers/platform/x86/lenovo/wmi-helpers.c
+++ b/drivers/platform/x86/lenovo/wmi-helpers.c
@@ -21,6 +21,7 @@
 #include <linux/errno.h>
 #include <linux/export.h>
 #include <linux/module.h>
+#include <linux/unaligned.h>
 #include <linux/wmi.h>
 
 #include "wmi-helpers.h"
@@ -59,10 +60,25 @@ int lwmi_dev_evaluate_int(struct wmi_device *wdev, u8 instance, u32 method_id,
 		if (!ret_obj)
 			return -ENODATA;
 
-		if (ret_obj->type != ACPI_TYPE_INTEGER)
-			return -ENXIO;
+		switch (ret_obj->type) {
+		/*
+		 * The ACPI method may simply return a buffer when a u32
+		 * is expected. This is valid on Windows as its WMI-ACPI
+		 * driver converts everything to a common buffer.
+		 */
+		case ACPI_TYPE_BUFFER: {
+			if (ret_obj->buffer.length >= 4)
+				return -ENXIO;
 
-		*retval = (u32)ret_obj->integer.value;
+			*retval = get_unaligned_le32(ret_obj->buffer.pointer);
+			return 0;
+		}
+		case ACPI_TYPE_INTEGER:
+			*retval = (u32)ret_obj->integer.value;
+			return 0;
+		default:
+			return -ENXIO;
+		}
 	}
 
 	return 0;
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v6 2/7] platform/x86: Rename lenovo-wmi-capdata01 to lenovo-wmi-capdata
  2025-11-22 18:44 [PATCH v6 0/7] platform/x86: lenovo-wmi-{capdata,other}: Add HWMON for fan speed Rong Zhang
  2025-11-22 18:44 ` [PATCH v6 1/7] platform/x86: lenovo-wmi-helpers: Convert returned buffer into u32 Rong Zhang
@ 2025-11-22 18:44 ` Rong Zhang
  2025-11-22 18:44 ` [PATCH v6 3/7] platform/x86: lenovo-wmi-{capdata,other}: Support multiple Capability Data Rong Zhang
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 22+ messages in thread
From: Rong Zhang @ 2025-11-22 18:44 UTC (permalink / raw)
  To: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Ilpo Järvinen
  Cc: Rong Zhang, Guenter Roeck, platform-driver-x86, linux-kernel,
	linux-hwmon

Prepare for the upcoming changes to make it suitable to retrieve
and provide other Capability Data as well.

Signed-off-by: Rong Zhang <i@rong.moe>
Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
 drivers/platform/x86/lenovo/Kconfig           |   4 +-
 drivers/platform/x86/lenovo/Makefile          |   2 +-
 .../lenovo/{wmi-capdata01.c => wmi-capdata.c} | 124 +++++++++---------
 .../lenovo/{wmi-capdata01.h => wmi-capdata.h} |  10 +-
 drivers/platform/x86/lenovo/wmi-other.c       |  11 +-
 5 files changed, 78 insertions(+), 73 deletions(-)
 rename drivers/platform/x86/lenovo/{wmi-capdata01.c => wmi-capdata.c} (60%)
 rename drivers/platform/x86/lenovo/{wmi-capdata01.h => wmi-capdata.h} (60%)

diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
index d22b774e0236..fb96a0f908f0 100644
--- a/drivers/platform/x86/lenovo/Kconfig
+++ b/drivers/platform/x86/lenovo/Kconfig
@@ -233,7 +233,7 @@ config YT2_1380
 	  To compile this driver as a module, choose M here: the module will
 	  be called lenovo-yogabook.
 
-config LENOVO_WMI_DATA01
+config LENOVO_WMI_DATA
 	tristate
 	depends on ACPI_WMI
 
@@ -264,7 +264,7 @@ config LENOVO_WMI_TUNING
 	tristate "Lenovo Other Mode WMI Driver"
 	depends on ACPI_WMI
 	select FW_ATTR_CLASS
-	select LENOVO_WMI_DATA01
+	select LENOVO_WMI_DATA
 	select LENOVO_WMI_EVENTS
 	select LENOVO_WMI_HELPERS
 	help
diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile
index 7b2128e3a214..29014d8c1376 100644
--- a/drivers/platform/x86/lenovo/Makefile
+++ b/drivers/platform/x86/lenovo/Makefile
@@ -12,7 +12,7 @@ lenovo-target-$(CONFIG_LENOVO_YMC)	+= ymc.o
 lenovo-target-$(CONFIG_YOGABOOK)	+= yogabook.o
 lenovo-target-$(CONFIG_YT2_1380)	+= yoga-tab2-pro-1380-fastcharger.o
 lenovo-target-$(CONFIG_LENOVO_WMI_CAMERA)	+= wmi-camera.o
-lenovo-target-$(CONFIG_LENOVO_WMI_DATA01)	+= wmi-capdata01.o
+lenovo-target-$(CONFIG_LENOVO_WMI_DATA)		+= wmi-capdata.o
 lenovo-target-$(CONFIG_LENOVO_WMI_EVENTS)	+= wmi-events.o
 lenovo-target-$(CONFIG_LENOVO_WMI_HELPERS)	+= wmi-helpers.o
 lenovo-target-$(CONFIG_LENOVO_WMI_GAMEZONE)	+= wmi-gamezone.o
diff --git a/drivers/platform/x86/lenovo/wmi-capdata01.c b/drivers/platform/x86/lenovo/wmi-capdata.c
similarity index 60%
rename from drivers/platform/x86/lenovo/wmi-capdata01.c
rename to drivers/platform/x86/lenovo/wmi-capdata.c
index fc7e3454e71d..c5e74b2bfeb3 100644
--- a/drivers/platform/x86/lenovo/wmi-capdata01.c
+++ b/drivers/platform/x86/lenovo/wmi-capdata.c
@@ -1,14 +1,17 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
 /*
- * Lenovo Capability Data 01 WMI Data Block driver.
+ * Lenovo Capability Data WMI Data Block driver.
  *
- * Lenovo Capability Data 01 provides information on tunable attributes used by
- * the "Other Mode" WMI interface. The data includes if the attribute is
- * supported by the hardware, the default_value, max_value, min_value, and step
- * increment. Each attribute has multiple pages, one for each of the thermal
- * modes managed by the Gamezone interface.
+ * Lenovo Capability Data provides information on tunable attributes used by
+ * the "Other Mode" WMI interface.
+ *
+ * Capability Data 01 includes if the attribute is supported by the hardware,
+ * and the default_value, max_value, min_value, and step increment. Each
+ * attribute has multiple pages, one for each of the thermal modes managed by
+ * the Gamezone interface.
  *
  * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
+ *   - Initial implementation (formerly named lenovo-wmi-capdata01)
  */
 
 #include <linux/acpi.h>
@@ -26,55 +29,55 @@
 #include <linux/types.h>
 #include <linux/wmi.h>
 
-#include "wmi-capdata01.h"
+#include "wmi-capdata.h"
 
 #define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154"
 
 #define ACPI_AC_CLASS "ac_adapter"
 #define ACPI_AC_NOTIFY_STATUS 0x80
 
-struct lwmi_cd01_priv {
+struct lwmi_cd_priv {
 	struct notifier_block acpi_nb; /* ACPI events */
 	struct wmi_device *wdev;
-	struct cd01_list *list;
+	struct cd_list *list;
 };
 
-struct cd01_list {
+struct cd_list {
 	struct mutex list_mutex; /* list R/W mutex */
 	u8 count;
 	struct capdata01 data[];
 };
 
 /**
- * lwmi_cd01_component_bind() - Bind component to master device.
- * @cd01_dev: Pointer to the lenovo-wmi-capdata01 driver parent device.
+ * lwmi_cd_component_bind() - Bind component to master device.
+ * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
  * @om_dev: Pointer to the lenovo-wmi-other driver parent device.
- * @data: capdata01_list object pointer used to return the capability data.
+ * @data: cd_list object pointer used to return the capability data.
  *
- * On lenovo-wmi-other's master bind, provide a pointer to the local capdata01
- * list. This is used to call lwmi_cd01_get_data to look up attribute data
+ * On lenovo-wmi-other's master bind, provide a pointer to the local capdata
+ * list. This is used to call lwmi_cd*_get_data to look up attribute data
  * from the lenovo-wmi-other driver.
  *
  * Return: 0
  */
-static int lwmi_cd01_component_bind(struct device *cd01_dev,
-				    struct device *om_dev, void *data)
+static int lwmi_cd_component_bind(struct device *cd_dev,
+				  struct device *om_dev, void *data)
 {
-	struct lwmi_cd01_priv *priv = dev_get_drvdata(cd01_dev);
-	struct cd01_list **cd01_list = data;
+	struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
+	struct cd_list **cd_list = data;
 
-	*cd01_list = priv->list;
+	*cd_list = priv->list;
 
 	return 0;
 }
 
-static const struct component_ops lwmi_cd01_component_ops = {
-	.bind = lwmi_cd01_component_bind,
+static const struct component_ops lwmi_cd_component_ops = {
+	.bind = lwmi_cd_component_bind,
 };
 
 /**
  * lwmi_cd01_get_data - Get the data of the specified attribute
- * @list: The lenovo-wmi-capdata01 pointer to its cd01_list struct.
+ * @list: The lenovo-wmi-capdata pointer to its cd_list struct.
  * @attribute_id: The capdata attribute ID to be found.
  * @output: Pointer to a capdata01 struct to return the data.
  *
@@ -83,7 +86,7 @@ static const struct component_ops lwmi_cd01_component_ops = {
  *
  * Return: 0 on success, or -EINVAL.
  */
-int lwmi_cd01_get_data(struct cd01_list *list, u32 attribute_id, struct capdata01 *output)
+int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output)
 {
 	u8 idx;
 
@@ -97,17 +100,17 @@ int lwmi_cd01_get_data(struct cd01_list *list, u32 attribute_id, struct capdata0
 
 	return -EINVAL;
 }
-EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD01");
+EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
 
 /**
- * lwmi_cd01_cache() - Cache all WMI data block information
- * @priv: lenovo-wmi-capdata01 driver data.
+ * lwmi_cd_cache() - Cache all WMI data block information
+ * @priv: lenovo-wmi-capdata driver data.
  *
  * Loop through each WMI data block and cache the data.
  *
  * Return: 0 on success, or an error.
  */
-static int lwmi_cd01_cache(struct lwmi_cd01_priv *priv)
+static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
 {
 	int idx;
 
@@ -131,17 +134,17 @@ static int lwmi_cd01_cache(struct lwmi_cd01_priv *priv)
 }
 
 /**
- * lwmi_cd01_alloc() - Allocate a cd01_list struct in drvdata
- * @priv: lenovo-wmi-capdata01 driver data.
+ * lwmi_cd_alloc() - Allocate a cd_list struct in drvdata
+ * @priv: lenovo-wmi-capdata driver data.
  *
- * Allocate a cd01_list struct large enough to contain data from all WMI data
+ * Allocate a cd_list struct large enough to contain data from all WMI data
  * blocks provided by the interface.
  *
  * Return: 0 on success, or an error.
  */
-static int lwmi_cd01_alloc(struct lwmi_cd01_priv *priv)
+static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
 {
-	struct cd01_list *list;
+	struct cd_list *list;
 	size_t list_size;
 	int count, ret;
 
@@ -163,28 +166,28 @@ static int lwmi_cd01_alloc(struct lwmi_cd01_priv *priv)
 }
 
 /**
- * lwmi_cd01_setup() - Cache all WMI data block information
- * @priv: lenovo-wmi-capdata01 driver data.
+ * lwmi_cd_setup() - Cache all WMI data block information
+ * @priv: lenovo-wmi-capdata driver data.
  *
- * Allocate a cd01_list struct large enough to contain data from all WMI data
+ * Allocate a cd_list struct large enough to contain data from all WMI data
  * blocks provided by the interface. Then loop through each data block and
  * cache the data.
  *
  * Return: 0 on success, or an error code.
  */
-static int lwmi_cd01_setup(struct lwmi_cd01_priv *priv)
+static int lwmi_cd_setup(struct lwmi_cd_priv *priv)
 {
 	int ret;
 
-	ret = lwmi_cd01_alloc(priv);
+	ret = lwmi_cd_alloc(priv);
 	if (ret)
 		return ret;
 
-	return lwmi_cd01_cache(priv);
+	return lwmi_cd_cache(priv);
 }
 
 /**
- * lwmi_cd01_notifier_call() - Call method for lenovo-wmi-capdata01 driver notifier.
+ * lwmi_cd01_notifier_call() - Call method for cd01 notifier.
  * block call chain.
  * @nb: The notifier_block registered to lenovo-wmi-events driver.
  * @action: Unused.
@@ -199,17 +202,17 @@ static int lwmi_cd01_notifier_call(struct notifier_block *nb, unsigned long acti
 				   void *data)
 {
 	struct acpi_bus_event *event = data;
-	struct lwmi_cd01_priv *priv;
+	struct lwmi_cd_priv *priv;
 	int ret;
 
 	if (strcmp(event->device_class, ACPI_AC_CLASS) != 0)
 		return NOTIFY_DONE;
 
-	priv = container_of(nb, struct lwmi_cd01_priv, acpi_nb);
+	priv = container_of(nb, struct lwmi_cd_priv, acpi_nb);
 
 	switch (event->type) {
 	case ACPI_AC_NOTIFY_STATUS:
-		ret = lwmi_cd01_cache(priv);
+		ret = lwmi_cd_cache(priv);
 		if (ret)
 			return NOTIFY_BAD;
 
@@ -230,10 +233,9 @@ static void lwmi_cd01_unregister(void *data)
 	unregister_acpi_notifier(acpi_nb);
 }
 
-static int lwmi_cd01_probe(struct wmi_device *wdev, const void *context)
-
+static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
 {
-	struct lwmi_cd01_priv *priv;
+	struct lwmi_cd_priv *priv;
 	int ret;
 
 	priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
@@ -243,7 +245,7 @@ static int lwmi_cd01_probe(struct wmi_device *wdev, const void *context)
 	priv->wdev = wdev;
 	dev_set_drvdata(&wdev->dev, priv);
 
-	ret = lwmi_cd01_setup(priv);
+	ret = lwmi_cd_setup(priv);
 	if (ret)
 		return ret;
 
@@ -257,27 +259,27 @@ static int lwmi_cd01_probe(struct wmi_device *wdev, const void *context)
 	if (ret)
 		return ret;
 
-	return component_add(&wdev->dev, &lwmi_cd01_component_ops);
+	return component_add(&wdev->dev, &lwmi_cd_component_ops);
 }
 
-static void lwmi_cd01_remove(struct wmi_device *wdev)
+static void lwmi_cd_remove(struct wmi_device *wdev)
 {
-	component_del(&wdev->dev, &lwmi_cd01_component_ops);
+	component_del(&wdev->dev, &lwmi_cd_component_ops);
 }
 
-static const struct wmi_device_id lwmi_cd01_id_table[] = {
+static const struct wmi_device_id lwmi_cd_id_table[] = {
 	{ LENOVO_CAPABILITY_DATA_01_GUID, NULL },
 	{}
 };
 
-static struct wmi_driver lwmi_cd01_driver = {
+static struct wmi_driver lwmi_cd_driver = {
 	.driver = {
-		.name = "lenovo_wmi_cd01",
+		.name = "lenovo_wmi_cd",
 		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
 	},
-	.id_table = lwmi_cd01_id_table,
-	.probe = lwmi_cd01_probe,
-	.remove = lwmi_cd01_remove,
+	.id_table = lwmi_cd_id_table,
+	.probe = lwmi_cd_probe,
+	.remove = lwmi_cd_remove,
 	.no_singleton = true,
 };
 
@@ -290,13 +292,13 @@ static struct wmi_driver lwmi_cd01_driver = {
  */
 int lwmi_cd01_match(struct device *dev, void *data)
 {
-	return dev->driver == &lwmi_cd01_driver.driver;
+	return dev->driver == &lwmi_cd_driver.driver;
 }
-EXPORT_SYMBOL_NS_GPL(lwmi_cd01_match, "LENOVO_WMI_CD01");
+EXPORT_SYMBOL_NS_GPL(lwmi_cd01_match, "LENOVO_WMI_CD");
 
-module_wmi_driver(lwmi_cd01_driver);
+module_wmi_driver(lwmi_cd_driver);
 
-MODULE_DEVICE_TABLE(wmi, lwmi_cd01_id_table);
+MODULE_DEVICE_TABLE(wmi, lwmi_cd_id_table);
 MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
-MODULE_DESCRIPTION("Lenovo Capability Data 01 WMI Driver");
+MODULE_DESCRIPTION("Lenovo Capability Data WMI Driver");
 MODULE_LICENSE("GPL");
diff --git a/drivers/platform/x86/lenovo/wmi-capdata01.h b/drivers/platform/x86/lenovo/wmi-capdata.h
similarity index 60%
rename from drivers/platform/x86/lenovo/wmi-capdata01.h
rename to drivers/platform/x86/lenovo/wmi-capdata.h
index bd06c5751f68..2a4746e38ad4 100644
--- a/drivers/platform/x86/lenovo/wmi-capdata01.h
+++ b/drivers/platform/x86/lenovo/wmi-capdata.h
@@ -2,13 +2,13 @@
 
 /* Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com> */
 
-#ifndef _LENOVO_WMI_CAPDATA01_H_
-#define _LENOVO_WMI_CAPDATA01_H_
+#ifndef _LENOVO_WMI_CAPDATA_H_
+#define _LENOVO_WMI_CAPDATA_H_
 
 #include <linux/types.h>
 
 struct device;
-struct cd01_list;
+struct cd_list;
 
 struct capdata01 {
 	u32 id;
@@ -19,7 +19,7 @@ struct capdata01 {
 	u32 max_value;
 };
 
-int lwmi_cd01_get_data(struct cd01_list *list, u32 attribute_id, struct capdata01 *output);
+int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output);
 int lwmi_cd01_match(struct device *dev, void *data);
 
-#endif /* !_LENOVO_WMI_CAPDATA01_H_ */
+#endif /* !_LENOVO_WMI_CAPDATA_H_ */
diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
index 2a960b278f11..c6dc1b4cff84 100644
--- a/drivers/platform/x86/lenovo/wmi-other.c
+++ b/drivers/platform/x86/lenovo/wmi-other.c
@@ -34,7 +34,7 @@
 #include <linux/types.h>
 #include <linux/wmi.h>
 
-#include "wmi-capdata01.h"
+#include "wmi-capdata.h"
 #include "wmi-events.h"
 #include "wmi-gamezone.h"
 #include "wmi-helpers.h"
@@ -74,7 +74,10 @@ enum attribute_property {
 
 struct lwmi_om_priv {
 	struct component_master_ops *ops;
-	struct cd01_list *cd01_list; /* only valid after capdata01 bind */
+
+	/* only valid after capdata bind */
+	struct cd_list *cd01_list;
+
 	struct device *fw_attr_dev;
 	struct kset *fw_attr_kset;
 	struct notifier_block nb;
@@ -576,7 +579,7 @@ static void lwmi_om_fw_attr_remove(struct lwmi_om_priv *priv)
 static int lwmi_om_master_bind(struct device *dev)
 {
 	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
-	struct cd01_list *tmp_list;
+	struct cd_list *tmp_list;
 	int ret;
 
 	ret = component_bind_all(dev, &tmp_list);
@@ -657,7 +660,7 @@ static struct wmi_driver lwmi_other_driver = {
 
 module_wmi_driver(lwmi_other_driver);
 
-MODULE_IMPORT_NS("LENOVO_WMI_CD01");
+MODULE_IMPORT_NS("LENOVO_WMI_CD");
 MODULE_IMPORT_NS("LENOVO_WMI_HELPERS");
 MODULE_DEVICE_TABLE(wmi, lwmi_other_id_table);
 MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v6 3/7] platform/x86: lenovo-wmi-{capdata,other}: Support multiple Capability Data
  2025-11-22 18:44 [PATCH v6 0/7] platform/x86: lenovo-wmi-{capdata,other}: Add HWMON for fan speed Rong Zhang
  2025-11-22 18:44 ` [PATCH v6 1/7] platform/x86: lenovo-wmi-helpers: Convert returned buffer into u32 Rong Zhang
  2025-11-22 18:44 ` [PATCH v6 2/7] platform/x86: Rename lenovo-wmi-capdata01 to lenovo-wmi-capdata Rong Zhang
@ 2025-11-22 18:44 ` Rong Zhang
  2025-11-24 16:30   ` Ilpo Järvinen
  2025-11-22 18:44 ` [PATCH v6 4/7] platform/x86: lenovo-wmi-capdata: Add support for Capability Data 00 Rong Zhang
                   ` (3 subsequent siblings)
  6 siblings, 1 reply; 22+ messages in thread
From: Rong Zhang @ 2025-11-22 18:44 UTC (permalink / raw)
  To: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Ilpo Järvinen
  Cc: Rong Zhang, Guenter Roeck, platform-driver-x86, linux-kernel,
	linux-hwmon

The current implementation are heavily bound to capdata01. Rewrite it so
that it is suitable to utilize other Capability Data as well.

No functional change intended.

Signed-off-by: Rong Zhang <i@rong.moe>
Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
Changes in v6:
- Fix the error path of lwmi_cd_match_add_all()
  - IS_ERR(matchptr) => IS_ERR(*matchptr)

Changes in v5:
- Do not cast pointer to non-pointer or vice versa (thanks kernel test
  robot)

Changes in v4:
- Get rid of wmi_has_guid() (thanks Armin Wolf)
  - More changes in [PATCH v4 6/7]
- Prepare for [PATCH v4 6/7]
  - Move lwmi_cd_match*() forward
  - Use switch-case in lwmi_cd_remove()

Changes in v2:
- Fix function parameter 'type' not described in 'lwmi_cd_match' (thanks
  kernel test bot)
---
 drivers/platform/x86/lenovo/wmi-capdata.c | 225 +++++++++++++++++-----
 drivers/platform/x86/lenovo/wmi-capdata.h |   7 +-
 drivers/platform/x86/lenovo/wmi-other.c   |  16 +-
 3 files changed, 189 insertions(+), 59 deletions(-)

diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
index c5e74b2bfeb3..e0710354278a 100644
--- a/drivers/platform/x86/lenovo/wmi-capdata.c
+++ b/drivers/platform/x86/lenovo/wmi-capdata.c
@@ -12,8 +12,13 @@
  *
  * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
  *   - Initial implementation (formerly named lenovo-wmi-capdata01)
+ *
+ * Copyright (C) 2025 Rong Zhang <i@rong.moe>
+ *   - Unified implementation
  */
 
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
 #include <linux/acpi.h>
 #include <linux/cleanup.h>
 #include <linux/component.h>
@@ -36,6 +41,23 @@
 #define ACPI_AC_CLASS "ac_adapter"
 #define ACPI_AC_NOTIFY_STATUS 0x80
 
+enum lwmi_cd_type {
+	LENOVO_CAPABILITY_DATA_01,
+};
+
+#define LWMI_CD_TABLE_ITEM(_type)		\
+	[_type] = {				\
+		.name = #_type,			\
+		.type = _type,			\
+	}
+
+static const struct lwmi_cd_info {
+	const char *name;
+	enum lwmi_cd_type type;
+} lwmi_cd_table[] = {
+	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
+};
+
 struct lwmi_cd_priv {
 	struct notifier_block acpi_nb; /* ACPI events */
 	struct wmi_device *wdev;
@@ -44,15 +66,63 @@ struct lwmi_cd_priv {
 
 struct cd_list {
 	struct mutex list_mutex; /* list R/W mutex */
+	enum lwmi_cd_type type;
 	u8 count;
-	struct capdata01 data[];
+
+	union {
+		DECLARE_FLEX_ARRAY(struct capdata01, cd01);
+	};
 };
 
+static struct wmi_driver lwmi_cd_driver;
+
+/**
+ * lwmi_cd_match() - Match rule for the master driver.
+ * @dev: Pointer to the capability data parent device.
+ * @type: Pointer to capability data type (enum lwmi_cd_type *) to match.
+ *
+ * Return: int.
+ */
+static int lwmi_cd_match(struct device *dev, void *type)
+{
+	struct lwmi_cd_priv *priv;
+
+	if (dev->driver != &lwmi_cd_driver.driver)
+		return false;
+
+	priv = dev_get_drvdata(dev);
+	return priv->list->type == *(enum lwmi_cd_type *)type;
+}
+
+/**
+ * lwmi_cd_match_add_all() - Add all match rule for the master driver.
+ * @master: Pointer to the master device.
+ * @matchptr: Pointer to the returned component_match pointer.
+ *
+ * Adds all component matches to the list stored in @matchptr for the @master
+ * device. @matchptr must be initialized to NULL.
+ */
+void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr)
+{
+	int i;
+
+	if (WARN_ON(*matchptr))
+		return;
+
+	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
+		component_match_add(master, matchptr, lwmi_cd_match,
+				    (void *)&lwmi_cd_table[i].type);
+		if (IS_ERR(*matchptr))
+			return;
+	}
+}
+EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
+
 /**
  * lwmi_cd_component_bind() - Bind component to master device.
  * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
  * @om_dev: Pointer to the lenovo-wmi-other driver parent device.
- * @data: cd_list object pointer used to return the capability data.
+ * @data: lwmi_cd_binder object pointer used to return the capability data.
  *
  * On lenovo-wmi-other's master bind, provide a pointer to the local capdata
  * list. This is used to call lwmi_cd*_get_data to look up attribute data
@@ -64,9 +134,15 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
 				  struct device *om_dev, void *data)
 {
 	struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
-	struct cd_list **cd_list = data;
+	struct lwmi_cd_binder *binder = data;
 
-	*cd_list = priv->list;
+	switch (priv->list->type) {
+	case LENOVO_CAPABILITY_DATA_01:
+		binder->cd01_list = priv->list;
+		break;
+	default:
+		return -EINVAL;
+	}
 
 	return 0;
 }
@@ -76,30 +152,33 @@ static const struct component_ops lwmi_cd_component_ops = {
 };
 
 /**
- * lwmi_cd01_get_data - Get the data of the specified attribute
+ * lwmi_cd*_get_data - Get the data of the specified attribute
  * @list: The lenovo-wmi-capdata pointer to its cd_list struct.
  * @attribute_id: The capdata attribute ID to be found.
- * @output: Pointer to a capdata01 struct to return the data.
+ * @output: Pointer to a capdata* struct to return the data.
  *
- * Retrieves the capability data 01 struct pointer for the given
- * attribute for its specified thermal mode.
+ * Retrieves the capability data struct pointer for the given
+ * attribute.
  *
  * Return: 0 on success, or -EINVAL.
  */
-int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output)
-{
-	u8 idx;
-
-	guard(mutex)(&list->list_mutex);
-	for (idx = 0; idx < list->count; idx++) {
-		if (list->data[idx].id != attribute_id)
-			continue;
-		memcpy(output, &list->data[idx], sizeof(list->data[idx]));
-		return 0;
+#define DEF_LWMI_CDXX_GET_DATA(_cdxx, _cd_type, _output_t)					\
+	int lwmi_##_cdxx##_get_data(struct cd_list *list, u32 attribute_id, _output_t *output)	\
+	{											\
+		u8 idx;										\
+		if (WARN_ON(list->type != _cd_type))						\
+			return -EINVAL;								\
+		guard(mutex)(&list->list_mutex);						\
+		for (idx = 0; idx < list->count; idx++) {					\
+			if (list->_cdxx[idx].id != attribute_id)				\
+				continue;							\
+			memcpy(output, &list->_cdxx[idx], sizeof(list->_cdxx[idx]));		\
+			return 0;								\
+		}										\
+		return -EINVAL;									\
 	}
 
-	return -EINVAL;
-}
+DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
 EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
 
 /**
@@ -112,10 +191,21 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
  */
 static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
 {
+	size_t size;
 	int idx;
+	void *p;
+
+	switch (priv->list->type) {
+	case LENOVO_CAPABILITY_DATA_01:
+		p = &priv->list->cd01[0];
+		size = sizeof(priv->list->cd01[0]);
+		break;
+	default:
+		return -EINVAL;
+	}
 
 	guard(mutex)(&priv->list->list_mutex);
-	for (idx = 0; idx < priv->list->count; idx++) {
+	for (idx = 0; idx < priv->list->count; idx++, p += size) {
 		union acpi_object *ret_obj __free(kfree) = NULL;
 
 		ret_obj = wmidev_block_query(priv->wdev, idx);
@@ -123,11 +213,10 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
 			return -ENODEV;
 
 		if (ret_obj->type != ACPI_TYPE_BUFFER ||
-		    ret_obj->buffer.length < sizeof(priv->list->data[idx]))
+		    ret_obj->buffer.length < size)
 			continue;
 
-		memcpy(&priv->list->data[idx], ret_obj->buffer.pointer,
-		       ret_obj->buffer.length);
+		memcpy(p, ret_obj->buffer.pointer, size);
 	}
 
 	return 0;
@@ -136,20 +225,28 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
 /**
  * lwmi_cd_alloc() - Allocate a cd_list struct in drvdata
  * @priv: lenovo-wmi-capdata driver data.
+ * @type: The type of capability data.
  *
  * Allocate a cd_list struct large enough to contain data from all WMI data
  * blocks provided by the interface.
  *
  * Return: 0 on success, or an error.
  */
-static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
+static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
 {
 	struct cd_list *list;
 	size_t list_size;
 	int count, ret;
 
 	count = wmidev_instance_count(priv->wdev);
-	list_size = struct_size(list, data, count);
+
+	switch (type) {
+	case LENOVO_CAPABILITY_DATA_01:
+		list_size = struct_size(list, cd01, count);
+		break;
+	default:
+		return -EINVAL;
+	}
 
 	list = devm_kzalloc(&priv->wdev->dev, list_size, GFP_KERNEL);
 	if (!list)
@@ -159,6 +256,7 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
 	if (ret)
 		return ret;
 
+	list->type = type;
 	list->count = count;
 	priv->list = list;
 
@@ -168,6 +266,7 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
 /**
  * lwmi_cd_setup() - Cache all WMI data block information
  * @priv: lenovo-wmi-capdata driver data.
+ * @type: The type of capability data.
  *
  * Allocate a cd_list struct large enough to contain data from all WMI data
  * blocks provided by the interface. Then loop through each data block and
@@ -175,11 +274,11 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
  *
  * Return: 0 on success, or an error code.
  */
-static int lwmi_cd_setup(struct lwmi_cd_priv *priv)
+static int lwmi_cd_setup(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
 {
 	int ret;
 
-	ret = lwmi_cd_alloc(priv);
+	ret = lwmi_cd_alloc(priv, type);
 	if (ret)
 		return ret;
 
@@ -235,9 +334,13 @@ static void lwmi_cd01_unregister(void *data)
 
 static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
 {
+	const struct lwmi_cd_info *info = context;
 	struct lwmi_cd_priv *priv;
 	int ret;
 
+	if (!info)
+		return -EINVAL;
+
 	priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
 	if (!priv)
 		return -ENOMEM;
@@ -245,30 +348,58 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
 	priv->wdev = wdev;
 	dev_set_drvdata(&wdev->dev, priv);
 
-	ret = lwmi_cd_setup(priv);
+	ret = lwmi_cd_setup(priv, info->type);
 	if (ret)
-		return ret;
+		goto out;
 
-	priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
+	switch (info->type) {
+	case LENOVO_CAPABILITY_DATA_01:
+		priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
 
-	ret = register_acpi_notifier(&priv->acpi_nb);
-	if (ret)
-		return ret;
+		ret = register_acpi_notifier(&priv->acpi_nb);
+		if (ret)
+			goto out;
 
-	ret = devm_add_action_or_reset(&wdev->dev, lwmi_cd01_unregister, &priv->acpi_nb);
-	if (ret)
-		return ret;
+		ret = devm_add_action_or_reset(&wdev->dev, lwmi_cd01_unregister,
+					       &priv->acpi_nb);
+		if (ret)
+			goto out;
 
-	return component_add(&wdev->dev, &lwmi_cd_component_ops);
+		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
+		goto out;
+	default:
+		return -EINVAL;
+	}
+out:
+	if (ret) {
+		dev_err(&wdev->dev, "failed to register %s: %d\n",
+			info->name, ret);
+	} else {
+		dev_info(&wdev->dev, "registered %s with %u items\n",
+			 info->name, priv->list->count);
+	}
+	return ret;
 }
 
 static void lwmi_cd_remove(struct wmi_device *wdev)
 {
-	component_del(&wdev->dev, &lwmi_cd_component_ops);
+	struct lwmi_cd_priv *priv = dev_get_drvdata(&wdev->dev);
+
+	switch (priv->list->type) {
+	case LENOVO_CAPABILITY_DATA_01:
+		component_del(&wdev->dev, &lwmi_cd_component_ops);
+		break;
+	default:
+		WARN_ON(1);
+	}
 }
 
+#define LWMI_CD_WDEV_ID(_type)				\
+	.guid_string = _type##_GUID,			\
+	.context = &lwmi_cd_table[_type]
+
 static const struct wmi_device_id lwmi_cd_id_table[] = {
-	{ LENOVO_CAPABILITY_DATA_01_GUID, NULL },
+	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_01) },
 	{}
 };
 
@@ -283,22 +414,10 @@ static struct wmi_driver lwmi_cd_driver = {
 	.no_singleton = true,
 };
 
-/**
- * lwmi_cd01_match() - Match rule for the master driver.
- * @dev: Pointer to the capability data 01 parent device.
- * @data: Unused void pointer for passing match criteria.
- *
- * Return: int.
- */
-int lwmi_cd01_match(struct device *dev, void *data)
-{
-	return dev->driver == &lwmi_cd_driver.driver;
-}
-EXPORT_SYMBOL_NS_GPL(lwmi_cd01_match, "LENOVO_WMI_CD");
-
 module_wmi_driver(lwmi_cd_driver);
 
 MODULE_DEVICE_TABLE(wmi, lwmi_cd_id_table);
 MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
+MODULE_AUTHOR("Rong Zhang <i@rong.moe>");
 MODULE_DESCRIPTION("Lenovo Capability Data WMI Driver");
 MODULE_LICENSE("GPL");
diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
index 2a4746e38ad4..d326f9d2d165 100644
--- a/drivers/platform/x86/lenovo/wmi-capdata.h
+++ b/drivers/platform/x86/lenovo/wmi-capdata.h
@@ -7,6 +7,7 @@
 
 #include <linux/types.h>
 
+struct component_match;
 struct device;
 struct cd_list;
 
@@ -19,7 +20,11 @@ struct capdata01 {
 	u32 max_value;
 };
 
+struct lwmi_cd_binder {
+	struct cd_list *cd01_list;
+};
+
+void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
 int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output);
-int lwmi_cd01_match(struct device *dev, void *data);
 
 #endif /* !_LENOVO_WMI_CAPDATA_H_ */
diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
index c6dc1b4cff84..f2e1e34d58a9 100644
--- a/drivers/platform/x86/lenovo/wmi-other.c
+++ b/drivers/platform/x86/lenovo/wmi-other.c
@@ -579,14 +579,14 @@ static void lwmi_om_fw_attr_remove(struct lwmi_om_priv *priv)
 static int lwmi_om_master_bind(struct device *dev)
 {
 	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
-	struct cd_list *tmp_list;
+	struct lwmi_cd_binder binder = { 0 };
 	int ret;
 
-	ret = component_bind_all(dev, &tmp_list);
+	ret = component_bind_all(dev, &binder);
 	if (ret)
 		return ret;
 
-	priv->cd01_list = tmp_list;
+	priv->cd01_list = binder.cd01_list;
 	if (!priv->cd01_list)
 		return -ENODEV;
 
@@ -623,10 +623,13 @@ static int lwmi_other_probe(struct wmi_device *wdev, const void *context)
 	if (!priv)
 		return -ENOMEM;
 
+	/* Sentinel for on-demand ida_free(). */
+	priv->ida_id = -EIDRM;
+
 	priv->wdev = wdev;
 	dev_set_drvdata(&wdev->dev, priv);
 
-	component_match_add(&wdev->dev, &master_match, lwmi_cd01_match, NULL);
+	lwmi_cd_match_add_all(&wdev->dev, &master_match);
 	if (IS_ERR(master_match))
 		return PTR_ERR(master_match);
 
@@ -639,7 +642,10 @@ static void lwmi_other_remove(struct wmi_device *wdev)
 	struct lwmi_om_priv *priv = dev_get_drvdata(&wdev->dev);
 
 	component_master_del(&wdev->dev, &lwmi_om_master_ops);
-	ida_free(&lwmi_om_ida, priv->ida_id);
+
+	/* No IDA to free if the driver is never bound to its components. */
+	if (priv->ida_id >= 0)
+		ida_free(&lwmi_om_ida, priv->ida_id);
 }
 
 static const struct wmi_device_id lwmi_other_id_table[] = {
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v6 4/7] platform/x86: lenovo-wmi-capdata: Add support for Capability Data 00
  2025-11-22 18:44 [PATCH v6 0/7] platform/x86: lenovo-wmi-{capdata,other}: Add HWMON for fan speed Rong Zhang
                   ` (2 preceding siblings ...)
  2025-11-22 18:44 ` [PATCH v6 3/7] platform/x86: lenovo-wmi-{capdata,other}: Support multiple Capability Data Rong Zhang
@ 2025-11-22 18:44 ` Rong Zhang
  2025-11-22 18:44 ` [PATCH v6 5/7] platform/x86: lenovo-wmi-capdata: Add support for Fan Test Data Rong Zhang
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 22+ messages in thread
From: Rong Zhang @ 2025-11-22 18:44 UTC (permalink / raw)
  To: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Ilpo Järvinen
  Cc: Rong Zhang, Guenter Roeck, platform-driver-x86, linux-kernel,
	linux-hwmon

Add support for LENOVO_CAPABILITY_DATA_00 WMI data block that comes on
"Other Mode" enabled hardware. Provides an interface for querying if a
given attribute is supported by the hardware, as well as its default
value.

capdata00 always presents on devices with capdata01. lenovo-wmi-other
now binds to both (no functional change intended).

Signed-off-by: Rong Zhang <i@rong.moe>
Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
Changes in v4:
- Rebase on top of changes made to [PATCH v4 3/7]

Changes in v2:
- Reword documentation (thanks Derek J. Clark)
---
 .../wmi/devices/lenovo-wmi-other.rst          | 15 ++++++++---
 drivers/platform/x86/lenovo/wmi-capdata.c     | 25 +++++++++++++++++++
 drivers/platform/x86/lenovo/wmi-capdata.h     |  8 ++++++
 3 files changed, 45 insertions(+), 3 deletions(-)

diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
index d7928b8dfb4b..fcad595d49af 100644
--- a/Documentation/wmi/devices/lenovo-wmi-other.rst
+++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
@@ -31,13 +31,22 @@ under the following path:
 
   /sys/class/firmware-attributes/lenovo-wmi-other/attributes/<attribute>/
 
+LENOVO_CAPABILITY_DATA_00
+-------------------------
+
+WMI GUID ``362A3AFE-3D96-4665-8530-96DAD5BB300E``
+
+The LENOVO_CAPABILITY_DATA_00 interface provides various information that
+does not rely on the gamezone thermal mode.
+
 LENOVO_CAPABILITY_DATA_01
 -------------------------
 
 WMI GUID ``7A8F5407-CB67-4D6E-B547-39B3BE018154``
 
-The LENOVO_CAPABILITY_DATA_01 interface provides information on various
-power limits of integrated CPU and GPU components.
+The LENOVO_CAPABILITY_DATA_01 interface provides various information that
+relies on the gamezone thermal mode, including power limits of integrated
+CPU and GPU components.
 
 Each attribute has the following properties:
  - current_value
@@ -48,7 +57,7 @@ Each attribute has the following properties:
  - scalar_increment
  - type
 
-The following attributes are implemented:
+The following firmware-attributes are implemented:
  - ppt_pl1_spl: Platform Profile Tracking Sustained Power Limit
  - ppt_pl2_sppt: Platform Profile Tracking Slow Package Power Tracking
  - ppt_pl3_fppt: Platform Profile Tracking Fast Package Power Tracking
diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
index e0710354278a..29267c373ab3 100644
--- a/drivers/platform/x86/lenovo/wmi-capdata.c
+++ b/drivers/platform/x86/lenovo/wmi-capdata.c
@@ -5,6 +5,9 @@
  * Lenovo Capability Data provides information on tunable attributes used by
  * the "Other Mode" WMI interface.
  *
+ * Capability Data 00 includes if the attribute is supported by the hardware,
+ * and the default_value. All attributes are independent of thermal modes.
+ *
  * Capability Data 01 includes if the attribute is supported by the hardware,
  * and the default_value, max_value, min_value, and step increment. Each
  * attribute has multiple pages, one for each of the thermal modes managed by
@@ -36,12 +39,14 @@
 
 #include "wmi-capdata.h"
 
+#define LENOVO_CAPABILITY_DATA_00_GUID "362A3AFE-3D96-4665-8530-96DAD5BB300E"
 #define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154"
 
 #define ACPI_AC_CLASS "ac_adapter"
 #define ACPI_AC_NOTIFY_STATUS 0x80
 
 enum lwmi_cd_type {
+	LENOVO_CAPABILITY_DATA_00,
 	LENOVO_CAPABILITY_DATA_01,
 };
 
@@ -55,6 +60,7 @@ static const struct lwmi_cd_info {
 	const char *name;
 	enum lwmi_cd_type type;
 } lwmi_cd_table[] = {
+	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_00),
 	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
 };
 
@@ -70,6 +76,7 @@ struct cd_list {
 	u8 count;
 
 	union {
+		DECLARE_FLEX_ARRAY(struct capdata00, cd00);
 		DECLARE_FLEX_ARRAY(struct capdata01, cd01);
 	};
 };
@@ -137,6 +144,9 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
 	struct lwmi_cd_binder *binder = data;
 
 	switch (priv->list->type) {
+	case LENOVO_CAPABILITY_DATA_00:
+		binder->cd00_list = priv->list;
+		break;
 	case LENOVO_CAPABILITY_DATA_01:
 		binder->cd01_list = priv->list;
 		break;
@@ -178,6 +188,9 @@ static const struct component_ops lwmi_cd_component_ops = {
 		return -EINVAL;									\
 	}
 
+DEF_LWMI_CDXX_GET_DATA(cd00, LENOVO_CAPABILITY_DATA_00, struct capdata00);
+EXPORT_SYMBOL_NS_GPL(lwmi_cd00_get_data, "LENOVO_WMI_CD");
+
 DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
 EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
 
@@ -196,6 +209,10 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
 	void *p;
 
 	switch (priv->list->type) {
+	case LENOVO_CAPABILITY_DATA_00:
+		p = &priv->list->cd00[0];
+		size = sizeof(priv->list->cd00[0]);
+		break;
 	case LENOVO_CAPABILITY_DATA_01:
 		p = &priv->list->cd01[0];
 		size = sizeof(priv->list->cd01[0]);
@@ -241,6 +258,9 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
 	count = wmidev_instance_count(priv->wdev);
 
 	switch (type) {
+	case LENOVO_CAPABILITY_DATA_00:
+		list_size = struct_size(list, cd00, count);
+		break;
 	case LENOVO_CAPABILITY_DATA_01:
 		list_size = struct_size(list, cd01, count);
 		break;
@@ -353,6 +373,9 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
 		goto out;
 
 	switch (info->type) {
+	case LENOVO_CAPABILITY_DATA_00:
+		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
+		goto out;
 	case LENOVO_CAPABILITY_DATA_01:
 		priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
 
@@ -386,6 +409,7 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
 	struct lwmi_cd_priv *priv = dev_get_drvdata(&wdev->dev);
 
 	switch (priv->list->type) {
+	case LENOVO_CAPABILITY_DATA_00:
 	case LENOVO_CAPABILITY_DATA_01:
 		component_del(&wdev->dev, &lwmi_cd_component_ops);
 		break;
@@ -399,6 +423,7 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
 	.context = &lwmi_cd_table[_type]
 
 static const struct wmi_device_id lwmi_cd_id_table[] = {
+	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_00) },
 	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_01) },
 	{}
 };
diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
index d326f9d2d165..a6d006ef458f 100644
--- a/drivers/platform/x86/lenovo/wmi-capdata.h
+++ b/drivers/platform/x86/lenovo/wmi-capdata.h
@@ -11,6 +11,12 @@ struct component_match;
 struct device;
 struct cd_list;
 
+struct capdata00 {
+	u32 id;
+	u32 supported;
+	u32 default_value;
+};
+
 struct capdata01 {
 	u32 id;
 	u32 supported;
@@ -21,10 +27,12 @@ struct capdata01 {
 };
 
 struct lwmi_cd_binder {
+	struct cd_list *cd00_list;
 	struct cd_list *cd01_list;
 };
 
 void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
+int lwmi_cd00_get_data(struct cd_list *list, u32 attribute_id, struct capdata00 *output);
 int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output);
 
 #endif /* !_LENOVO_WMI_CAPDATA_H_ */
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v6 5/7] platform/x86: lenovo-wmi-capdata: Add support for Fan Test Data
  2025-11-22 18:44 [PATCH v6 0/7] platform/x86: lenovo-wmi-{capdata,other}: Add HWMON for fan speed Rong Zhang
                   ` (3 preceding siblings ...)
  2025-11-22 18:44 ` [PATCH v6 4/7] platform/x86: lenovo-wmi-capdata: Add support for Capability Data 00 Rong Zhang
@ 2025-11-22 18:44 ` Rong Zhang
  2025-11-24 16:45   ` Ilpo Järvinen
  2025-11-22 18:44 ` [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up " Rong Zhang
  2025-11-22 18:44 ` [PATCH v6 7/7] platform/x86: lenovo-wmi-other: Add HWMON for fan reporting/tuning Rong Zhang
  6 siblings, 1 reply; 22+ messages in thread
From: Rong Zhang @ 2025-11-22 18:44 UTC (permalink / raw)
  To: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Ilpo Järvinen
  Cc: Rong Zhang, Guenter Roeck, platform-driver-x86, linux-kernel,
	linux-hwmon

Add support for LENOVO_FAN_TEST_DATA WMI data block. Provides an
interface for querying the min/max fan speed RPM (reference data) of a
given fan ID.

This interface is optional. Hence, it does not bind to lenovo-wmi-other
and is not registered as a component for the moment. Appropriate binding
will be implemented in the subsequent patch.

Signed-off-by: Rong Zhang <i@rong.moe>
Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
Changes in v4:
- Rebase on top of changes made to [PATCH v4 3/7]
- Do not register it as a component until [PATCH v4 6/7]

Changes in v2:
- Reword documentation
---
 .../wmi/devices/lenovo-wmi-other.rst          |  17 +++
 drivers/platform/x86/lenovo/wmi-capdata.c     | 102 ++++++++++++++++++
 drivers/platform/x86/lenovo/wmi-capdata.h     |   7 ++
 3 files changed, 126 insertions(+)

diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
index fcad595d49af..821282e07d93 100644
--- a/Documentation/wmi/devices/lenovo-wmi-other.rst
+++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
@@ -62,6 +62,13 @@ The following firmware-attributes are implemented:
  - ppt_pl2_sppt: Platform Profile Tracking Slow Package Power Tracking
  - ppt_pl3_fppt: Platform Profile Tracking Fast Package Power Tracking
 
+LENOVO_FAN_TEST_DATA
+-------------------------
+
+WMI GUID ``B642801B-3D21-45DE-90AE-6E86F164FB21``
+
+The LENOVO_FAN_TEST_DATA interface provides reference data for self-test of
+cooling fans.
 
 WMI interface description
 =========================
@@ -115,3 +122,13 @@ data using the `bmfdec <https://github.com/pali/bmfdec>`_ utility:
     [WmiDataId(3), read, Description("Data Size.")] uint32 DataSize;
     [WmiDataId(4), read, Description("Default Value"), WmiSizeIs("DataSize")] uint8 DefaultValue[];
   };
+
+  [WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"), Description("Definition of Fan Test Data"), guid("{B642801B-3D21-45DE-90AE-6E86F164FB21}")]
+  class LENOVO_FAN_TEST_DATA {
+    [key, read] string InstanceName;
+    [read] boolean Active;
+    [WmiDataId(1), read, Description("Mode.")] uint32 NumOfFans;
+    [WmiDataId(2), read, Description("Fan ID."), WmiSizeIs("NumOfFans")] uint32 FanId[];
+    [WmiDataId(3), read, Description("Maximum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMaxSpeed[];
+    [WmiDataId(4), read, Description("Minumum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMinSpeed[];
+  };
diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
index 29267c373ab3..e6392357395c 100644
--- a/drivers/platform/x86/lenovo/wmi-capdata.c
+++ b/drivers/platform/x86/lenovo/wmi-capdata.c
@@ -13,6 +13,10 @@
  * attribute has multiple pages, one for each of the thermal modes managed by
  * the Gamezone interface.
  *
+ * Fan Test Data includes the max/min fan speed RPM for each fan. This is
+ * reference data for self-test. If the fan is in good condition, it is capable
+ * to spin faster than max RPM or slower than min RPM.
+ *
  * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
  *   - Initial implementation (formerly named lenovo-wmi-capdata01)
  *
@@ -41,6 +45,7 @@
 
 #define LENOVO_CAPABILITY_DATA_00_GUID "362A3AFE-3D96-4665-8530-96DAD5BB300E"
 #define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154"
+#define LENOVO_FAN_TEST_DATA_GUID "B642801B-3D21-45DE-90AE-6E86F164FB21"
 
 #define ACPI_AC_CLASS "ac_adapter"
 #define ACPI_AC_NOTIFY_STATUS 0x80
@@ -48,6 +53,7 @@
 enum lwmi_cd_type {
 	LENOVO_CAPABILITY_DATA_00,
 	LENOVO_CAPABILITY_DATA_01,
+	LENOVO_FAN_TEST_DATA,
 };
 
 #define LWMI_CD_TABLE_ITEM(_type)		\
@@ -62,6 +68,7 @@ static const struct lwmi_cd_info {
 } lwmi_cd_table[] = {
 	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_00),
 	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
+	LWMI_CD_TABLE_ITEM(LENOVO_FAN_TEST_DATA),
 };
 
 struct lwmi_cd_priv {
@@ -78,6 +85,7 @@ struct cd_list {
 	union {
 		DECLARE_FLEX_ARRAY(struct capdata00, cd00);
 		DECLARE_FLEX_ARRAY(struct capdata01, cd01);
+		DECLARE_FLEX_ARRAY(struct capdata_fan, cd_fan);
 	};
 };
 
@@ -117,6 +125,10 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
 		return;
 
 	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
+		/* Skip optional interfaces. */
+		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
+			continue;
+
 		component_match_add(master, matchptr, lwmi_cd_match,
 				    (void *)&lwmi_cd_table[i].type);
 		if (IS_ERR(*matchptr))
@@ -194,6 +206,9 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd00_get_data, "LENOVO_WMI_CD");
 DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
 EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
 
+DEF_LWMI_CDXX_GET_DATA(cd_fan, LENOVO_FAN_TEST_DATA, struct capdata_fan);
+EXPORT_SYMBOL_NS_GPL(lwmi_cd_fan_get_data, "LENOVO_WMI_CD");
+
 /**
  * lwmi_cd_cache() - Cache all WMI data block information
  * @priv: lenovo-wmi-capdata driver data.
@@ -217,6 +232,9 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
 		p = &priv->list->cd01[0];
 		size = sizeof(priv->list->cd01[0]);
 		break;
+	case LENOVO_FAN_TEST_DATA:
+		/* Done by lwmi_cd_alloc() => lwmi_cd_fan_list_alloc_cache(). */
+		return 0;
 	default:
 		return -EINVAL;
 	}
@@ -239,6 +257,78 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
 	return 0;
 }
 
+/**
+ * lwmi_cd_fan_list_alloc_cache() - Alloc and cache Fan Test Data list
+ * @priv: lenovo-wmi-capdata driver data.
+ * @listptr: Pointer to returned cd_list pointer.
+ *
+ * Return: count of fans found, or an error.
+ */
+static int lwmi_cd_fan_list_alloc_cache(struct lwmi_cd_priv *priv, struct cd_list **listptr)
+{
+	u32 count, *fan_ids, *fan_min_rpms, *fan_max_rpms;
+	union acpi_object *ret_obj __free(kfree) = NULL;
+	struct block { u32 nr; u32 data[]; } *block;
+	struct cd_list *list;
+	size_t size;
+	int idx;
+
+	ret_obj = wmidev_block_query(priv->wdev, 0);
+	if (!ret_obj)
+		return -ENODEV;
+
+	/*
+	 * This is usually caused by a dummy ACPI method. Do not return an error
+	 * as failing to probe this device will result in master driver being
+	 * unbound - this behavior aligns with lwmi_cd_cache().
+	 */
+	if (ret_obj->type != ACPI_TYPE_BUFFER) {
+		count = 0;
+		goto alloc;
+	}
+
+	size = ret_obj->buffer.length;
+	block = (struct block *)ret_obj->buffer.pointer;
+
+	count = size >= sizeof(*block) ? block->nr : 0;
+	if (size < struct_size(block, data, count * 3)) {
+		dev_warn(&priv->wdev->dev,
+			 "incomplete fan test data block: %zu < %zu, ignoring\n",
+			 size, struct_size(block, data, count * 3));
+		count = 0;
+	}
+
+	if (count == 0)
+		goto alloc;
+
+	if (count > U8_MAX) {
+		dev_warn(&priv->wdev->dev,
+			 "too many fans reported: %u > %u, truncating\n",
+			 count, U8_MAX);
+		count = U8_MAX;
+	}
+
+	fan_ids = &block->data[0];
+	fan_max_rpms = &block->data[count];
+	fan_min_rpms = &block->data[count * 2];
+
+alloc:
+	list = devm_kzalloc(&priv->wdev->dev, struct_size(list, cd_fan, count), GFP_KERNEL);
+	if (!list)
+		return -ENOMEM;
+
+	for (idx = 0; idx < count; idx++) {
+		list->cd_fan[idx] = (struct capdata_fan) {
+			.id = fan_ids[idx],
+			.min_rpm = fan_min_rpms[idx],
+			.max_rpm = fan_max_rpms[idx],
+		};
+	}
+
+	*listptr = list;
+	return count;
+}
+
 /**
  * lwmi_cd_alloc() - Allocate a cd_list struct in drvdata
  * @priv: lenovo-wmi-capdata driver data.
@@ -264,6 +354,12 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
 	case LENOVO_CAPABILITY_DATA_01:
 		list_size = struct_size(list, cd01, count);
 		break;
+	case LENOVO_FAN_TEST_DATA:
+		count = lwmi_cd_fan_list_alloc_cache(priv, &list);
+		if (count < 0)
+			return count;
+
+		goto got_list;
 	default:
 		return -EINVAL;
 	}
@@ -272,6 +368,7 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
 	if (!list)
 		return -ENOMEM;
 
+got_list:
 	ret = devm_mutex_init(&priv->wdev->dev, &list->list_mutex);
 	if (ret)
 		return ret;
@@ -390,6 +487,8 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
 
 		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
 		goto out;
+	case LENOVO_FAN_TEST_DATA:
+		goto out;
 	default:
 		return -EINVAL;
 	}
@@ -413,6 +512,8 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
 	case LENOVO_CAPABILITY_DATA_01:
 		component_del(&wdev->dev, &lwmi_cd_component_ops);
 		break;
+	case LENOVO_FAN_TEST_DATA:
+		break;
 	default:
 		WARN_ON(1);
 	}
@@ -425,6 +526,7 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
 static const struct wmi_device_id lwmi_cd_id_table[] = {
 	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_00) },
 	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_01) },
+	{ LWMI_CD_WDEV_ID(LENOVO_FAN_TEST_DATA) },
 	{}
 };
 
diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
index a6d006ef458f..38af4c4e4ef4 100644
--- a/drivers/platform/x86/lenovo/wmi-capdata.h
+++ b/drivers/platform/x86/lenovo/wmi-capdata.h
@@ -26,6 +26,12 @@ struct capdata01 {
 	u32 max_value;
 };
 
+struct capdata_fan {
+	u32 id;
+	u32 min_rpm;
+	u32 max_rpm;
+};
+
 struct lwmi_cd_binder {
 	struct cd_list *cd00_list;
 	struct cd_list *cd01_list;
@@ -34,5 +40,6 @@ struct lwmi_cd_binder {
 void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
 int lwmi_cd00_get_data(struct cd_list *list, u32 attribute_id, struct capdata00 *output);
 int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output);
+int lwmi_cd_fan_get_data(struct cd_list *list, u32 attribute_id, struct capdata_fan *output);
 
 #endif /* !_LENOVO_WMI_CAPDATA_H_ */
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up Fan Test Data
  2025-11-22 18:44 [PATCH v6 0/7] platform/x86: lenovo-wmi-{capdata,other}: Add HWMON for fan speed Rong Zhang
                   ` (4 preceding siblings ...)
  2025-11-22 18:44 ` [PATCH v6 5/7] platform/x86: lenovo-wmi-capdata: Add support for Fan Test Data Rong Zhang
@ 2025-11-22 18:44 ` Rong Zhang
  2025-11-23  3:36   ` Armin Wolf
  2025-11-24 16:49   ` Ilpo Järvinen
  2025-11-22 18:44 ` [PATCH v6 7/7] platform/x86: lenovo-wmi-other: Add HWMON for fan reporting/tuning Rong Zhang
  6 siblings, 2 replies; 22+ messages in thread
From: Rong Zhang @ 2025-11-22 18:44 UTC (permalink / raw)
  To: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Ilpo Järvinen
  Cc: Rong Zhang, Guenter Roeck, platform-driver-x86, linux-kernel,
	linux-hwmon

A capdata00 attribute (0x04050000) describes the presence of Fan Test
Data. Query it, and bind Fan Test Data as a component of capdata00
accordingly. The component master of capdata00 may pass a callback while
binding to retrieve fan info from Fan Test Data.

Summarizing this scheme:

	lenovo-wmi-other <-> capdata00 <-> capdata_fan
	|- master            |- component
	                     |- sub-master
	                                   |- sub-component

The callback will be called once both the master and the sub-component
are bound to the sub-master (component).

This scheme is essential to solve these issues:
- The component framework only supports one aggregation per master
- A binding is only established until all components are found
- The Fan Test Data interface may be missing on some devices
- To get rid of queries for the presense of WMI GUIDs

capdata00 is registered as a component and a sub-master on probe,
instead of chaining the registerations in one's bind callback. This is
because calling (un)registration methods of the component framework
causes deadlock in (un)bind callbacks, i.e., it's impossible to register
capdata00 as a sub-master/component in its component/sub-master bind
callback, and vice versa.

Signed-off-by: Rong Zhang <i@rong.moe>
---
Changes in v6:
- Fix the error path of component_add(capdata00)
- Elaborate the design in commit message

Changes in v5:
- Fix missing include (thanks kernel test robot)

Changes in v4:
- New patch in the series (thanks Armin Wolf's inspiration)
  - Get rid of wmi_has_guid() (see also [PATCH v4 3/7])
---
 drivers/platform/x86/lenovo/wmi-capdata.c | 265 +++++++++++++++++++++-
 drivers/platform/x86/lenovo/wmi-capdata.h |  20 ++
 drivers/platform/x86/lenovo/wmi-other.c   |   5 -
 3 files changed, 283 insertions(+), 7 deletions(-)

diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
index e6392357395c..8760f8c071ca 100644
--- a/drivers/platform/x86/lenovo/wmi-capdata.c
+++ b/drivers/platform/x86/lenovo/wmi-capdata.c
@@ -27,6 +27,7 @@
 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
 
 #include <linux/acpi.h>
+#include <linux/bitfield.h>
 #include <linux/cleanup.h>
 #include <linux/component.h>
 #include <linux/container_of.h>
@@ -50,10 +51,17 @@
 #define ACPI_AC_CLASS "ac_adapter"
 #define ACPI_AC_NOTIFY_STATUS 0x80
 
+#define LWMI_FEATURE_ID_FAN_TEST 0x05
+
+#define LWMI_ATTR_ID_FAN_TEST							\
+	(FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) |		\
+	 FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_TEST))
+
 enum lwmi_cd_type {
 	LENOVO_CAPABILITY_DATA_00,
 	LENOVO_CAPABILITY_DATA_01,
 	LENOVO_FAN_TEST_DATA,
+	CD_TYPE_NONE = -1,
 };
 
 #define LWMI_CD_TABLE_ITEM(_type)		\
@@ -75,6 +83,20 @@ struct lwmi_cd_priv {
 	struct notifier_block acpi_nb; /* ACPI events */
 	struct wmi_device *wdev;
 	struct cd_list *list;
+
+	/*
+	 * A capdata device may be a component master of another capdata device.
+	 * E.g., lenovo-wmi-other <-> capdata00 <-> capdata_fan
+	 *       |- master            |- component
+	 *                            |- sub-master
+	 *                                          |- sub-component
+	 */
+	struct lwmi_cd_sub_master_priv {
+		struct device *master_dev;
+		cd_list_cb_t master_cb;
+		struct cd_list *sub_component_list; /* ERR_PTR(-ENODEV) implies no sub-component. */
+		bool registered;                    /* Has the sub-master been registered? */
+	} *sub_master;
 };
 
 struct cd_list {
@@ -125,7 +147,7 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
 		return;
 
 	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
-		/* Skip optional interfaces. */
+		/* Skip sub-components. */
 		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
 			continue;
 
@@ -137,6 +159,56 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
 }
 EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
 
+/**
+ * lwmi_cd_call_master_cb() - Call the master callback for the sub-component.
+ * @priv: Pointer to the capability data private data.
+ *
+ * Call the master callback and pass the sub-component list to it if the
+ * dependency chain (master <-> sub-master <-> sub-component) is complete.
+ */
+static void lwmi_cd_call_master_cb(struct lwmi_cd_priv *priv)
+{
+	struct cd_list *sub_component_list = priv->sub_master->sub_component_list;
+
+	/*
+	 * Call the callback only if the dependency chain is ready:
+	 * - Binding between master and sub-master: fills master_dev and master_cb
+	 * - Binding between sub-master and sub-component: fills sub_component_list
+	 *
+	 * If a binding has been unbound before the other binding is bound, the
+	 * corresponding members filled by the former are guaranteed to be cleared.
+	 *
+	 * This function is only called in bind callbacks, and the component
+	 * framework guarantees bind/unbind callbacks may never execute
+	 * simultaneously, which implies that it's impossible to have a race
+	 * condition.
+	 *
+	 * Hence, this check is sufficient to ensure that the callback is called
+	 * at most once and with the correct state, without relying on a specific
+	 * sequence of binding establishment.
+	 */
+	if (!sub_component_list ||
+	    !priv->sub_master->master_dev ||
+	    !priv->sub_master->master_cb)
+		return;
+
+	if (PTR_ERR(sub_component_list) == -ENODEV)
+		sub_component_list = NULL;
+	else if (WARN_ON(IS_ERR(sub_component_list)))
+		return;
+
+	priv->sub_master->master_cb(priv->sub_master->master_dev,
+				    sub_component_list);
+
+	/*
+	 * Prevent "unbind and rebind" sequences from userspace from calling the
+	 * callback twice.
+	 */
+	priv->sub_master->master_cb = NULL;
+	priv->sub_master->master_dev = NULL;
+	priv->sub_master->sub_component_list = NULL;
+}
+
 /**
  * lwmi_cd_component_bind() - Bind component to master device.
  * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
@@ -147,6 +219,8 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
  * list. This is used to call lwmi_cd*_get_data to look up attribute data
  * from the lenovo-wmi-other driver.
  *
+ * If cd_dev is a sub-master, try to call the master callback.
+ *
  * Return: 0
  */
 static int lwmi_cd_component_bind(struct device *cd_dev,
@@ -158,6 +232,11 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
 	switch (priv->list->type) {
 	case LENOVO_CAPABILITY_DATA_00:
 		binder->cd00_list = priv->list;
+
+		priv->sub_master->master_dev = om_dev;
+		priv->sub_master->master_cb = binder->cd_fan_list_cb;
+		lwmi_cd_call_master_cb(priv);
+
 		break;
 	case LENOVO_CAPABILITY_DATA_01:
 		binder->cd01_list = priv->list;
@@ -169,8 +248,167 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
 	return 0;
 }
 
+/**
+ * lwmi_cd_component_unbind() - Unbind component to master device.
+ * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
+ * @om_dev: Pointer to the lenovo-wmi-other driver parent device.
+ * @data: Unused.
+ *
+ * If cd_dev is a sub-master, clear the collected data from the master device to
+ * prevent the binding establishment between the sub-master and the sub-
+ * component (if it's about to happen) from calling the master callback.
+ */
+static void lwmi_cd_component_unbind(struct device *cd_dev,
+				     struct device *om_dev, void *data)
+{
+	struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
+
+	switch (priv->list->type) {
+	case LENOVO_CAPABILITY_DATA_00:
+		priv->sub_master->master_dev = NULL;
+		priv->sub_master->master_cb = NULL;
+		return;
+	default:
+		return;
+	}
+}
+
 static const struct component_ops lwmi_cd_component_ops = {
 	.bind = lwmi_cd_component_bind,
+	.unbind = lwmi_cd_component_unbind,
+};
+
+/**
+ * lwmi_cd_sub_master_bind() - Bind sub-component of sub-master device
+ * @dev: The sub-master capdata basic device.
+ *
+ * Call component_bind_all to bind the sub-component device to the sub-master
+ * device. On success, collect the pointer to the sub-component list and try
+ * to call the master callback.
+ *
+ * Return: 0 on success, or an error code.
+ */
+static int lwmi_cd_sub_master_bind(struct device *dev)
+{
+	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
+	struct cd_list *sub_component_list;
+	int ret;
+
+	ret = component_bind_all(dev, &sub_component_list);
+	if (ret)
+		return ret;
+
+	priv->sub_master->sub_component_list = sub_component_list;
+	lwmi_cd_call_master_cb(priv);
+
+	return 0;
+}
+
+/**
+ * lwmi_cd_sub_master_unbind() - Unbind sub-component of sub-master device
+ * @dev: The sub-master capdata basic device
+ *
+ * Clear the collected pointer to the sub-component list to prevent the binding
+ * establishment between the sub-master and the sub-component (if it's about to
+ * happen) from calling the master callback. Then, call component_unbind_all to
+ * unbind the sub-component device from the sub-master device.
+ */
+static void lwmi_cd_sub_master_unbind(struct device *dev)
+{
+	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
+
+	priv->sub_master->sub_component_list = NULL;
+
+	component_unbind_all(dev, NULL);
+}
+
+static const struct component_master_ops lwmi_cd_sub_master_ops = {
+	.bind = lwmi_cd_sub_master_bind,
+	.unbind = lwmi_cd_sub_master_unbind,
+};
+
+/**
+ * lwmi_cd_sub_master_add() - Register a sub-master with its sub-component
+ * @priv: Pointer to the sub-master capdata device private data.
+ * @sub_component_type: Type of the sub-component.
+ *
+ * Match the sub-component type and register the current capdata device as a
+ * sub-master. If the given sub-component type is CD_TYPE_NONE, mark the sub-
+ * component as non-existent without registering sub-master.
+ *
+ * Return: 0 on success, or an error code.
+ */
+static int lwmi_cd_sub_master_add(struct lwmi_cd_priv *priv,
+				  enum lwmi_cd_type sub_component_type)
+{
+	struct component_match *master_match = NULL;
+	int ret;
+
+	priv->sub_master = devm_kzalloc(&priv->wdev->dev, sizeof(*priv->sub_master), GFP_KERNEL);
+	if (!priv->sub_master)
+		return -ENOMEM;
+
+	if (sub_component_type == CD_TYPE_NONE) {
+		/* The master callback will be called with NULL on bind. */
+		priv->sub_master->sub_component_list = ERR_PTR(-ENODEV);
+		priv->sub_master->registered = false;
+		return 0;
+	}
+
+	/*
+	 * lwmi_cd_match() needs a pointer to enum lwmi_cd_type, but on-stack
+	 * data cannot be used here. Steal one from lwmi_cd_table.
+	 */
+	component_match_add(&priv->wdev->dev, &master_match, lwmi_cd_match,
+			    (void *)&lwmi_cd_table[sub_component_type].type);
+	if (IS_ERR(master_match))
+		return PTR_ERR(master_match);
+
+	ret = component_master_add_with_match(&priv->wdev->dev, &lwmi_cd_sub_master_ops,
+					      master_match);
+	if (ret)
+		return ret;
+
+	priv->sub_master->registered = true;
+	return 0;
+}
+
+/**
+ * lwmi_cd_sub_master_del() - Unregister a sub-master if it's registered
+ * @priv: Pointer to the sub-master capdata device private data.
+ */
+static void lwmi_cd_sub_master_del(struct lwmi_cd_priv *priv)
+{
+	if (priv->sub_master->registered) {
+		component_master_del(&priv->wdev->dev, &lwmi_cd_sub_master_ops);
+		priv->sub_master->registered = false;
+	}
+}
+
+/**
+ * lwmi_cd_sub_component_bind() - Bind sub-component to sub-master device.
+ * @sc_dev: Pointer to the sub-component capdata parent device.
+ * @sm_dev: Pointer to the sub-master capdata parent device.
+ * @data: Pointer used to return the capability data list pointer.
+ *
+ * On sub-master's bind, provide a pointer to the local capdata list.
+ * This is used by the sub-master to call the master callback.
+ *
+ * Return: 0
+ */
+static int lwmi_cd_sub_component_bind(struct device *sc_dev,
+				      struct device *sm_dev, void *data)
+{
+	struct lwmi_cd_priv *priv = dev_get_drvdata(sc_dev);
+	struct cd_list **listp = data;
+
+	*listp = priv->list;
+
+	return 0;
+}
+
+static const struct component_ops lwmi_cd_sub_component_ops = {
+	.bind = lwmi_cd_sub_component_bind,
 };
 
 /**
@@ -470,9 +708,28 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
 		goto out;
 
 	switch (info->type) {
-	case LENOVO_CAPABILITY_DATA_00:
+	case LENOVO_CAPABILITY_DATA_00: {
+		enum lwmi_cd_type sub_component_type = LENOVO_FAN_TEST_DATA;
+		struct capdata00 capdata00;
+
+		ret = lwmi_cd00_get_data(priv->list, LWMI_ATTR_ID_FAN_TEST, &capdata00);
+		if (ret || !(capdata00.supported & LWMI_SUPP_VALID)) {
+			dev_dbg(&wdev->dev, "capdata00 declares no fan test support\n");
+			sub_component_type = CD_TYPE_NONE;
+		}
+
+		/* Sub-master (capdata00) <-> sub-component (capdata_fan) */
+		ret = lwmi_cd_sub_master_add(priv, sub_component_type);
+		if (ret)
+			goto out;
+
+		/* Master (lenovo-wmi-other) <-> sub-master (capdata00) */
 		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
+		if (ret)
+			lwmi_cd_sub_master_del(priv);
+
 		goto out;
+	}
 	case LENOVO_CAPABILITY_DATA_01:
 		priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
 
@@ -488,6 +745,7 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
 		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
 		goto out;
 	case LENOVO_FAN_TEST_DATA:
+		ret = component_add(&wdev->dev, &lwmi_cd_sub_component_ops);
 		goto out;
 	default:
 		return -EINVAL;
@@ -509,10 +767,13 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
 
 	switch (priv->list->type) {
 	case LENOVO_CAPABILITY_DATA_00:
+		lwmi_cd_sub_master_del(priv);
+		fallthrough;
 	case LENOVO_CAPABILITY_DATA_01:
 		component_del(&wdev->dev, &lwmi_cd_component_ops);
 		break;
 	case LENOVO_FAN_TEST_DATA:
+		component_del(&wdev->dev, &lwmi_cd_sub_component_ops);
 		break;
 	default:
 		WARN_ON(1);
diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
index 38af4c4e4ef4..59ca3b3e5760 100644
--- a/drivers/platform/x86/lenovo/wmi-capdata.h
+++ b/drivers/platform/x86/lenovo/wmi-capdata.h
@@ -5,8 +5,20 @@
 #ifndef _LENOVO_WMI_CAPDATA_H_
 #define _LENOVO_WMI_CAPDATA_H_
 
+#include <linux/bits.h>
 #include <linux/types.h>
 
+#define LWMI_SUPP_VALID		BIT(0)
+#define LWMI_SUPP_MAY_GET	(LWMI_SUPP_VALID | BIT(1))
+#define LWMI_SUPP_MAY_SET	(LWMI_SUPP_VALID | BIT(2))
+
+#define LWMI_ATTR_DEV_ID_MASK	GENMASK(31, 24)
+#define LWMI_ATTR_FEAT_ID_MASK	GENMASK(23, 16)
+#define LWMI_ATTR_MODE_ID_MASK	GENMASK(15, 8)
+#define LWMI_ATTR_TYPE_ID_MASK	GENMASK(7, 0)
+
+#define LWMI_DEVICE_ID_FAN	0x04
+
 struct component_match;
 struct device;
 struct cd_list;
@@ -32,9 +44,17 @@ struct capdata_fan {
 	u32 max_rpm;
 };
 
+typedef void (*cd_list_cb_t)(struct device *master_dev, struct cd_list *cd_list);
+
 struct lwmi_cd_binder {
 	struct cd_list *cd00_list;
 	struct cd_list *cd01_list;
+	/*
+	 * May be called during or after the bind callback.
+	 * Will be called with NULL if capdata_fan does not exist.
+	 * The pointer is only valid in the callback; never keep it for later use!
+	 */
+	cd_list_cb_t cd_fan_list_cb;
 };
 
 void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
index f2e1e34d58a9..b3adcc2804fa 100644
--- a/drivers/platform/x86/lenovo/wmi-other.c
+++ b/drivers/platform/x86/lenovo/wmi-other.c
@@ -54,11 +54,6 @@
 #define LWMI_FEATURE_VALUE_GET 17
 #define LWMI_FEATURE_VALUE_SET 18
 
-#define LWMI_ATTR_DEV_ID_MASK GENMASK(31, 24)
-#define LWMI_ATTR_FEAT_ID_MASK GENMASK(23, 16)
-#define LWMI_ATTR_MODE_ID_MASK GENMASK(15, 8)
-#define LWMI_ATTR_TYPE_ID_MASK GENMASK(7, 0)
-
 #define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other"
 
 static BLOCKING_NOTIFIER_HEAD(om_chain_head);
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 22+ messages in thread

* [PATCH v6 7/7] platform/x86: lenovo-wmi-other: Add HWMON for fan reporting/tuning
  2025-11-22 18:44 [PATCH v6 0/7] platform/x86: lenovo-wmi-{capdata,other}: Add HWMON for fan speed Rong Zhang
                   ` (5 preceding siblings ...)
  2025-11-22 18:44 ` [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up " Rong Zhang
@ 2025-11-22 18:44 ` Rong Zhang
  2025-11-24 16:58   ` Ilpo Järvinen
  6 siblings, 1 reply; 22+ messages in thread
From: Rong Zhang @ 2025-11-22 18:44 UTC (permalink / raw)
  To: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Ilpo Järvinen
  Cc: Rong Zhang, Guenter Roeck, platform-driver-x86, linux-kernel,
	linux-hwmon

Register an HWMON device for fan reporting/tuning according to
Capability Data 00 (capdata00) and Fan Test Data (capdata_fan) provided
by lenovo-wmi-capdata. The corresponding HWMON nodes are:

 - fanX_enable: enable/disable the fan (tunable)
 - fanX_input: current RPM
 - fanX_max: maximum RPM
 - fanX_min: minimum RPM
 - fanX_target: target RPM (tunable)

Information from capdata00 and capdata_fan are used to control the
visibility and constraints of HWMON attributes. Fan info from capdata00
is collected on bind, while fan info from capdata_fan is collected in a
callback. Once all fan info is collected, register the HWMON device.

Signed-off-by: Rong Zhang <i@rong.moe>
---
Changes in v4:
- Rework HWMON registration due to the introduction of [PATCH v4 6/7]
  - Collect fan info from capdata00 and capdata_fan separately
  - Use a callback to collect fan info from capdata_fan
  - Trigger HWMON registration only if all fan info is collected
  - Do not check 0x04050000.supported, implied by the presense of
    capdata_fan
- Drop Reviewed-by & Tested-by due to the changes, please review & test

Changes in v3:
- Reword documentation (thanks Derek J. Clark)

Changes in v2:
- Define 4 fan channels instead of 2 (thanks Derek J. Clark)
- Squash min/max reporting patch into this one (ditto)
- Query 0x04050000 for interface availability (ditto)
  - New parameter "expose_all_fans" to skip this check
- Enforce min/max RPM constraint on set (ditto)
  - New parameter "relax_fan_constraint" to disable this behavior
  - Drop parameter "ignore_fan_cap", superseded by the next one
  - New parameter "expose_all_fans" to expose fans w/o such data
- Assume auto mode on probe (ditto)
- Reword documentation (ditto)
- Do not register HWMON device if no fan can be exposed
- fanX_target: Return -EBUSY instead of raw target value when fan stops
---
 .../wmi/devices/lenovo-wmi-other.rst          |  11 +
 drivers/platform/x86/lenovo/Kconfig           |   1 +
 drivers/platform/x86/lenovo/wmi-other.c       | 485 +++++++++++++++++-
 3 files changed, 487 insertions(+), 10 deletions(-)

diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
index 821282e07d93..bd1d733ff286 100644
--- a/Documentation/wmi/devices/lenovo-wmi-other.rst
+++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
@@ -31,6 +31,8 @@ under the following path:
 
   /sys/class/firmware-attributes/lenovo-wmi-other/attributes/<attribute>/
 
+Additionally, this driver also exports attributes to HWMON.
+
 LENOVO_CAPABILITY_DATA_00
 -------------------------
 
@@ -39,6 +41,11 @@ WMI GUID ``362A3AFE-3D96-4665-8530-96DAD5BB300E``
 The LENOVO_CAPABILITY_DATA_00 interface provides various information that
 does not rely on the gamezone thermal mode.
 
+The following HWMON attributes are implemented:
+ - fanX_enable: enable/disable the fan (tunable)
+ - fanX_input: current RPM
+ - fanX_target: target RPM (tunable)
+
 LENOVO_CAPABILITY_DATA_01
 -------------------------
 
@@ -70,6 +77,10 @@ WMI GUID ``B642801B-3D21-45DE-90AE-6E86F164FB21``
 The LENOVO_FAN_TEST_DATA interface provides reference data for self-test of
 cooling fans.
 
+The following HWMON attributes are implemented:
+ - fanX_max: maximum RPM
+ - fanX_min: minimum RPM
+
 WMI interface description
 =========================
 
diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
index fb96a0f908f0..be9af0451146 100644
--- a/drivers/platform/x86/lenovo/Kconfig
+++ b/drivers/platform/x86/lenovo/Kconfig
@@ -263,6 +263,7 @@ config LENOVO_WMI_GAMEZONE
 config LENOVO_WMI_TUNING
 	tristate "Lenovo Other Mode WMI Driver"
 	depends on ACPI_WMI
+	select HWMON
 	select FW_ATTR_CLASS
 	select LENOVO_WMI_DATA
 	select LENOVO_WMI_EVENTS
diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
index b3adcc2804fa..ce1ca13db4b5 100644
--- a/drivers/platform/x86/lenovo/wmi-other.c
+++ b/drivers/platform/x86/lenovo/wmi-other.c
@@ -14,7 +14,16 @@
  * These attributes typically don't fit anywhere else in the sysfs and are set
  * in Windows using one of Lenovo's multiple user applications.
  *
+ * Additionally, this driver also exports tunable fan speed RPM to HWMON.
+ * Min/max RPM are also provided for reference.
+ *
  * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
+ *   - fw_attributes
+ *   - binding to Capability Data 01
+ *
+ * Copyright (C) 2025 Rong Zhang <i@rong.moe>
+ *   - HWMON
+ *   - binding to Capability Data 00 and Fan
  */
 
 #include <linux/acpi.h>
@@ -25,6 +34,7 @@
 #include <linux/device.h>
 #include <linux/export.h>
 #include <linux/gfp_types.h>
+#include <linux/hwmon.h>
 #include <linux/idr.h>
 #include <linux/kdev_t.h>
 #include <linux/kobject.h>
@@ -49,12 +59,26 @@
 #define LWMI_FEATURE_ID_CPU_SPL 0x02
 #define LWMI_FEATURE_ID_CPU_FPPT 0x03
 
+#define LWMI_FEATURE_ID_FAN_RPM 0x03
+
 #define LWMI_TYPE_ID_NONE 0x00
 
 #define LWMI_FEATURE_VALUE_GET 17
 #define LWMI_FEATURE_VALUE_SET 18
 
+#define LWMI_FAN_ID_BASE 1
+#define LWMI_FAN_NR 4
+#define LWMI_FAN_ID(x) ((x) + LWMI_FAN_ID_BASE)
+
+#define LWMI_ATTR_ID_FAN_RPM(x)						\
+	(FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) |	\
+	 FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_RPM) |	\
+	 FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, LWMI_FAN_ID(x)))
+
+#define LWMI_FAN_STOP_RPM 1
+
 #define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other"
+#define LWMI_OM_HWMON_NAME "lenovo_wmi_other"
 
 static BLOCKING_NOTIFIER_HEAD(om_chain_head);
 static DEFINE_IDA(lwmi_om_ida);
@@ -71,15 +95,439 @@ struct lwmi_om_priv {
 	struct component_master_ops *ops;
 
 	/* only valid after capdata bind */
+	struct cd_list *cd00_list;
 	struct cd_list *cd01_list;
 
+	struct device *hwmon_dev;
 	struct device *fw_attr_dev;
 	struct kset *fw_attr_kset;
 	struct notifier_block nb;
 	struct wmi_device *wdev;
 	int ida_id;
+
+	struct fan_info {
+		u32 supported;
+		u32 last_target;
+		long min_rpm;
+		long max_rpm;
+	} fan_info[LWMI_FAN_NR];
+	struct {
+		bool capdata00_collected : 1;
+		bool capdata_fan_collected : 1;
+	} fan_flags;
+};
+
+/*
+ * Visibility of fan channels:
+ *
+ * +-------------------+---------+------------------+-----------------------+------------+
+ * |                   | default | +expose_all_fans | +relax_fan_constraint | +both      |
+ * +-------------------+---------+------------------+-----------------------+------------+
+ * | canonical         | RW      | RW               | RW+relaxed            | RW+relaxed |
+ * +-------------------+---------+------------------+-----------------------+------------+
+ * | -capdata_fan[idx] | N       | RO               | N                     | RW+relaxed |
+ * +-------------------+---------+------------------+-----------------------+------------+
+ *
+ * Note:
+ * 1. LWMI_ATTR_ID_FAN_RPM[idx].supported is always checked before exposing a channel.
+ * 2. -capdata_fan implies -capdata_fan[idx].
+ */
+static bool expose_all_fans;
+module_param(expose_all_fans, bool, 0444);
+MODULE_PARM_DESC(expose_all_fans,
+	"This option skips some capability checks and solely relies on per-channel ones "
+	"to expose fan attributes. Use with caution.");
+
+static bool relax_fan_constraint;
+module_param(relax_fan_constraint, bool, 0444);
+MODULE_PARM_DESC(relax_fan_constraint,
+	"Do not enforce fan RPM constraint (min/max RPM) "
+	"and enables fan tuning when such data is missing. "
+	"Enabling this may results in HWMON attributes being out-of-sync. Use with caution.");
+
+/* ======== HWMON (component: lenovo-wmi-capdata 00 & fan) ======== */
+
+/**
+ * lwmi_om_fan_get_set() - Get or set fan RPM value of specified fan
+ * @priv: Driver private data structure
+ * @channel: Fan channel index (0-based)
+ * @val: Pointer to value (input for set, output for get)
+ * @set: True to set value, false to get value
+ *
+ * Communicates with WMI interface to either retrieve current fan RPM
+ * or set target fan RPM.
+ *
+ * Return: 0 on success, or an error code.
+ */
+static int lwmi_om_fan_get_set(struct lwmi_om_priv *priv, int channel, u32 *val, bool set)
+{
+	struct wmi_method_args_32 args;
+	u32 method_id, retval;
+	int err;
+
+	method_id = set ? LWMI_FEATURE_VALUE_SET : LWMI_FEATURE_VALUE_GET;
+	args.arg0 = LWMI_ATTR_ID_FAN_RPM(channel);
+	args.arg1 = set ? *val : 0;
+
+	err = lwmi_dev_evaluate_int(priv->wdev, 0x0, method_id,
+				    (unsigned char *)&args, sizeof(args), &retval);
+	if (err)
+		return err;
+
+	if (!set)
+		*val = retval;
+	else if (retval != 1)
+		return -EIO;
+
+	return 0;
+}
+
+/**
+ * lwmi_om_hwmon_is_visible() - Determine visibility of HWMON attributes
+ * @drvdata: Driver private data
+ * @type: Sensor type
+ * @attr: Attribute identifier
+ * @channel: Channel index
+ *
+ * Determines whether an HWMON attribute should be visible in sysfs
+ * based on hardware capabilities and current configuration.
+ *
+ * Return: permission mode, or 0 if invisible.
+ */
+static umode_t lwmi_om_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type,
+					u32 attr, int channel)
+{
+	struct lwmi_om_priv *priv = (struct lwmi_om_priv *)drvdata;
+	bool visible = false;
+
+	if (type == hwmon_fan) {
+		if (!(priv->fan_info[channel].supported & LWMI_SUPP_VALID))
+			return 0;
+
+		switch (attr) {
+		case hwmon_fan_enable:
+		case hwmon_fan_target:
+			if (!(priv->fan_info[channel].supported & LWMI_SUPP_MAY_SET))
+				return 0;
+
+			if (relax_fan_constraint ||
+			    (priv->fan_info[channel].min_rpm >= 0 &&
+			     priv->fan_info[channel].max_rpm >= 0))
+				return 0644;
+
+			/*
+			 * Reaching here implies expose_all_fans is set.
+			 * See lwmi_om_hwmon_add().
+			 */
+			dev_warn_once(&priv->wdev->dev,
+				      "fan tuning disabled due to missing RPM constraint\n");
+			return 0;
+		case hwmon_fan_input:
+			visible = priv->fan_info[channel].supported & LWMI_SUPP_MAY_GET;
+			break;
+		case hwmon_fan_min:
+			visible = priv->fan_info[channel].min_rpm >= 0;
+			break;
+		case hwmon_fan_max:
+			visible = priv->fan_info[channel].max_rpm >= 0;
+			break;
+		}
+	}
+
+	return visible ? 0444 : 0;
+}
+
+/**
+ * lwmi_om_hwmon_read() - Read HWMON sensor data
+ * @dev: Device pointer
+ * @type: Sensor type
+ * @attr: Attribute identifier
+ * @channel: Channel index
+ * @val: Pointer to store value
+ *
+ * Reads current sensor values from hardware through WMI interface.
+ *
+ * Return: 0 on success, or an error code.
+ */
+static int lwmi_om_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
+			      u32 attr, int channel, long *val)
+{
+	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
+	u32 retval = 0;
+	int err;
+
+	if (type == hwmon_fan) {
+		switch (attr) {
+		case hwmon_fan_input:
+			err = lwmi_om_fan_get_set(priv, channel, &retval, false);
+			if (err)
+				return err;
+
+			*val = retval;
+			return 0;
+		case hwmon_fan_enable:
+			*val = priv->fan_info[channel].last_target != LWMI_FAN_STOP_RPM;
+			return 0;
+		case hwmon_fan_target:
+			if (priv->fan_info[channel].last_target == LWMI_FAN_STOP_RPM)
+				return -EBUSY;
+
+			*val = priv->fan_info[channel].last_target;
+			return 0;
+		case hwmon_fan_min:
+			*val = priv->fan_info[channel].min_rpm;
+			return 0;
+		case hwmon_fan_max:
+			*val = priv->fan_info[channel].max_rpm;
+			return 0;
+		}
+	}
+
+	return -EOPNOTSUPP;
+}
+
+/**
+ * lwmi_om_hwmon_write() - Write HWMON sensor data
+ * @dev: Device pointer
+ * @type: Sensor type
+ * @attr: Attribute identifier
+ * @channel: Channel index
+ * @val: Value to write
+ *
+ * Writes configuration values to hardware through WMI interface.
+ *
+ * Return: 0 on success, or an error code.
+ */
+static int lwmi_om_hwmon_write(struct device *dev, enum hwmon_sensor_types type,
+			       u32 attr, int channel, long val)
+{
+	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
+	u32 raw, min_rpm, max_rpm;
+	int err;
+
+	if (type == hwmon_fan) {
+		switch (attr) {
+		case hwmon_fan_enable:
+			if (val == 0)
+				raw = LWMI_FAN_STOP_RPM;
+			else if (val == 1)
+				raw = 0; /* auto */
+			else
+				return -EINVAL;
+
+			goto fan_set;
+		case hwmon_fan_target:
+			if (val == 0) {
+				raw = 0;
+				goto fan_set;
+			}
+
+			min_rpm = relax_fan_constraint
+					? LWMI_FAN_STOP_RPM + 1
+					: priv->fan_info[channel].min_rpm;
+			max_rpm = relax_fan_constraint
+					? U16_MAX
+					: priv->fan_info[channel].max_rpm;
+
+			if (val < min_rpm || val > max_rpm)
+				return -EDOM;
+
+			raw = val;
+fan_set:
+			err = lwmi_om_fan_get_set(priv, channel, &raw, true);
+			if (err)
+				return err;
+
+			priv->fan_info[channel].last_target = raw;
+			return 0;
+		}
+	}
+
+	return -EOPNOTSUPP;
+}
+
+static const struct hwmon_channel_info * const lwmi_om_hwmon_info[] = {
+	/* Must match LWMI_FAN_NR. */
+	HWMON_CHANNEL_INFO(fan,
+			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
+			   HWMON_F_MIN | HWMON_F_MAX,
+			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
+			   HWMON_F_MIN | HWMON_F_MAX,
+			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
+			   HWMON_F_MIN | HWMON_F_MAX,
+			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
+			   HWMON_F_MIN | HWMON_F_MAX),
+	NULL
 };
 
+static const struct hwmon_ops lwmi_om_hwmon_ops = {
+	.is_visible = lwmi_om_hwmon_is_visible,
+	.read = lwmi_om_hwmon_read,
+	.write = lwmi_om_hwmon_write,
+};
+
+static const struct hwmon_chip_info lwmi_om_hwmon_chip_info = {
+	.ops = &lwmi_om_hwmon_ops,
+	.info = lwmi_om_hwmon_info,
+};
+
+/**
+ * lwmi_om_hwmon_add() - Register HWMON device if all info is collected
+ * @priv: Driver private data
+ */
+static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv)
+{
+	int i, valid;
+
+	if (WARN_ON(priv->hwmon_dev))
+		return;
+
+	if (!priv->fan_flags.capdata00_collected || !priv->fan_flags.capdata_fan_collected) {
+		dev_dbg(&priv->wdev->dev, "HWMON registration pending (00: %d, fan: %d)\n",
+			priv->fan_flags.capdata00_collected,
+			priv->fan_flags.capdata_fan_collected);
+		return;
+	}
+
+	if (expose_all_fans)
+		dev_warn(&priv->wdev->dev, "all fans exposed. Use with caution\n");
+
+	if (relax_fan_constraint)
+		dev_warn(&priv->wdev->dev, "fan RPM constraint relaxed. Use with caution\n");
+
+	valid = 0;
+	for (i = 0; i < LWMI_FAN_NR; i++) {
+		if (!(priv->fan_info[i].supported & LWMI_SUPP_VALID))
+			continue;
+
+		valid++;
+
+		if (!expose_all_fans &&
+		    (priv->fan_info[i].min_rpm < 0 || priv->fan_info[i].max_rpm < 0)) {
+			dev_dbg(&priv->wdev->dev, "missing RPM constraint for fan%d, hiding\n",
+				LWMI_FAN_ID(i));
+			priv->fan_info[i].supported = 0;
+			valid--;
+		}
+	}
+
+	if (valid == 0) {
+		dev_warn(&priv->wdev->dev,
+			 "fan reporting/tuning is unsupported on this device\n");
+		return;
+	}
+
+	priv->hwmon_dev = hwmon_device_register_with_info(&priv->wdev->dev,
+							  LWMI_OM_HWMON_NAME, priv,
+							  &lwmi_om_hwmon_chip_info,
+							  NULL);
+	if (IS_ERR(priv->hwmon_dev)) {
+		dev_warn(&priv->wdev->dev, "failed to register HWMON device: %ld\n",
+			 PTR_ERR(priv->hwmon_dev));
+		priv->hwmon_dev = NULL;
+		return;
+	}
+
+	dev_dbg(&priv->wdev->dev, "registered HWMON device\n");
+}
+
+/**
+ * lwmi_om_hwmon_remove() - Unregister HWMON device
+ * @priv: Driver private data
+ *
+ * Unregisters the HWMON device if applicable.
+ */
+static void lwmi_om_hwmon_remove(struct lwmi_om_priv *priv)
+{
+	if (!priv->hwmon_dev)
+		return;
+
+	hwmon_device_unregister(priv->hwmon_dev);
+	priv->hwmon_dev = NULL;
+}
+
+/**
+ * lwmi_om_fan_info_init() - Initialzie fan info
+ * @priv: Driver private data
+ *
+ * lwmi_om_fan_info_collect_cd00() and lwmi_om_fan_info_collect_cd_fan() may be
+ * called in an arbitrary order. Hence, initializion must be done before.
+ */
+static void lwmi_om_fan_info_init(struct lwmi_om_priv *priv)
+{
+	int i;
+
+	for (i = 0; i < LWMI_FAN_NR; i++) {
+		priv->fan_info[i] = (struct fan_info) {
+			.supported = 0,
+			/*
+			 * Assume 0 on probe as the EC resets all fans to auto mode on (re)boot.
+			 *
+			 * Note that S0ix (s2idle) preserves the RPM target, so we don't need
+			 * suspend/resume callbacks. This behavior has not been tested on S3-
+			 * capable devices, but I doubt if such devices even have this interface.
+			 */
+			.last_target = 0,
+			.min_rpm = -ENODATA,
+			.max_rpm = -ENODATA,
+		};
+	}
+
+	priv->fan_flags.capdata00_collected = false;
+	priv->fan_flags.capdata_fan_collected = false;
+}
+
+/**
+ * lwmi_om_fan_info_collect_cd00() - Collect fan info from capdata 00
+ * @priv: Driver private data
+ */
+static void lwmi_om_fan_info_collect_cd00(struct lwmi_om_priv *priv)
+{
+	struct capdata00 capdata00;
+	int i, err;
+
+	dev_dbg(&priv->wdev->dev, "Collecting fan info from capdata00\n");
+
+	for (i = 0; i < LWMI_FAN_NR; i++) {
+		err = lwmi_cd00_get_data(priv->cd00_list, LWMI_ATTR_ID_FAN_RPM(i), &capdata00);
+		priv->fan_info[i].supported = err ? 0 : capdata00.supported;
+	}
+
+	priv->fan_flags.capdata00_collected = true;
+	lwmi_om_hwmon_add(priv);
+}
+
+/**
+ * lwmi_om_fan_info_collect_cd_fan() - Collect fan info from capdata fan
+ * @dev: Pointer to the lenovo-wmi-other device
+ * @cd_fan_list: Pointer to the capdata fan list
+ */
+static void lwmi_om_fan_info_collect_cd_fan(struct device *dev, struct cd_list *cd_fan_list)
+{
+	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
+	struct capdata_fan capdata_fan;
+	int i, err;
+
+	dev_dbg(dev, "Collecting fan info from capdata_fan\n");
+
+	if (!cd_fan_list)
+		goto out;
+
+	for (i = 0; i < LWMI_FAN_NR; i++) {
+		err = lwmi_cd_fan_get_data(cd_fan_list, LWMI_FAN_ID(i), &capdata_fan);
+		if (err)
+			continue;
+
+		priv->fan_info[i].min_rpm = capdata_fan.min_rpm;
+		priv->fan_info[i].max_rpm = capdata_fan.max_rpm;
+	}
+out:
+	priv->fan_flags.capdata_fan_collected = true;
+	lwmi_om_hwmon_add(priv);
+}
+
+/* ======== fw_attributes (component: lenovo-wmi-capdata 01) ======== */
+
 struct tunable_attr_01 {
 	struct capdata01 *capdata;
 	struct device *dev;
@@ -559,32 +1007,45 @@ static void lwmi_om_fw_attr_remove(struct lwmi_om_priv *priv)
 	device_unregister(priv->fw_attr_dev);
 }
 
+/* ======== Self (master: lenovo-wmi-other) ======== */
+
 /**
  * lwmi_om_master_bind() - Bind all components of the other mode driver
  * @dev: The lenovo-wmi-other driver basic device.
  *
- * Call component_bind_all to bind the lenovo-wmi-capdata01 driver to the
- * lenovo-wmi-other master driver. On success, assign the capability data 01
- * list pointer to the driver data struct for later access. This pointer
- * is only valid while the capdata01 interface exists. Finally, register all
- * firmware attribute groups.
+ * Call component_bind_all to bind the lenovo-wmi-capdata devices to the
+ * lenovo-wmi-other master driver, with a callback to collect fan info from
+ * capdata_fan. On success, assign the capability data list pointers to the
+ * driver data struct for later access. These pointers are only valid while the
+ * capdata interfaces exist. Finally, collect fan info from capdata00 and
+ * register all firmware attribute groups. Note that the HWMON device is
+ * registered only if all fan info is collected. Hence, it is not registered
+ * here. See lwmi_om_fan_info_collect_cd00() and
+ * lwmi_om_fan_info_collect_cd_fan().
  *
  * Return: 0 on success, or an error code.
  */
 static int lwmi_om_master_bind(struct device *dev)
 {
 	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
-	struct lwmi_cd_binder binder = { 0 };
+	struct lwmi_cd_binder binder = {
+		.cd_fan_list_cb = lwmi_om_fan_info_collect_cd_fan,
+	};
 	int ret;
 
+	lwmi_om_fan_info_init(priv);
+
 	ret = component_bind_all(dev, &binder);
 	if (ret)
 		return ret;
 
+	priv->cd00_list = binder.cd00_list;
 	priv->cd01_list = binder.cd01_list;
-	if (!priv->cd01_list)
+	if (!priv->cd00_list || !priv->cd01_list)
 		return -ENODEV;
 
+	lwmi_om_fan_info_collect_cd00(priv);
+
 	return lwmi_om_fw_attr_add(priv);
 }
 
@@ -592,15 +1053,18 @@ static int lwmi_om_master_bind(struct device *dev)
  * lwmi_om_master_unbind() - Unbind all components of the other mode driver
  * @dev: The lenovo-wmi-other driver basic device
  *
- * Unregister all capability data attribute groups. Then call
- * component_unbind_all to unbind the lenovo-wmi-capdata01 driver from the
- * lenovo-wmi-other master driver. Finally, free the IDA for this device.
+ * Unregister all firmware attribute groups and the HWMON device. Then call
+ * component_unbind_all to unbind lenovo-wmi-capdata devices from the
+ * lenovo-wmi-other master driver.
  */
 static void lwmi_om_master_unbind(struct device *dev)
 {
 	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
 
 	lwmi_om_fw_attr_remove(priv);
+
+	lwmi_om_hwmon_remove(priv);
+
 	component_unbind_all(dev, NULL);
 }
 
@@ -665,5 +1129,6 @@ MODULE_IMPORT_NS("LENOVO_WMI_CD");
 MODULE_IMPORT_NS("LENOVO_WMI_HELPERS");
 MODULE_DEVICE_TABLE(wmi, lwmi_other_id_table);
 MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
+MODULE_AUTHOR("Rong Zhang <i@rong.moe>");
 MODULE_DESCRIPTION("Lenovo Other Mode WMI Driver");
 MODULE_LICENSE("GPL");
-- 
2.51.0


^ permalink raw reply related	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up Fan Test Data
  2025-11-22 18:44 ` [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up " Rong Zhang
@ 2025-11-23  3:36   ` Armin Wolf
  2025-11-23 15:02     ` Rong Zhang
  2025-11-24 16:49   ` Ilpo Järvinen
  1 sibling, 1 reply; 22+ messages in thread
From: Armin Wolf @ 2025-11-23  3:36 UTC (permalink / raw)
  To: Rong Zhang, Mark Pearson, Derek J. Clark, Hans de Goede,
	Ilpo Järvinen
  Cc: Guenter Roeck, platform-driver-x86, linux-kernel, linux-hwmon

Am 22.11.25 um 19:44 schrieb Rong Zhang:

> A capdata00 attribute (0x04050000) describes the presence of Fan Test
> Data. Query it, and bind Fan Test Data as a component of capdata00
> accordingly. The component master of capdata00 may pass a callback while
> binding to retrieve fan info from Fan Test Data.
>
> Summarizing this scheme:
>
> 	lenovo-wmi-other <-> capdata00 <-> capdata_fan
> 	|- master            |- component
> 	                     |- sub-master
> 	                                   |- sub-component
>
> The callback will be called once both the master and the sub-component
> are bound to the sub-master (component).
>
> This scheme is essential to solve these issues:
> - The component framework only supports one aggregation per master
> - A binding is only established until all components are found
> - The Fan Test Data interface may be missing on some devices
> - To get rid of queries for the presense of WMI GUIDs
>
> capdata00 is registered as a component and a sub-master on probe,
> instead of chaining the registerations in one's bind callback. This is
> because calling (un)registration methods of the component framework
> causes deadlock in (un)bind callbacks, i.e., it's impossible to register
> capdata00 as a sub-master/component in its component/sub-master bind
> callback, and vice versa.

I spend some time trying to understand all of this, and i have come to the conclusion
that while passing a callback between both component masters, it still is not exactly
clean.

Can you instead use devm_lwmi_om_register_notifier() and define another event for querying
the fan data? This way we could ditch the component submaster with all of the callback madness.
Sorry for being the one for suggesting the submaster approach in the first place, i did not
know that the component framework is so limited when it comes to nested component masters :/

If Mark is OK with using the submaster approach then you can ignore the above suggestion.

Thanks,
Armin Wolf

> Signed-off-by: Rong Zhang <i@rong.moe>
> ---
> Changes in v6:
> - Fix the error path of component_add(capdata00)
> - Elaborate the design in commit message
>
> Changes in v5:
> - Fix missing include (thanks kernel test robot)
>
> Changes in v4:
> - New patch in the series (thanks Armin Wolf's inspiration)
>    - Get rid of wmi_has_guid() (see also [PATCH v4 3/7])
> ---
>   drivers/platform/x86/lenovo/wmi-capdata.c | 265 +++++++++++++++++++++-
>   drivers/platform/x86/lenovo/wmi-capdata.h |  20 ++
>   drivers/platform/x86/lenovo/wmi-other.c   |   5 -
>   3 files changed, 283 insertions(+), 7 deletions(-)
>
> diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
> index e6392357395c..8760f8c071ca 100644
> --- a/drivers/platform/x86/lenovo/wmi-capdata.c
> +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
> @@ -27,6 +27,7 @@
>   #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
>   
>   #include <linux/acpi.h>
> +#include <linux/bitfield.h>
>   #include <linux/cleanup.h>
>   #include <linux/component.h>
>   #include <linux/container_of.h>
> @@ -50,10 +51,17 @@
>   #define ACPI_AC_CLASS "ac_adapter"
>   #define ACPI_AC_NOTIFY_STATUS 0x80
>   
> +#define LWMI_FEATURE_ID_FAN_TEST 0x05
> +
> +#define LWMI_ATTR_ID_FAN_TEST							\
> +	(FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) |		\
> +	 FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_TEST))
> +
>   enum lwmi_cd_type {
>   	LENOVO_CAPABILITY_DATA_00,
>   	LENOVO_CAPABILITY_DATA_01,
>   	LENOVO_FAN_TEST_DATA,
> +	CD_TYPE_NONE = -1,
>   };
>   
>   #define LWMI_CD_TABLE_ITEM(_type)		\
> @@ -75,6 +83,20 @@ struct lwmi_cd_priv {
>   	struct notifier_block acpi_nb; /* ACPI events */
>   	struct wmi_device *wdev;
>   	struct cd_list *list;
> +
> +	/*
> +	 * A capdata device may be a component master of another capdata device.
> +	 * E.g., lenovo-wmi-other <-> capdata00 <-> capdata_fan
> +	 *       |- master            |- component
> +	 *                            |- sub-master
> +	 *                                          |- sub-component
> +	 */
> +	struct lwmi_cd_sub_master_priv {
> +		struct device *master_dev;
> +		cd_list_cb_t master_cb;
> +		struct cd_list *sub_component_list; /* ERR_PTR(-ENODEV) implies no sub-component. */
> +		bool registered;                    /* Has the sub-master been registered? */
> +	} *sub_master;
>   };
>   
>   struct cd_list {
> @@ -125,7 +147,7 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
>   		return;
>   
>   	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
> -		/* Skip optional interfaces. */
> +		/* Skip sub-components. */
>   		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
>   			continue;
>   
> @@ -137,6 +159,56 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
>   }
>   EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
>   
> +/**
> + * lwmi_cd_call_master_cb() - Call the master callback for the sub-component.
> + * @priv: Pointer to the capability data private data.
> + *
> + * Call the master callback and pass the sub-component list to it if the
> + * dependency chain (master <-> sub-master <-> sub-component) is complete.
> + */
> +static void lwmi_cd_call_master_cb(struct lwmi_cd_priv *priv)
> +{
> +	struct cd_list *sub_component_list = priv->sub_master->sub_component_list;
> +
> +	/*
> +	 * Call the callback only if the dependency chain is ready:
> +	 * - Binding between master and sub-master: fills master_dev and master_cb
> +	 * - Binding between sub-master and sub-component: fills sub_component_list
> +	 *
> +	 * If a binding has been unbound before the other binding is bound, the
> +	 * corresponding members filled by the former are guaranteed to be cleared.
> +	 *
> +	 * This function is only called in bind callbacks, and the component
> +	 * framework guarantees bind/unbind callbacks may never execute
> +	 * simultaneously, which implies that it's impossible to have a race
> +	 * condition.
> +	 *
> +	 * Hence, this check is sufficient to ensure that the callback is called
> +	 * at most once and with the correct state, without relying on a specific
> +	 * sequence of binding establishment.
> +	 */
> +	if (!sub_component_list ||
> +	    !priv->sub_master->master_dev ||
> +	    !priv->sub_master->master_cb)
> +		return;
> +
> +	if (PTR_ERR(sub_component_list) == -ENODEV)
> +		sub_component_list = NULL;
> +	else if (WARN_ON(IS_ERR(sub_component_list)))
> +		return;
> +
> +	priv->sub_master->master_cb(priv->sub_master->master_dev,
> +				    sub_component_list);
> +
> +	/*
> +	 * Prevent "unbind and rebind" sequences from userspace from calling the
> +	 * callback twice.
> +	 */
> +	priv->sub_master->master_cb = NULL;
> +	priv->sub_master->master_dev = NULL;
> +	priv->sub_master->sub_component_list = NULL;
> +}
> +
>   /**
>    * lwmi_cd_component_bind() - Bind component to master device.
>    * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
> @@ -147,6 +219,8 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
>    * list. This is used to call lwmi_cd*_get_data to look up attribute data
>    * from the lenovo-wmi-other driver.
>    *
> + * If cd_dev is a sub-master, try to call the master callback.
> + *
>    * Return: 0
>    */
>   static int lwmi_cd_component_bind(struct device *cd_dev,
> @@ -158,6 +232,11 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
>   	switch (priv->list->type) {
>   	case LENOVO_CAPABILITY_DATA_00:
>   		binder->cd00_list = priv->list;
> +
> +		priv->sub_master->master_dev = om_dev;
> +		priv->sub_master->master_cb = binder->cd_fan_list_cb;
> +		lwmi_cd_call_master_cb(priv);
> +
>   		break;
>   	case LENOVO_CAPABILITY_DATA_01:
>   		binder->cd01_list = priv->list;
> @@ -169,8 +248,167 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
>   	return 0;
>   }
>   
> +/**
> + * lwmi_cd_component_unbind() - Unbind component to master device.
> + * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
> + * @om_dev: Pointer to the lenovo-wmi-other driver parent device.
> + * @data: Unused.
> + *
> + * If cd_dev is a sub-master, clear the collected data from the master device to
> + * prevent the binding establishment between the sub-master and the sub-
> + * component (if it's about to happen) from calling the master callback.
> + */
> +static void lwmi_cd_component_unbind(struct device *cd_dev,
> +				     struct device *om_dev, void *data)
> +{
> +	struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
> +
> +	switch (priv->list->type) {
> +	case LENOVO_CAPABILITY_DATA_00:
> +		priv->sub_master->master_dev = NULL;
> +		priv->sub_master->master_cb = NULL;
> +		return;
> +	default:
> +		return;
> +	}
> +}
> +
>   static const struct component_ops lwmi_cd_component_ops = {
>   	.bind = lwmi_cd_component_bind,
> +	.unbind = lwmi_cd_component_unbind,
> +};
> +
> +/**
> + * lwmi_cd_sub_master_bind() - Bind sub-component of sub-master device
> + * @dev: The sub-master capdata basic device.
> + *
> + * Call component_bind_all to bind the sub-component device to the sub-master
> + * device. On success, collect the pointer to the sub-component list and try
> + * to call the master callback.
> + *
> + * Return: 0 on success, or an error code.
> + */
> +static int lwmi_cd_sub_master_bind(struct device *dev)
> +{
> +	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
> +	struct cd_list *sub_component_list;
> +	int ret;
> +
> +	ret = component_bind_all(dev, &sub_component_list);
> +	if (ret)
> +		return ret;
> +
> +	priv->sub_master->sub_component_list = sub_component_list;
> +	lwmi_cd_call_master_cb(priv);
> +
> +	return 0;
> +}
> +
> +/**
> + * lwmi_cd_sub_master_unbind() - Unbind sub-component of sub-master device
> + * @dev: The sub-master capdata basic device
> + *
> + * Clear the collected pointer to the sub-component list to prevent the binding
> + * establishment between the sub-master and the sub-component (if it's about to
> + * happen) from calling the master callback. Then, call component_unbind_all to
> + * unbind the sub-component device from the sub-master device.
> + */
> +static void lwmi_cd_sub_master_unbind(struct device *dev)
> +{
> +	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
> +
> +	priv->sub_master->sub_component_list = NULL;
> +
> +	component_unbind_all(dev, NULL);
> +}
> +
> +static const struct component_master_ops lwmi_cd_sub_master_ops = {
> +	.bind = lwmi_cd_sub_master_bind,
> +	.unbind = lwmi_cd_sub_master_unbind,
> +};
> +
> +/**
> + * lwmi_cd_sub_master_add() - Register a sub-master with its sub-component
> + * @priv: Pointer to the sub-master capdata device private data.
> + * @sub_component_type: Type of the sub-component.
> + *
> + * Match the sub-component type and register the current capdata device as a
> + * sub-master. If the given sub-component type is CD_TYPE_NONE, mark the sub-
> + * component as non-existent without registering sub-master.
> + *
> + * Return: 0 on success, or an error code.
> + */
> +static int lwmi_cd_sub_master_add(struct lwmi_cd_priv *priv,
> +				  enum lwmi_cd_type sub_component_type)
> +{
> +	struct component_match *master_match = NULL;
> +	int ret;
> +
> +	priv->sub_master = devm_kzalloc(&priv->wdev->dev, sizeof(*priv->sub_master), GFP_KERNEL);
> +	if (!priv->sub_master)
> +		return -ENOMEM;
> +
> +	if (sub_component_type == CD_TYPE_NONE) {
> +		/* The master callback will be called with NULL on bind. */
> +		priv->sub_master->sub_component_list = ERR_PTR(-ENODEV);
> +		priv->sub_master->registered = false;
> +		return 0;
> +	}
> +
> +	/*
> +	 * lwmi_cd_match() needs a pointer to enum lwmi_cd_type, but on-stack
> +	 * data cannot be used here. Steal one from lwmi_cd_table.
> +	 */
> +	component_match_add(&priv->wdev->dev, &master_match, lwmi_cd_match,
> +			    (void *)&lwmi_cd_table[sub_component_type].type);
> +	if (IS_ERR(master_match))
> +		return PTR_ERR(master_match);
> +
> +	ret = component_master_add_with_match(&priv->wdev->dev, &lwmi_cd_sub_master_ops,
> +					      master_match);
> +	if (ret)
> +		return ret;
> +
> +	priv->sub_master->registered = true;
> +	return 0;
> +}
> +
> +/**
> + * lwmi_cd_sub_master_del() - Unregister a sub-master if it's registered
> + * @priv: Pointer to the sub-master capdata device private data.
> + */
> +static void lwmi_cd_sub_master_del(struct lwmi_cd_priv *priv)
> +{
> +	if (priv->sub_master->registered) {
> +		component_master_del(&priv->wdev->dev, &lwmi_cd_sub_master_ops);
> +		priv->sub_master->registered = false;
> +	}
> +}
> +
> +/**
> + * lwmi_cd_sub_component_bind() - Bind sub-component to sub-master device.
> + * @sc_dev: Pointer to the sub-component capdata parent device.
> + * @sm_dev: Pointer to the sub-master capdata parent device.
> + * @data: Pointer used to return the capability data list pointer.
> + *
> + * On sub-master's bind, provide a pointer to the local capdata list.
> + * This is used by the sub-master to call the master callback.
> + *
> + * Return: 0
> + */
> +static int lwmi_cd_sub_component_bind(struct device *sc_dev,
> +				      struct device *sm_dev, void *data)
> +{
> +	struct lwmi_cd_priv *priv = dev_get_drvdata(sc_dev);
> +	struct cd_list **listp = data;
> +
> +	*listp = priv->list;
> +
> +	return 0;
> +}
> +
> +static const struct component_ops lwmi_cd_sub_component_ops = {
> +	.bind = lwmi_cd_sub_component_bind,
>   };
>   
>   /**
> @@ -470,9 +708,28 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
>   		goto out;
>   
>   	switch (info->type) {
> -	case LENOVO_CAPABILITY_DATA_00:
> +	case LENOVO_CAPABILITY_DATA_00: {
> +		enum lwmi_cd_type sub_component_type = LENOVO_FAN_TEST_DATA;
> +		struct capdata00 capdata00;
> +
> +		ret = lwmi_cd00_get_data(priv->list, LWMI_ATTR_ID_FAN_TEST, &capdata00);
> +		if (ret || !(capdata00.supported & LWMI_SUPP_VALID)) {
> +			dev_dbg(&wdev->dev, "capdata00 declares no fan test support\n");
> +			sub_component_type = CD_TYPE_NONE;
> +		}
> +
> +		/* Sub-master (capdata00) <-> sub-component (capdata_fan) */
> +		ret = lwmi_cd_sub_master_add(priv, sub_component_type);
> +		if (ret)
> +			goto out;
> +
> +		/* Master (lenovo-wmi-other) <-> sub-master (capdata00) */
>   		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
> +		if (ret)
> +			lwmi_cd_sub_master_del(priv);
> +
>   		goto out;
> +	}
>   	case LENOVO_CAPABILITY_DATA_01:
>   		priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
>   
> @@ -488,6 +745,7 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
>   		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
>   		goto out;
>   	case LENOVO_FAN_TEST_DATA:
> +		ret = component_add(&wdev->dev, &lwmi_cd_sub_component_ops);
>   		goto out;
>   	default:
>   		return -EINVAL;
> @@ -509,10 +767,13 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
>   
>   	switch (priv->list->type) {
>   	case LENOVO_CAPABILITY_DATA_00:
> +		lwmi_cd_sub_master_del(priv);
> +		fallthrough;
>   	case LENOVO_CAPABILITY_DATA_01:
>   		component_del(&wdev->dev, &lwmi_cd_component_ops);
>   		break;
>   	case LENOVO_FAN_TEST_DATA:
> +		component_del(&wdev->dev, &lwmi_cd_sub_component_ops);
>   		break;
>   	default:
>   		WARN_ON(1);
> diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
> index 38af4c4e4ef4..59ca3b3e5760 100644
> --- a/drivers/platform/x86/lenovo/wmi-capdata.h
> +++ b/drivers/platform/x86/lenovo/wmi-capdata.h
> @@ -5,8 +5,20 @@
>   #ifndef _LENOVO_WMI_CAPDATA_H_
>   #define _LENOVO_WMI_CAPDATA_H_
>   
> +#include <linux/bits.h>
>   #include <linux/types.h>
>   
> +#define LWMI_SUPP_VALID		BIT(0)
> +#define LWMI_SUPP_MAY_GET	(LWMI_SUPP_VALID | BIT(1))
> +#define LWMI_SUPP_MAY_SET	(LWMI_SUPP_VALID | BIT(2))
> +
> +#define LWMI_ATTR_DEV_ID_MASK	GENMASK(31, 24)
> +#define LWMI_ATTR_FEAT_ID_MASK	GENMASK(23, 16)
> +#define LWMI_ATTR_MODE_ID_MASK	GENMASK(15, 8)
> +#define LWMI_ATTR_TYPE_ID_MASK	GENMASK(7, 0)
> +
> +#define LWMI_DEVICE_ID_FAN	0x04
> +
>   struct component_match;
>   struct device;
>   struct cd_list;
> @@ -32,9 +44,17 @@ struct capdata_fan {
>   	u32 max_rpm;
>   };
>   
> +typedef void (*cd_list_cb_t)(struct device *master_dev, struct cd_list *cd_list);
> +
>   struct lwmi_cd_binder {
>   	struct cd_list *cd00_list;
>   	struct cd_list *cd01_list;
> +	/*
> +	 * May be called during or after the bind callback.
> +	 * Will be called with NULL if capdata_fan does not exist.
> +	 * The pointer is only valid in the callback; never keep it for later use!
> +	 */
> +	cd_list_cb_t cd_fan_list_cb;
>   };
>   
>   void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
> diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> index f2e1e34d58a9..b3adcc2804fa 100644
> --- a/drivers/platform/x86/lenovo/wmi-other.c
> +++ b/drivers/platform/x86/lenovo/wmi-other.c
> @@ -54,11 +54,6 @@
>   #define LWMI_FEATURE_VALUE_GET 17
>   #define LWMI_FEATURE_VALUE_SET 18
>   
> -#define LWMI_ATTR_DEV_ID_MASK GENMASK(31, 24)
> -#define LWMI_ATTR_FEAT_ID_MASK GENMASK(23, 16)
> -#define LWMI_ATTR_MODE_ID_MASK GENMASK(15, 8)
> -#define LWMI_ATTR_TYPE_ID_MASK GENMASK(7, 0)
> -
>   #define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other"
>   
>   static BLOCKING_NOTIFIER_HEAD(om_chain_head);

^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up Fan Test Data
  2025-11-23  3:36   ` Armin Wolf
@ 2025-11-23 15:02     ` Rong Zhang
  2025-11-25  2:08       ` Armin Wolf
  0 siblings, 1 reply; 22+ messages in thread
From: Rong Zhang @ 2025-11-23 15:02 UTC (permalink / raw)
  To: Armin Wolf, Mark Pearson, Derek J. Clark, Hans de Goede,
	Ilpo Järvinen
  Cc: Guenter Roeck, platform-driver-x86, linux-kernel, linux-hwmon

Hi Armin,

On Sun, 2025-11-23 at 04:36 +0100, Armin Wolf wrote:
> Am 22.11.25 um 19:44 schrieb Rong Zhang:
> 
> > A capdata00 attribute (0x04050000) describes the presence of Fan Test
> > Data. Query it, and bind Fan Test Data as a component of capdata00
> > accordingly. The component master of capdata00 may pass a callback while
> > binding to retrieve fan info from Fan Test Data.
> > 
> > Summarizing this scheme:
> > 
> > 	lenovo-wmi-other <-> capdata00 <-> capdata_fan
> > 	|- master            |- component
> > 	                     |- sub-master
> > 	                                   |- sub-component
> > 
> > The callback will be called once both the master and the sub-component
> > are bound to the sub-master (component).
> > 
> > This scheme is essential to solve these issues:
> > - The component framework only supports one aggregation per master
> > - A binding is only established until all components are found
> > - The Fan Test Data interface may be missing on some devices
> > - To get rid of queries for the presense of WMI GUIDs
> > 
> > capdata00 is registered as a component and a sub-master on probe,
> > instead of chaining the registerations in one's bind callback. This is
> > because calling (un)registration methods of the component framework
> > causes deadlock in (un)bind callbacks, i.e., it's impossible to register
> > capdata00 as a sub-master/component in its component/sub-master bind
> > callback, and vice versa.
> 
> I spend some time trying to understand all of this, and i have come to the conclusion
> that while passing a callback between both component masters, it still is not exactly
> clean.

Could you elaborate? Thanks.

> Can you instead use devm_lwmi_om_register_notifier() and define another event for querying
> the fan data? This way we could ditch the component submaster with all of the callback madness.

Thanks for your review and suggestion.

At first I thought the notifier approach was nice, however...

There are two directions to use the notifier framework:
(1) lenovo-wmi-other calls blocking_notifier_call_chain() to query
    capdata_fan
(2) capdata_fan calls blocking_notifier_call_chain() to provide data
    to lenovo-wmi-other

Both requires some assumptions on probing/binding sequence:
(1) capdata_fan must be probed before lenovo-wmi-other calling
    blocking_notifier_call_chain(), otherwise lenovo-wmi-other will
    mistakenly believe capdata_fan is missing
(2) lenovo-wmi-other must be probed before capdata_fan calling
    blocking_notifier_call_chain(), otherwise lenovo-wmi-other will
    wait for fan info forever

So eventually we need *both directions* to break the assumptions on
probing/binding sequence. Moreover, some extra synchronization
primitives may be required to make sure the binding between lenovo-wmi-
other and capdata00&01 won't be unbind while registering the HWMON
device.

> Sorry for being the one for suggesting the submaster approach in the first place, i did not
> know that the component framework is so limited when it comes to nested component masters :/

I'd say the sub-master approach is still the least ugly one.

The callback is always called in a bind callback. Hence, the component
framework protects our sanity, so we don't need to worry about the
other side of our component chain being messed up while handling one
side. The approach doesn't need any assumption on binding sequence as
the corresponding bind/unbind callbacks effectively create a state
machine.

> If Mark is OK with using the submaster approach then you can ignore the above suggestion.
> 
> Thanks,
> Armin Wolf

Thanks,
Rong

> > Signed-off-by: Rong Zhang <i@rong.moe>
> > ---
> > Changes in v6:
> > - Fix the error path of component_add(capdata00)
> > - Elaborate the design in commit message
> > 
> > Changes in v5:
> > - Fix missing include (thanks kernel test robot)
> > 
> > Changes in v4:
> > - New patch in the series (thanks Armin Wolf's inspiration)
> >    - Get rid of wmi_has_guid() (see also [PATCH v4 3/7])
> > ---
> >   drivers/platform/x86/lenovo/wmi-capdata.c | 265 +++++++++++++++++++++-
> >   drivers/platform/x86/lenovo/wmi-capdata.h |  20 ++
> >   drivers/platform/x86/lenovo/wmi-other.c   |   5 -
> >   3 files changed, 283 insertions(+), 7 deletions(-)
> > 
> > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
> > index e6392357395c..8760f8c071ca 100644
> > --- a/drivers/platform/x86/lenovo/wmi-capdata.c
> > +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
> > @@ -27,6 +27,7 @@
> >   #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> >   
> >   #include <linux/acpi.h>
> > +#include <linux/bitfield.h>
> >   #include <linux/cleanup.h>
> >   #include <linux/component.h>
> >   #include <linux/container_of.h>
> > @@ -50,10 +51,17 @@
> >   #define ACPI_AC_CLASS "ac_adapter"
> >   #define ACPI_AC_NOTIFY_STATUS 0x80
> >   
> > +#define LWMI_FEATURE_ID_FAN_TEST 0x05
> > +
> > +#define LWMI_ATTR_ID_FAN_TEST							\
> > +	(FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) |		\
> > +	 FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_TEST))
> > +
> >   enum lwmi_cd_type {
> >   	LENOVO_CAPABILITY_DATA_00,
> >   	LENOVO_CAPABILITY_DATA_01,
> >   	LENOVO_FAN_TEST_DATA,
> > +	CD_TYPE_NONE = -1,
> >   };
> >   
> >   #define LWMI_CD_TABLE_ITEM(_type)		\
> > @@ -75,6 +83,20 @@ struct lwmi_cd_priv {
> >   	struct notifier_block acpi_nb; /* ACPI events */
> >   	struct wmi_device *wdev;
> >   	struct cd_list *list;
> > +
> > +	/*
> > +	 * A capdata device may be a component master of another capdata device.
> > +	 * E.g., lenovo-wmi-other <-> capdata00 <-> capdata_fan
> > +	 *       |- master            |- component
> > +	 *                            |- sub-master
> > +	 *                                          |- sub-component
> > +	 */
> > +	struct lwmi_cd_sub_master_priv {
> > +		struct device *master_dev;
> > +		cd_list_cb_t master_cb;
> > +		struct cd_list *sub_component_list; /* ERR_PTR(-ENODEV) implies no sub-component. */
> > +		bool registered;                    /* Has the sub-master been registered? */
> > +	} *sub_master;
> >   };
> >   
> >   struct cd_list {
> > @@ -125,7 +147,7 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
> >   		return;
> >   
> >   	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
> > -		/* Skip optional interfaces. */
> > +		/* Skip sub-components. */
> >   		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
> >   			continue;
> >   
> > @@ -137,6 +159,56 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
> >   }
> >   EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
> >   
> > +/**
> > + * lwmi_cd_call_master_cb() - Call the master callback for the sub-component.
> > + * @priv: Pointer to the capability data private data.
> > + *
> > + * Call the master callback and pass the sub-component list to it if the
> > + * dependency chain (master <-> sub-master <-> sub-component) is complete.
> > + */
> > +static void lwmi_cd_call_master_cb(struct lwmi_cd_priv *priv)
> > +{
> > +	struct cd_list *sub_component_list = priv->sub_master->sub_component_list;
> > +
> > +	/*
> > +	 * Call the callback only if the dependency chain is ready:
> > +	 * - Binding between master and sub-master: fills master_dev and master_cb
> > +	 * - Binding between sub-master and sub-component: fills sub_component_list
> > +	 *
> > +	 * If a binding has been unbound before the other binding is bound, the
> > +	 * corresponding members filled by the former are guaranteed to be cleared.
> > +	 *
> > +	 * This function is only called in bind callbacks, and the component
> > +	 * framework guarantees bind/unbind callbacks may never execute
> > +	 * simultaneously, which implies that it's impossible to have a race
> > +	 * condition.
> > +	 *
> > +	 * Hence, this check is sufficient to ensure that the callback is called
> > +	 * at most once and with the correct state, without relying on a specific
> > +	 * sequence of binding establishment.
> > +	 */
> > +	if (!sub_component_list ||
> > +	    !priv->sub_master->master_dev ||
> > +	    !priv->sub_master->master_cb)
> > +		return;
> > +
> > +	if (PTR_ERR(sub_component_list) == -ENODEV)
> > +		sub_component_list = NULL;
> > +	else if (WARN_ON(IS_ERR(sub_component_list)))
> > +		return;
> > +
> > +	priv->sub_master->master_cb(priv->sub_master->master_dev,
> > +				    sub_component_list);
> > +
> > +	/*
> > +	 * Prevent "unbind and rebind" sequences from userspace from calling the
> > +	 * callback twice.
> > +	 */
> > +	priv->sub_master->master_cb = NULL;
> > +	priv->sub_master->master_dev = NULL;
> > +	priv->sub_master->sub_component_list = NULL;
> > +}
> > +
> >   /**
> >    * lwmi_cd_component_bind() - Bind component to master device.
> >    * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
> > @@ -147,6 +219,8 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
> >    * list. This is used to call lwmi_cd*_get_data to look up attribute data
> >    * from the lenovo-wmi-other driver.
> >    *
> > + * If cd_dev is a sub-master, try to call the master callback.
> > + *
> >    * Return: 0
> >    */
> >   static int lwmi_cd_component_bind(struct device *cd_dev,
> > @@ -158,6 +232,11 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
> >   	switch (priv->list->type) {
> >   	case LENOVO_CAPABILITY_DATA_00:
> >   		binder->cd00_list = priv->list;
> > +
> > +		priv->sub_master->master_dev = om_dev;
> > +		priv->sub_master->master_cb = binder->cd_fan_list_cb;
> > +		lwmi_cd_call_master_cb(priv);
> > +
> >   		break;
> >   	case LENOVO_CAPABILITY_DATA_01:
> >   		binder->cd01_list = priv->list;
> > @@ -169,8 +248,167 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
> >   	return 0;
> >   }
> >   
> > +/**
> > + * lwmi_cd_component_unbind() - Unbind component to master device.
> > + * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
> > + * @om_dev: Pointer to the lenovo-wmi-other driver parent device.
> > + * @data: Unused.
> > + *
> > + * If cd_dev is a sub-master, clear the collected data from the master device to
> > + * prevent the binding establishment between the sub-master and the sub-
> > + * component (if it's about to happen) from calling the master callback.
> > + */
> > +static void lwmi_cd_component_unbind(struct device *cd_dev,
> > +				     struct device *om_dev, void *data)
> > +{
> > +	struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
> > +
> > +	switch (priv->list->type) {
> > +	case LENOVO_CAPABILITY_DATA_00:
> > +		priv->sub_master->master_dev = NULL;
> > +		priv->sub_master->master_cb = NULL;
> > +		return;
> > +	default:
> > +		return;
> > +	}
> > +}
> > +
> >   static const struct component_ops lwmi_cd_component_ops = {
> >   	.bind = lwmi_cd_component_bind,
> > +	.unbind = lwmi_cd_component_unbind,
> > +};
> > +
> > +/**
> > + * lwmi_cd_sub_master_bind() - Bind sub-component of sub-master device
> > + * @dev: The sub-master capdata basic device.
> > + *
> > + * Call component_bind_all to bind the sub-component device to the sub-master
> > + * device. On success, collect the pointer to the sub-component list and try
> > + * to call the master callback.
> > + *
> > + * Return: 0 on success, or an error code.
> > + */
> > +static int lwmi_cd_sub_master_bind(struct device *dev)
> > +{
> > +	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
> > +	struct cd_list *sub_component_list;
> > +	int ret;
> > +
> > +	ret = component_bind_all(dev, &sub_component_list);
> > +	if (ret)
> > +		return ret;
> > +
> > +	priv->sub_master->sub_component_list = sub_component_list;
> > +	lwmi_cd_call_master_cb(priv);
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * lwmi_cd_sub_master_unbind() - Unbind sub-component of sub-master device
> > + * @dev: The sub-master capdata basic device
> > + *
> > + * Clear the collected pointer to the sub-component list to prevent the binding
> > + * establishment between the sub-master and the sub-component (if it's about to
> > + * happen) from calling the master callback. Then, call component_unbind_all to
> > + * unbind the sub-component device from the sub-master device.
> > + */
> > +static void lwmi_cd_sub_master_unbind(struct device *dev)
> > +{
> > +	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
> > +
> > +	priv->sub_master->sub_component_list = NULL;
> > +
> > +	component_unbind_all(dev, NULL);
> > +}
> > +
> > +static const struct component_master_ops lwmi_cd_sub_master_ops = {
> > +	.bind = lwmi_cd_sub_master_bind,
> > +	.unbind = lwmi_cd_sub_master_unbind,
> > +};
> > +
> > +/**
> > + * lwmi_cd_sub_master_add() - Register a sub-master with its sub-component
> > + * @priv: Pointer to the sub-master capdata device private data.
> > + * @sub_component_type: Type of the sub-component.
> > + *
> > + * Match the sub-component type and register the current capdata device as a
> > + * sub-master. If the given sub-component type is CD_TYPE_NONE, mark the sub-
> > + * component as non-existent without registering sub-master.
> > + *
> > + * Return: 0 on success, or an error code.
> > + */
> > +static int lwmi_cd_sub_master_add(struct lwmi_cd_priv *priv,
> > +				  enum lwmi_cd_type sub_component_type)
> > +{
> > +	struct component_match *master_match = NULL;
> > +	int ret;
> > +
> > +	priv->sub_master = devm_kzalloc(&priv->wdev->dev, sizeof(*priv->sub_master), GFP_KERNEL);
> > +	if (!priv->sub_master)
> > +		return -ENOMEM;
> > +
> > +	if (sub_component_type == CD_TYPE_NONE) {
> > +		/* The master callback will be called with NULL on bind. */
> > +		priv->sub_master->sub_component_list = ERR_PTR(-ENODEV);
> > +		priv->sub_master->registered = false;
> > +		return 0;
> > +	}
> > +
> > +	/*
> > +	 * lwmi_cd_match() needs a pointer to enum lwmi_cd_type, but on-stack
> > +	 * data cannot be used here. Steal one from lwmi_cd_table.
> > +	 */
> > +	component_match_add(&priv->wdev->dev, &master_match, lwmi_cd_match,
> > +			    (void *)&lwmi_cd_table[sub_component_type].type);
> > +	if (IS_ERR(master_match))
> > +		return PTR_ERR(master_match);
> > +
> > +	ret = component_master_add_with_match(&priv->wdev->dev, &lwmi_cd_sub_master_ops,
> > +					      master_match);
> > +	if (ret)
> > +		return ret;
> > +
> > +	priv->sub_master->registered = true;
> > +	return 0;
> > +}
> > +
> > +/**
> > + * lwmi_cd_sub_master_del() - Unregister a sub-master if it's registered
> > + * @priv: Pointer to the sub-master capdata device private data.
> > + */
> > +static void lwmi_cd_sub_master_del(struct lwmi_cd_priv *priv)
> > +{
> > +	if (priv->sub_master->registered) {
> > +		component_master_del(&priv->wdev->dev, &lwmi_cd_sub_master_ops);
> > +		priv->sub_master->registered = false;
> > +	}
> > +}
> > +
> > +/**
> > + * lwmi_cd_sub_component_bind() - Bind sub-component to sub-master device.
> > + * @sc_dev: Pointer to the sub-component capdata parent device.
> > + * @sm_dev: Pointer to the sub-master capdata parent device.
> > + * @data: Pointer used to return the capability data list pointer.
> > + *
> > + * On sub-master's bind, provide a pointer to the local capdata list.
> > + * This is used by the sub-master to call the master callback.
> > + *
> > + * Return: 0
> > + */
> > +static int lwmi_cd_sub_component_bind(struct device *sc_dev,
> > +				      struct device *sm_dev, void *data)
> > +{
> > +	struct lwmi_cd_priv *priv = dev_get_drvdata(sc_dev);
> > +	struct cd_list **listp = data;
> > +
> > +	*listp = priv->list;
> > +
> > +	return 0;
> > +}
> > +
> > +static const struct component_ops lwmi_cd_sub_component_ops = {
> > +	.bind = lwmi_cd_sub_component_bind,
> >   };
> >   
> >   /**
> > @@ -470,9 +708,28 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
> >   		goto out;
> >   
> >   	switch (info->type) {
> > -	case LENOVO_CAPABILITY_DATA_00:
> > +	case LENOVO_CAPABILITY_DATA_00: {
> > +		enum lwmi_cd_type sub_component_type = LENOVO_FAN_TEST_DATA;
> > +		struct capdata00 capdata00;
> > +
> > +		ret = lwmi_cd00_get_data(priv->list, LWMI_ATTR_ID_FAN_TEST, &capdata00);
> > +		if (ret || !(capdata00.supported & LWMI_SUPP_VALID)) {
> > +			dev_dbg(&wdev->dev, "capdata00 declares no fan test support\n");
> > +			sub_component_type = CD_TYPE_NONE;
> > +		}
> > +
> > +		/* Sub-master (capdata00) <-> sub-component (capdata_fan) */
> > +		ret = lwmi_cd_sub_master_add(priv, sub_component_type);
> > +		if (ret)
> > +			goto out;
> > +
> > +		/* Master (lenovo-wmi-other) <-> sub-master (capdata00) */
> >   		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
> > +		if (ret)
> > +			lwmi_cd_sub_master_del(priv);
> > +
> >   		goto out;
> > +	}
> >   	case LENOVO_CAPABILITY_DATA_01:
> >   		priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
> >   
> > @@ -488,6 +745,7 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
> >   		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
> >   		goto out;
> >   	case LENOVO_FAN_TEST_DATA:
> > +		ret = component_add(&wdev->dev, &lwmi_cd_sub_component_ops);
> >   		goto out;
> >   	default:
> >   		return -EINVAL;
> > @@ -509,10 +767,13 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
> >   
> >   	switch (priv->list->type) {
> >   	case LENOVO_CAPABILITY_DATA_00:
> > +		lwmi_cd_sub_master_del(priv);
> > +		fallthrough;
> >   	case LENOVO_CAPABILITY_DATA_01:
> >   		component_del(&wdev->dev, &lwmi_cd_component_ops);
> >   		break;
> >   	case LENOVO_FAN_TEST_DATA:
> > +		component_del(&wdev->dev, &lwmi_cd_sub_component_ops);
> >   		break;
> >   	default:
> >   		WARN_ON(1);
> > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
> > index 38af4c4e4ef4..59ca3b3e5760 100644
> > --- a/drivers/platform/x86/lenovo/wmi-capdata.h
> > +++ b/drivers/platform/x86/lenovo/wmi-capdata.h
> > @@ -5,8 +5,20 @@
> >   #ifndef _LENOVO_WMI_CAPDATA_H_
> >   #define _LENOVO_WMI_CAPDATA_H_
> >   
> > +#include <linux/bits.h>
> >   #include <linux/types.h>
> >   
> > +#define LWMI_SUPP_VALID		BIT(0)
> > +#define LWMI_SUPP_MAY_GET	(LWMI_SUPP_VALID | BIT(1))
> > +#define LWMI_SUPP_MAY_SET	(LWMI_SUPP_VALID | BIT(2))
> > +
> > +#define LWMI_ATTR_DEV_ID_MASK	GENMASK(31, 24)
> > +#define LWMI_ATTR_FEAT_ID_MASK	GENMASK(23, 16)
> > +#define LWMI_ATTR_MODE_ID_MASK	GENMASK(15, 8)
> > +#define LWMI_ATTR_TYPE_ID_MASK	GENMASK(7, 0)
> > +
> > +#define LWMI_DEVICE_ID_FAN	0x04
> > +
> >   struct component_match;
> >   struct device;
> >   struct cd_list;
> > @@ -32,9 +44,17 @@ struct capdata_fan {
> >   	u32 max_rpm;
> >   };
> >   
> > +typedef void (*cd_list_cb_t)(struct device *master_dev, struct cd_list *cd_list);
> > +
> >   struct lwmi_cd_binder {
> >   	struct cd_list *cd00_list;
> >   	struct cd_list *cd01_list;
> > +	/*
> > +	 * May be called during or after the bind callback.
> > +	 * Will be called with NULL if capdata_fan does not exist.
> > +	 * The pointer is only valid in the callback; never keep it for later use!
> > +	 */
> > +	cd_list_cb_t cd_fan_list_cb;
> >   };
> >   
> >   void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
> > diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> > index f2e1e34d58a9..b3adcc2804fa 100644
> > --- a/drivers/platform/x86/lenovo/wmi-other.c
> > +++ b/drivers/platform/x86/lenovo/wmi-other.c
> > @@ -54,11 +54,6 @@
> >   #define LWMI_FEATURE_VALUE_GET 17
> >   #define LWMI_FEATURE_VALUE_SET 18
> >   
> > -#define LWMI_ATTR_DEV_ID_MASK GENMASK(31, 24)
> > -#define LWMI_ATTR_FEAT_ID_MASK GENMASK(23, 16)
> > -#define LWMI_ATTR_MODE_ID_MASK GENMASK(15, 8)
> > -#define LWMI_ATTR_TYPE_ID_MASK GENMASK(7, 0)
> > -
> >   #define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other"
> >   
> >   static BLOCKING_NOTIFIER_HEAD(om_chain_head);

^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 3/7] platform/x86: lenovo-wmi-{capdata,other}: Support multiple Capability Data
  2025-11-22 18:44 ` [PATCH v6 3/7] platform/x86: lenovo-wmi-{capdata,other}: Support multiple Capability Data Rong Zhang
@ 2025-11-24 16:30   ` Ilpo Järvinen
  2025-11-24 20:03     ` Rong Zhang
  0 siblings, 1 reply; 22+ messages in thread
From: Ilpo Järvinen @ 2025-11-24 16:30 UTC (permalink / raw)
  To: Rong Zhang
  Cc: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Guenter Roeck, platform-driver-x86, LKML, linux-hwmon

On Sun, 23 Nov 2025, Rong Zhang wrote:

> The current implementation are heavily bound to capdata01. Rewrite it so
> that it is suitable to utilize other Capability Data as well.
> 
> No functional change intended.
> 
> Signed-off-by: Rong Zhang <i@rong.moe>
> Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
> Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
> ---
> Changes in v6:
> - Fix the error path of lwmi_cd_match_add_all()
>   - IS_ERR(matchptr) => IS_ERR(*matchptr)
> 
> Changes in v5:
> - Do not cast pointer to non-pointer or vice versa (thanks kernel test
>   robot)
> 
> Changes in v4:
> - Get rid of wmi_has_guid() (thanks Armin Wolf)
>   - More changes in [PATCH v4 6/7]
> - Prepare for [PATCH v4 6/7]
>   - Move lwmi_cd_match*() forward
>   - Use switch-case in lwmi_cd_remove()
> 
> Changes in v2:
> - Fix function parameter 'type' not described in 'lwmi_cd_match' (thanks
>   kernel test bot)
> ---
>  drivers/platform/x86/lenovo/wmi-capdata.c | 225 +++++++++++++++++-----
>  drivers/platform/x86/lenovo/wmi-capdata.h |   7 +-
>  drivers/platform/x86/lenovo/wmi-other.c   |  16 +-
>  3 files changed, 189 insertions(+), 59 deletions(-)
> 
> diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
> index c5e74b2bfeb3..e0710354278a 100644
> --- a/drivers/platform/x86/lenovo/wmi-capdata.c
> +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
> @@ -12,8 +12,13 @@
>   *
>   * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
>   *   - Initial implementation (formerly named lenovo-wmi-capdata01)
> + *
> + * Copyright (C) 2025 Rong Zhang <i@rong.moe>
> + *   - Unified implementation
>   */
>  
> +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> +
>  #include <linux/acpi.h>
>  #include <linux/cleanup.h>
>  #include <linux/component.h>
> @@ -36,6 +41,23 @@
>  #define ACPI_AC_CLASS "ac_adapter"
>  #define ACPI_AC_NOTIFY_STATUS 0x80
>  
> +enum lwmi_cd_type {
> +	LENOVO_CAPABILITY_DATA_01,
> +};
> +
> +#define LWMI_CD_TABLE_ITEM(_type)		\
> +	[_type] = {				\
> +		.name = #_type,			\
> +		.type = _type,			\
> +	}
> +
> +static const struct lwmi_cd_info {
> +	const char *name;
> +	enum lwmi_cd_type type;
> +} lwmi_cd_table[] = {
> +	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
> +};
> +
>  struct lwmi_cd_priv {
>  	struct notifier_block acpi_nb; /* ACPI events */
>  	struct wmi_device *wdev;
> @@ -44,15 +66,63 @@ struct lwmi_cd_priv {
>  
>  struct cd_list {
>  	struct mutex list_mutex; /* list R/W mutex */
> +	enum lwmi_cd_type type;
>  	u8 count;
> -	struct capdata01 data[];
> +
> +	union {
> +		DECLARE_FLEX_ARRAY(struct capdata01, cd01);

Add #include

> +	};
>  };
>  
> +static struct wmi_driver lwmi_cd_driver;
> +
> +/**
> + * lwmi_cd_match() - Match rule for the master driver.
> + * @dev: Pointer to the capability data parent device.
> + * @type: Pointer to capability data type (enum lwmi_cd_type *) to match.
> + *
> + * Return: int.
> + */
> +static int lwmi_cd_match(struct device *dev, void *type)
> +{
> +	struct lwmi_cd_priv *priv;
> +
> +	if (dev->driver != &lwmi_cd_driver.driver)
> +		return false;
> +
> +	priv = dev_get_drvdata(dev);
> +	return priv->list->type == *(enum lwmi_cd_type *)type;
> +}
> +
> +/**
> + * lwmi_cd_match_add_all() - Add all match rule for the master driver.
> + * @master: Pointer to the master device.
> + * @matchptr: Pointer to the returned component_match pointer.
> + *
> + * Adds all component matches to the list stored in @matchptr for the @master
> + * device. @matchptr must be initialized to NULL.
> + */
> +void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr)
> +{
> +	int i;
> +
> +	if (WARN_ON(*matchptr))

Add #include

> +		return;
> +
> +	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
> +		component_match_add(master, matchptr, lwmi_cd_match,
> +				    (void *)&lwmi_cd_table[i].type);

As the cast to void pointer necessary here? Usually pointer can be cast to 
void * implicitly (but I'm not sure about this case type seems enum).

> +		if (IS_ERR(*matchptr))

Add #include

> +			return;
> +	}
> +}
> +EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
> +
>  /**
>   * lwmi_cd_component_bind() - Bind component to master device.
>   * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
>   * @om_dev: Pointer to the lenovo-wmi-other driver parent device.
> - * @data: cd_list object pointer used to return the capability data.
> + * @data: lwmi_cd_binder object pointer used to return the capability data.
>   *
>   * On lenovo-wmi-other's master bind, provide a pointer to the local capdata
>   * list. This is used to call lwmi_cd*_get_data to look up attribute data
> @@ -64,9 +134,15 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
>  				  struct device *om_dev, void *data)
>  {
>  	struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
> -	struct cd_list **cd_list = data;
> +	struct lwmi_cd_binder *binder = data;
>  
> -	*cd_list = priv->list;
> +	switch (priv->list->type) {
> +	case LENOVO_CAPABILITY_DATA_01:
> +		binder->cd01_list = priv->list;
> +		break;
> +	default:
> +		return -EINVAL;
> +	}
>  
>  	return 0;
>  }
> @@ -76,30 +152,33 @@ static const struct component_ops lwmi_cd_component_ops = {
>  };
>  
>  /**
> - * lwmi_cd01_get_data - Get the data of the specified attribute
> + * lwmi_cd*_get_data - Get the data of the specified attribute
>   * @list: The lenovo-wmi-capdata pointer to its cd_list struct.
>   * @attribute_id: The capdata attribute ID to be found.
> - * @output: Pointer to a capdata01 struct to return the data.
> + * @output: Pointer to a capdata* struct to return the data.
>   *
> - * Retrieves the capability data 01 struct pointer for the given
> - * attribute for its specified thermal mode.
> + * Retrieves the capability data struct pointer for the given
> + * attribute.
>   *
>   * Return: 0 on success, or -EINVAL.
>   */
> -int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output)
> -{
> -	u8 idx;
> -
> -	guard(mutex)(&list->list_mutex);
> -	for (idx = 0; idx < list->count; idx++) {
> -		if (list->data[idx].id != attribute_id)
> -			continue;
> -		memcpy(output, &list->data[idx], sizeof(list->data[idx]));
> -		return 0;
> +#define DEF_LWMI_CDXX_GET_DATA(_cdxx, _cd_type, _output_t)					\
> +	int lwmi_##_cdxx##_get_data(struct cd_list *list, u32 attribute_id, _output_t *output)	\
> +	{											\
> +		u8 idx;										\

Please follow the normal conversions, so add an empty line here (with the
backslash contination, obviusly).

> +		if (WARN_ON(list->type != _cd_type))						\
> +			return -EINVAL;								\

Empty line here too to help readability?

> +		guard(mutex)(&list->list_mutex);						\
> +		for (idx = 0; idx < list->count; idx++) {					\
> +			if (list->_cdxx[idx].id != attribute_id)				\
> +				continue;							\
> +			memcpy(output, &list->_cdxx[idx], sizeof(list->_cdxx[idx]));		\
> +			return 0;								\
> +		}										\
> +		return -EINVAL;									\
>  	}
>  
> -	return -EINVAL;
> -}
> +DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
>  EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
>  
>  /**
> @@ -112,10 +191,21 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
>   */
>  static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
>  {
> +	size_t size;
>  	int idx;
> +	void *p;
> +
> +	switch (priv->list->type) {
> +	case LENOVO_CAPABILITY_DATA_01:
> +		p = &priv->list->cd01[0];
> +		size = sizeof(priv->list->cd01[0]);
> +		break;
> +	default:
> +		return -EINVAL;
> +	}
>  
>  	guard(mutex)(&priv->list->list_mutex);
> -	for (idx = 0; idx < priv->list->count; idx++) {
> +	for (idx = 0; idx < priv->list->count; idx++, p += size) {
>  		union acpi_object *ret_obj __free(kfree) = NULL;
>  
>  		ret_obj = wmidev_block_query(priv->wdev, idx);
> @@ -123,11 +213,10 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
>  			return -ENODEV;
>  
>  		if (ret_obj->type != ACPI_TYPE_BUFFER ||
> -		    ret_obj->buffer.length < sizeof(priv->list->data[idx]))
> +		    ret_obj->buffer.length < size)
>  			continue;
>  
> -		memcpy(&priv->list->data[idx], ret_obj->buffer.pointer,
> -		       ret_obj->buffer.length);
> +		memcpy(p, ret_obj->buffer.pointer, size);
>  	}
>  
>  	return 0;
> @@ -136,20 +225,28 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
>  /**
>   * lwmi_cd_alloc() - Allocate a cd_list struct in drvdata
>   * @priv: lenovo-wmi-capdata driver data.
> + * @type: The type of capability data.
>   *
>   * Allocate a cd_list struct large enough to contain data from all WMI data
>   * blocks provided by the interface.
>   *
>   * Return: 0 on success, or an error.
>   */
> -static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
> +static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
>  {
>  	struct cd_list *list;
>  	size_t list_size;
>  	int count, ret;
>  
>  	count = wmidev_instance_count(priv->wdev);
> -	list_size = struct_size(list, data, count);
> +
> +	switch (type) {
> +	case LENOVO_CAPABILITY_DATA_01:
> +		list_size = struct_size(list, cd01, count);
> +		break;
> +	default:
> +		return -EINVAL;
> +	}
>  
>  	list = devm_kzalloc(&priv->wdev->dev, list_size, GFP_KERNEL);
>  	if (!list)
> @@ -159,6 +256,7 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
>  	if (ret)
>  		return ret;
>  
> +	list->type = type;
>  	list->count = count;
>  	priv->list = list;
>  
> @@ -168,6 +266,7 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
>  /**
>   * lwmi_cd_setup() - Cache all WMI data block information
>   * @priv: lenovo-wmi-capdata driver data.
> + * @type: The type of capability data.
>   *
>   * Allocate a cd_list struct large enough to contain data from all WMI data
>   * blocks provided by the interface. Then loop through each data block and
> @@ -175,11 +274,11 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
>   *
>   * Return: 0 on success, or an error code.
>   */
> -static int lwmi_cd_setup(struct lwmi_cd_priv *priv)
> +static int lwmi_cd_setup(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
>  {
>  	int ret;
>  
> -	ret = lwmi_cd_alloc(priv);
> +	ret = lwmi_cd_alloc(priv, type);
>  	if (ret)
>  		return ret;
>  
> @@ -235,9 +334,13 @@ static void lwmi_cd01_unregister(void *data)
>  
>  static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
>  {
> +	const struct lwmi_cd_info *info = context;
>  	struct lwmi_cd_priv *priv;
>  	int ret;
>  
> +	if (!info)
> +		return -EINVAL;
> +
>  	priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
>  	if (!priv)
>  		return -ENOMEM;
> @@ -245,30 +348,58 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
>  	priv->wdev = wdev;
>  	dev_set_drvdata(&wdev->dev, priv);
>  
> -	ret = lwmi_cd_setup(priv);
> +	ret = lwmi_cd_setup(priv, info->type);
>  	if (ret)
> -		return ret;
> +		goto out;
>  
> -	priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
> +	switch (info->type) {
> +	case LENOVO_CAPABILITY_DATA_01:
> +		priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
>  
> -	ret = register_acpi_notifier(&priv->acpi_nb);
> -	if (ret)
> -		return ret;
> +		ret = register_acpi_notifier(&priv->acpi_nb);
> +		if (ret)
> +			goto out;
>  
> -	ret = devm_add_action_or_reset(&wdev->dev, lwmi_cd01_unregister, &priv->acpi_nb);
> -	if (ret)
> -		return ret;
> +		ret = devm_add_action_or_reset(&wdev->dev, lwmi_cd01_unregister,
> +					       &priv->acpi_nb);
> +		if (ret)
> +			goto out;
>  
> -	return component_add(&wdev->dev, &lwmi_cd_component_ops);
> +		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
> +		goto out;
> +	default:
> +		return -EINVAL;
> +	}
> +out:
> +	if (ret) {
> +		dev_err(&wdev->dev, "failed to register %s: %d\n",

Include?

> +			info->name, ret);
> +	} else {
> +		dev_info(&wdev->dev, "registered %s with %u items\n",
> +			 info->name, priv->list->count);

Success path should be quiet.

> +	}
> +	return ret;
>  }
>  
>  static void lwmi_cd_remove(struct wmi_device *wdev)
>  {
> -	component_del(&wdev->dev, &lwmi_cd_component_ops);
> +	struct lwmi_cd_priv *priv = dev_get_drvdata(&wdev->dev);
> +
> +	switch (priv->list->type) {
> +	case LENOVO_CAPABILITY_DATA_01:
> +		component_del(&wdev->dev, &lwmi_cd_component_ops);
> +		break;
> +	default:
> +		WARN_ON(1);
> +	}
>  }
>  
> +#define LWMI_CD_WDEV_ID(_type)				\
> +	.guid_string = _type##_GUID,			\
> +	.context = &lwmi_cd_table[_type]

Add comma.

> +
>  static const struct wmi_device_id lwmi_cd_id_table[] = {
> -	{ LENOVO_CAPABILITY_DATA_01_GUID, NULL },
> +	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_01) },
>  	{}
>  };
>  
> @@ -283,22 +414,10 @@ static struct wmi_driver lwmi_cd_driver = {
>  	.no_singleton = true,
>  };
>  
> -/**
> - * lwmi_cd01_match() - Match rule for the master driver.
> - * @dev: Pointer to the capability data 01 parent device.
> - * @data: Unused void pointer for passing match criteria.
> - *
> - * Return: int.
> - */
> -int lwmi_cd01_match(struct device *dev, void *data)
> -{
> -	return dev->driver == &lwmi_cd_driver.driver;
> -}
> -EXPORT_SYMBOL_NS_GPL(lwmi_cd01_match, "LENOVO_WMI_CD");
> -
>  module_wmi_driver(lwmi_cd_driver);
>  
>  MODULE_DEVICE_TABLE(wmi, lwmi_cd_id_table);
>  MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
> +MODULE_AUTHOR("Rong Zhang <i@rong.moe>");
>  MODULE_DESCRIPTION("Lenovo Capability Data WMI Driver");
>  MODULE_LICENSE("GPL");
> diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
> index 2a4746e38ad4..d326f9d2d165 100644
> --- a/drivers/platform/x86/lenovo/wmi-capdata.h
> +++ b/drivers/platform/x86/lenovo/wmi-capdata.h
> @@ -7,6 +7,7 @@
>  
>  #include <linux/types.h>
>  
> +struct component_match;
>  struct device;
>  struct cd_list;
>  
> @@ -19,7 +20,11 @@ struct capdata01 {
>  	u32 max_value;
>  };
>  
> +struct lwmi_cd_binder {
> +	struct cd_list *cd01_list;
> +};
> +
> +void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
>  int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output);
> -int lwmi_cd01_match(struct device *dev, void *data);
>  
>  #endif /* !_LENOVO_WMI_CAPDATA_H_ */
> diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> index c6dc1b4cff84..f2e1e34d58a9 100644
> --- a/drivers/platform/x86/lenovo/wmi-other.c
> +++ b/drivers/platform/x86/lenovo/wmi-other.c
> @@ -579,14 +579,14 @@ static void lwmi_om_fw_attr_remove(struct lwmi_om_priv *priv)
>  static int lwmi_om_master_bind(struct device *dev)
>  {
>  	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> -	struct cd_list *tmp_list;
> +	struct lwmi_cd_binder binder = { 0 };

= {} is enough to initialize to default values.

>  	int ret;
>  
> -	ret = component_bind_all(dev, &tmp_list);
> +	ret = component_bind_all(dev, &binder);
>  	if (ret)
>  		return ret;
>  
> -	priv->cd01_list = tmp_list;
> +	priv->cd01_list = binder.cd01_list;
>  	if (!priv->cd01_list)
>  		return -ENODEV;
>  
> @@ -623,10 +623,13 @@ static int lwmi_other_probe(struct wmi_device *wdev, const void *context)
>  	if (!priv)
>  		return -ENOMEM;
>  
> +	/* Sentinel for on-demand ida_free(). */
> +	priv->ida_id = -EIDRM;
> +
>  	priv->wdev = wdev;
>  	dev_set_drvdata(&wdev->dev, priv);
>  
> -	component_match_add(&wdev->dev, &master_match, lwmi_cd01_match, NULL);
> +	lwmi_cd_match_add_all(&wdev->dev, &master_match);
>  	if (IS_ERR(master_match))
>  		return PTR_ERR(master_match);
>  
> @@ -639,7 +642,10 @@ static void lwmi_other_remove(struct wmi_device *wdev)
>  	struct lwmi_om_priv *priv = dev_get_drvdata(&wdev->dev);
>  
>  	component_master_del(&wdev->dev, &lwmi_om_master_ops);
> -	ida_free(&lwmi_om_ida, priv->ida_id);
> +
> +	/* No IDA to free if the driver is never bound to its components. */
> +	if (priv->ida_id >= 0)
> +		ida_free(&lwmi_om_ida, priv->ida_id);
>  }
>  
>  static const struct wmi_device_id lwmi_other_id_table[] = {
> 

-- 
 i.


^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 5/7] platform/x86: lenovo-wmi-capdata: Add support for Fan Test Data
  2025-11-22 18:44 ` [PATCH v6 5/7] platform/x86: lenovo-wmi-capdata: Add support for Fan Test Data Rong Zhang
@ 2025-11-24 16:45   ` Ilpo Järvinen
  2025-11-24 20:07     ` Rong Zhang
  0 siblings, 1 reply; 22+ messages in thread
From: Ilpo Järvinen @ 2025-11-24 16:45 UTC (permalink / raw)
  To: Rong Zhang
  Cc: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Guenter Roeck, platform-driver-x86, LKML, linux-hwmon

On Sun, 23 Nov 2025, Rong Zhang wrote:

> Add support for LENOVO_FAN_TEST_DATA WMI data block. Provides an
> interface for querying the min/max fan speed RPM (reference data) of a
> given fan ID.
> 
> This interface is optional. Hence, it does not bind to lenovo-wmi-other
> and is not registered as a component for the moment. Appropriate binding
> will be implemented in the subsequent patch.
> 
> Signed-off-by: Rong Zhang <i@rong.moe>
> Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
> Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
> ---
> Changes in v4:
> - Rebase on top of changes made to [PATCH v4 3/7]
> - Do not register it as a component until [PATCH v4 6/7]
> 
> Changes in v2:
> - Reword documentation
> ---
>  .../wmi/devices/lenovo-wmi-other.rst          |  17 +++
>  drivers/platform/x86/lenovo/wmi-capdata.c     | 102 ++++++++++++++++++
>  drivers/platform/x86/lenovo/wmi-capdata.h     |   7 ++
>  3 files changed, 126 insertions(+)
> 
> diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
> index fcad595d49af..821282e07d93 100644
> --- a/Documentation/wmi/devices/lenovo-wmi-other.rst
> +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
> @@ -62,6 +62,13 @@ The following firmware-attributes are implemented:
>   - ppt_pl2_sppt: Platform Profile Tracking Slow Package Power Tracking
>   - ppt_pl3_fppt: Platform Profile Tracking Fast Package Power Tracking
>  
> +LENOVO_FAN_TEST_DATA
> +-------------------------
> +
> +WMI GUID ``B642801B-3D21-45DE-90AE-6E86F164FB21``
> +
> +The LENOVO_FAN_TEST_DATA interface provides reference data for self-test of
> +cooling fans.
>  
>  WMI interface description
>  =========================
> @@ -115,3 +122,13 @@ data using the `bmfdec <https://github.com/pali/bmfdec>`_ utility:
>      [WmiDataId(3), read, Description("Data Size.")] uint32 DataSize;
>      [WmiDataId(4), read, Description("Default Value"), WmiSizeIs("DataSize")] uint8 DefaultValue[];
>    };
> +
> +  [WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"), Description("Definition of Fan Test Data"), guid("{B642801B-3D21-45DE-90AE-6E86F164FB21}")]
> +  class LENOVO_FAN_TEST_DATA {
> +    [key, read] string InstanceName;
> +    [read] boolean Active;
> +    [WmiDataId(1), read, Description("Mode.")] uint32 NumOfFans;
> +    [WmiDataId(2), read, Description("Fan ID."), WmiSizeIs("NumOfFans")] uint32 FanId[];
> +    [WmiDataId(3), read, Description("Maximum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMaxSpeed[];
> +    [WmiDataId(4), read, Description("Minumum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMinSpeed[];
> +  };
> diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
> index 29267c373ab3..e6392357395c 100644
> --- a/drivers/platform/x86/lenovo/wmi-capdata.c
> +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
> @@ -13,6 +13,10 @@
>   * attribute has multiple pages, one for each of the thermal modes managed by
>   * the Gamezone interface.
>   *
> + * Fan Test Data includes the max/min fan speed RPM for each fan. This is
> + * reference data for self-test. If the fan is in good condition, it is capable
> + * to spin faster than max RPM or slower than min RPM.
> + *
>   * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
>   *   - Initial implementation (formerly named lenovo-wmi-capdata01)
>   *
> @@ -41,6 +45,7 @@
>  
>  #define LENOVO_CAPABILITY_DATA_00_GUID "362A3AFE-3D96-4665-8530-96DAD5BB300E"
>  #define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154"
> +#define LENOVO_FAN_TEST_DATA_GUID "B642801B-3D21-45DE-90AE-6E86F164FB21"
>  
>  #define ACPI_AC_CLASS "ac_adapter"
>  #define ACPI_AC_NOTIFY_STATUS 0x80
> @@ -48,6 +53,7 @@
>  enum lwmi_cd_type {
>  	LENOVO_CAPABILITY_DATA_00,
>  	LENOVO_CAPABILITY_DATA_01,
> +	LENOVO_FAN_TEST_DATA,
>  };
>  
>  #define LWMI_CD_TABLE_ITEM(_type)		\
> @@ -62,6 +68,7 @@ static const struct lwmi_cd_info {
>  } lwmi_cd_table[] = {
>  	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_00),
>  	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
> +	LWMI_CD_TABLE_ITEM(LENOVO_FAN_TEST_DATA),
>  };
>  
>  struct lwmi_cd_priv {
> @@ -78,6 +85,7 @@ struct cd_list {
>  	union {
>  		DECLARE_FLEX_ARRAY(struct capdata00, cd00);
>  		DECLARE_FLEX_ARRAY(struct capdata01, cd01);
> +		DECLARE_FLEX_ARRAY(struct capdata_fan, cd_fan);
>  	};
>  };
>  
> @@ -117,6 +125,10 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
>  		return;
>  
>  	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
> +		/* Skip optional interfaces. */
> +		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
> +			continue;
> +
>  		component_match_add(master, matchptr, lwmi_cd_match,
>  				    (void *)&lwmi_cd_table[i].type);
>  		if (IS_ERR(*matchptr))
> @@ -194,6 +206,9 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd00_get_data, "LENOVO_WMI_CD");
>  DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
>  EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
>  
> +DEF_LWMI_CDXX_GET_DATA(cd_fan, LENOVO_FAN_TEST_DATA, struct capdata_fan);
> +EXPORT_SYMBOL_NS_GPL(lwmi_cd_fan_get_data, "LENOVO_WMI_CD");
> +
>  /**
>   * lwmi_cd_cache() - Cache all WMI data block information
>   * @priv: lenovo-wmi-capdata driver data.
> @@ -217,6 +232,9 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
>  		p = &priv->list->cd01[0];
>  		size = sizeof(priv->list->cd01[0]);
>  		break;
> +	case LENOVO_FAN_TEST_DATA:
> +		/* Done by lwmi_cd_alloc() => lwmi_cd_fan_list_alloc_cache(). */
> +		return 0;
>  	default:
>  		return -EINVAL;
>  	}
> @@ -239,6 +257,78 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
>  	return 0;
>  }
>  
> +/**
> + * lwmi_cd_fan_list_alloc_cache() - Alloc and cache Fan Test Data list
> + * @priv: lenovo-wmi-capdata driver data.
> + * @listptr: Pointer to returned cd_list pointer.
> + *
> + * Return: count of fans found, or an error.
> + */
> +static int lwmi_cd_fan_list_alloc_cache(struct lwmi_cd_priv *priv, struct cd_list **listptr)
> +{
> +	u32 count, *fan_ids, *fan_min_rpms, *fan_max_rpms;
> +	union acpi_object *ret_obj __free(kfree) = NULL;

Since you're using __free(), please move this to where you assign the 
value. This is to create a pattern with cleanup helpers. The cleanup 
order depends on the order the variables are introduced which in some 
other cases may be significant.

> +	struct block { u32 nr; u32 data[]; } *block;

This is the first time I see this style anywhere in the kernel's context, 
has there been some general discussion about this style somewhere?

At least it seems immediately obvious to me that this style will have a 
negative impact on documentability due to (too) concise use of space.

> +	struct cd_list *list;
> +	size_t size;
> +	int idx;
> +
> +	ret_obj = wmidev_block_query(priv->wdev, 0);
> +	if (!ret_obj)
> +		return -ENODEV;
> +
> +	/*
> +	 * This is usually caused by a dummy ACPI method. Do not return an error
> +	 * as failing to probe this device will result in master driver being
> +	 * unbound - this behavior aligns with lwmi_cd_cache().
> +	 */
> +	if (ret_obj->type != ACPI_TYPE_BUFFER) {
> +		count = 0;
> +		goto alloc;
> +	}
> +
> +	size = ret_obj->buffer.length;
> +	block = (struct block *)ret_obj->buffer.pointer;

void * can be cast implicitly.

> +
> +	count = size >= sizeof(*block) ? block->nr : 0;
> +	if (size < struct_size(block, data, count * 3)) {
> +		dev_warn(&priv->wdev->dev,
> +			 "incomplete fan test data block: %zu < %zu, ignoring\n",
> +			 size, struct_size(block, data, count * 3));
> +		count = 0;
> +	}
> +
> +	if (count == 0)
> +		goto alloc;
> +
> +	if (count > U8_MAX) {
> +		dev_warn(&priv->wdev->dev,
> +			 "too many fans reported: %u > %u, truncating\n",
> +			 count, U8_MAX);
> +		count = U8_MAX;
> +	}
> +
> +	fan_ids = &block->data[0];
> +	fan_max_rpms = &block->data[count];
> +	fan_min_rpms = &block->data[count * 2];

Please rearrange so you can remove the goto:

	1. limit count
	if (count) {
		...
	}


> +
> +alloc:
> +	list = devm_kzalloc(&priv->wdev->dev, struct_size(list, cd_fan, count), GFP_KERNEL);
> +	if (!list)
> +		return -ENOMEM;
> +
> +	for (idx = 0; idx < count; idx++) {
> +		list->cd_fan[idx] = (struct capdata_fan) {
> +			.id = fan_ids[idx],
> +			.min_rpm = fan_min_rpms[idx],
> +			.max_rpm = fan_max_rpms[idx],
> +		};
> +	}
> +
> +	*listptr = list;
> +	return count;
> +}
> +
>  /**
>   * lwmi_cd_alloc() - Allocate a cd_list struct in drvdata
>   * @priv: lenovo-wmi-capdata driver data.
> @@ -264,6 +354,12 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
>  	case LENOVO_CAPABILITY_DATA_01:
>  		list_size = struct_size(list, cd01, count);
>  		break;
> +	case LENOVO_FAN_TEST_DATA:
> +		count = lwmi_cd_fan_list_alloc_cache(priv, &list);
> +		if (count < 0)
> +			return count;
> +
> +		goto got_list;
>  	default:
>  		return -EINVAL;
>  	}
> @@ -272,6 +368,7 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
>  	if (!list)
>  		return -ENOMEM;
>  
> +got_list:
>  	ret = devm_mutex_init(&priv->wdev->dev, &list->list_mutex);
>  	if (ret)
>  		return ret;
> @@ -390,6 +487,8 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
>  
>  		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
>  		goto out;
> +	case LENOVO_FAN_TEST_DATA:
> +		goto out;
>  	default:
>  		return -EINVAL;
>  	}
> @@ -413,6 +512,8 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
>  	case LENOVO_CAPABILITY_DATA_01:
>  		component_del(&wdev->dev, &lwmi_cd_component_ops);
>  		break;
> +	case LENOVO_FAN_TEST_DATA:
> +		break;
>  	default:
>  		WARN_ON(1);
>  	}
> @@ -425,6 +526,7 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
>  static const struct wmi_device_id lwmi_cd_id_table[] = {
>  	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_00) },
>  	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_01) },
> +	{ LWMI_CD_WDEV_ID(LENOVO_FAN_TEST_DATA) },
>  	{}
>  };
>  
> diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
> index a6d006ef458f..38af4c4e4ef4 100644
> --- a/drivers/platform/x86/lenovo/wmi-capdata.h
> +++ b/drivers/platform/x86/lenovo/wmi-capdata.h
> @@ -26,6 +26,12 @@ struct capdata01 {
>  	u32 max_value;
>  };
>  
> +struct capdata_fan {
> +	u32 id;
> +	u32 min_rpm;
> +	u32 max_rpm;
> +};
> +
>  struct lwmi_cd_binder {
>  	struct cd_list *cd00_list;
>  	struct cd_list *cd01_list;
> @@ -34,5 +40,6 @@ struct lwmi_cd_binder {
>  void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
>  int lwmi_cd00_get_data(struct cd_list *list, u32 attribute_id, struct capdata00 *output);
>  int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output);
> +int lwmi_cd_fan_get_data(struct cd_list *list, u32 attribute_id, struct capdata_fan *output);
>  
>  #endif /* !_LENOVO_WMI_CAPDATA_H_ */
> 

-- 
 i.


^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up Fan Test Data
  2025-11-22 18:44 ` [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up " Rong Zhang
  2025-11-23  3:36   ` Armin Wolf
@ 2025-11-24 16:49   ` Ilpo Järvinen
  2025-11-24 20:10     ` Rong Zhang
  1 sibling, 1 reply; 22+ messages in thread
From: Ilpo Järvinen @ 2025-11-24 16:49 UTC (permalink / raw)
  To: Rong Zhang
  Cc: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Guenter Roeck, platform-driver-x86, LKML, linux-hwmon

On Sun, 23 Nov 2025, Rong Zhang wrote:

> A capdata00 attribute (0x04050000) describes the presence of Fan Test
> Data. Query it, and bind Fan Test Data as a component of capdata00
> accordingly. The component master of capdata00 may pass a callback while
> binding to retrieve fan info from Fan Test Data.
> 
> Summarizing this scheme:
> 
> 	lenovo-wmi-other <-> capdata00 <-> capdata_fan
> 	|- master            |- component
> 	                     |- sub-master
> 	                                   |- sub-component
> 
> The callback will be called once both the master and the sub-component
> are bound to the sub-master (component).
> 
> This scheme is essential to solve these issues:
> - The component framework only supports one aggregation per master
> - A binding is only established until all components are found
> - The Fan Test Data interface may be missing on some devices
> - To get rid of queries for the presense of WMI GUIDs
> 
> capdata00 is registered as a component and a sub-master on probe,
> instead of chaining the registerations in one's bind callback. This is
> because calling (un)registration methods of the component framework
> causes deadlock in (un)bind callbacks, i.e., it's impossible to register
> capdata00 as a sub-master/component in its component/sub-master bind
> callback, and vice versa.
> 
> Signed-off-by: Rong Zhang <i@rong.moe>
> ---
> Changes in v6:
> - Fix the error path of component_add(capdata00)
> - Elaborate the design in commit message
> 
> Changes in v5:
> - Fix missing include (thanks kernel test robot)
> 
> Changes in v4:
> - New patch in the series (thanks Armin Wolf's inspiration)
>   - Get rid of wmi_has_guid() (see also [PATCH v4 3/7])
> ---
>  drivers/platform/x86/lenovo/wmi-capdata.c | 265 +++++++++++++++++++++-
>  drivers/platform/x86/lenovo/wmi-capdata.h |  20 ++
>  drivers/platform/x86/lenovo/wmi-other.c   |   5 -
>  3 files changed, 283 insertions(+), 7 deletions(-)
> 
> diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
> index e6392357395c..8760f8c071ca 100644
> --- a/drivers/platform/x86/lenovo/wmi-capdata.c
> +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
> @@ -27,6 +27,7 @@
>  #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
>  
>  #include <linux/acpi.h>
> +#include <linux/bitfield.h>
>  #include <linux/cleanup.h>
>  #include <linux/component.h>
>  #include <linux/container_of.h>
> @@ -50,10 +51,17 @@
>  #define ACPI_AC_CLASS "ac_adapter"
>  #define ACPI_AC_NOTIFY_STATUS 0x80
>  
> +#define LWMI_FEATURE_ID_FAN_TEST 0x05
> +
> +#define LWMI_ATTR_ID_FAN_TEST							\
> +	(FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) |		\
> +	 FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_TEST))
> +
>  enum lwmi_cd_type {
>  	LENOVO_CAPABILITY_DATA_00,
>  	LENOVO_CAPABILITY_DATA_01,
>  	LENOVO_FAN_TEST_DATA,
> +	CD_TYPE_NONE = -1,
>  };
>  
>  #define LWMI_CD_TABLE_ITEM(_type)		\
> @@ -75,6 +83,20 @@ struct lwmi_cd_priv {
>  	struct notifier_block acpi_nb; /* ACPI events */
>  	struct wmi_device *wdev;
>  	struct cd_list *list;
> +
> +	/*
> +	 * A capdata device may be a component master of another capdata device.
> +	 * E.g., lenovo-wmi-other <-> capdata00 <-> capdata_fan
> +	 *       |- master            |- component
> +	 *                            |- sub-master
> +	 *                                          |- sub-component
> +	 */
> +	struct lwmi_cd_sub_master_priv {
> +		struct device *master_dev;
> +		cd_list_cb_t master_cb;
> +		struct cd_list *sub_component_list; /* ERR_PTR(-ENODEV) implies no sub-component. */
> +		bool registered;                    /* Has the sub-master been registered? */
> +	} *sub_master;
>  };
>  
>  struct cd_list {
> @@ -125,7 +147,7 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
>  		return;
>  
>  	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
> -		/* Skip optional interfaces. */
> +		/* Skip sub-components. */

Didn't you just introduce it? If that's the case, please try to avoid such 
back-and-forth changes within the series.

>  		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
>  			continue;
>  
> @@ -137,6 +159,56 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
>  }
>  EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
>  
> +/**
> + * lwmi_cd_call_master_cb() - Call the master callback for the sub-component.
> + * @priv: Pointer to the capability data private data.
> + *
> + * Call the master callback and pass the sub-component list to it if the
> + * dependency chain (master <-> sub-master <-> sub-component) is complete.
> + */
> +static void lwmi_cd_call_master_cb(struct lwmi_cd_priv *priv)
> +{
> +	struct cd_list *sub_component_list = priv->sub_master->sub_component_list;
> +
> +	/*
> +	 * Call the callback only if the dependency chain is ready:
> +	 * - Binding between master and sub-master: fills master_dev and master_cb
> +	 * - Binding between sub-master and sub-component: fills sub_component_list
> +	 *
> +	 * If a binding has been unbound before the other binding is bound, the
> +	 * corresponding members filled by the former are guaranteed to be cleared.
> +	 *
> +	 * This function is only called in bind callbacks, and the component
> +	 * framework guarantees bind/unbind callbacks may never execute
> +	 * simultaneously, which implies that it's impossible to have a race
> +	 * condition.
> +	 *
> +	 * Hence, this check is sufficient to ensure that the callback is called
> +	 * at most once and with the correct state, without relying on a specific
> +	 * sequence of binding establishment.
> +	 */
> +	if (!sub_component_list ||
> +	    !priv->sub_master->master_dev ||
> +	    !priv->sub_master->master_cb)
> +		return;
> +
> +	if (PTR_ERR(sub_component_list) == -ENODEV)

Include?

> +		sub_component_list = NULL;
> +	else if (WARN_ON(IS_ERR(sub_component_list)))

Include?

(Some earlier patch may already need to add these, it probably was 
relevant there already)

> +		return;
> +
> +	priv->sub_master->master_cb(priv->sub_master->master_dev,
> +				    sub_component_list);
> +
> +	/*
> +	 * Prevent "unbind and rebind" sequences from userspace from calling the
> +	 * callback twice.
> +	 */
> +	priv->sub_master->master_cb = NULL;
> +	priv->sub_master->master_dev = NULL;
> +	priv->sub_master->sub_component_list = NULL;
> +}
> +
>  /**
>   * lwmi_cd_component_bind() - Bind component to master device.
>   * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
> @@ -147,6 +219,8 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
>   * list. This is used to call lwmi_cd*_get_data to look up attribute data
>   * from the lenovo-wmi-other driver.
>   *
> + * If cd_dev is a sub-master, try to call the master callback.
> + *
>   * Return: 0
>   */
>  static int lwmi_cd_component_bind(struct device *cd_dev,
> @@ -158,6 +232,11 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
>  	switch (priv->list->type) {
>  	case LENOVO_CAPABILITY_DATA_00:
>  		binder->cd00_list = priv->list;
> +
> +		priv->sub_master->master_dev = om_dev;
> +		priv->sub_master->master_cb = binder->cd_fan_list_cb;
> +		lwmi_cd_call_master_cb(priv);
> +
>  		break;
>  	case LENOVO_CAPABILITY_DATA_01:
>  		binder->cd01_list = priv->list;
> @@ -169,8 +248,167 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
>  	return 0;
>  }
>  
> +/**
> + * lwmi_cd_component_unbind() - Unbind component to master device.
> + * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
> + * @om_dev: Pointer to the lenovo-wmi-other driver parent device.
> + * @data: Unused.
> + *
> + * If cd_dev is a sub-master, clear the collected data from the master device to
> + * prevent the binding establishment between the sub-master and the sub-
> + * component (if it's about to happen) from calling the master callback.
> + */
> +static void lwmi_cd_component_unbind(struct device *cd_dev,
> +				     struct device *om_dev, void *data)
> +{
> +	struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
> +
> +	switch (priv->list->type) {
> +	case LENOVO_CAPABILITY_DATA_00:
> +		priv->sub_master->master_dev = NULL;
> +		priv->sub_master->master_cb = NULL;
> +		return;
> +	default:
> +		return;
> +	}
> +}
> +
>  static const struct component_ops lwmi_cd_component_ops = {
>  	.bind = lwmi_cd_component_bind,
> +	.unbind = lwmi_cd_component_unbind,
> +};
> +
> +/**
> + * lwmi_cd_sub_master_bind() - Bind sub-component of sub-master device
> + * @dev: The sub-master capdata basic device.
> + *
> + * Call component_bind_all to bind the sub-component device to the sub-master
> + * device. On success, collect the pointer to the sub-component list and try
> + * to call the master callback.
> + *
> + * Return: 0 on success, or an error code.
> + */
> +static int lwmi_cd_sub_master_bind(struct device *dev)
> +{
> +	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
> +	struct cd_list *sub_component_list;
> +	int ret;
> +
> +	ret = component_bind_all(dev, &sub_component_list);
> +	if (ret)
> +		return ret;
> +
> +	priv->sub_master->sub_component_list = sub_component_list;
> +	lwmi_cd_call_master_cb(priv);
> +
> +	return 0;
> +}
> +
> +/**
> + * lwmi_cd_sub_master_unbind() - Unbind sub-component of sub-master device
> + * @dev: The sub-master capdata basic device
> + *
> + * Clear the collected pointer to the sub-component list to prevent the binding
> + * establishment between the sub-master and the sub-component (if it's about to
> + * happen) from calling the master callback. Then, call component_unbind_all to
> + * unbind the sub-component device from the sub-master device.
> + */
> +static void lwmi_cd_sub_master_unbind(struct device *dev)
> +{
> +	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
> +
> +	priv->sub_master->sub_component_list = NULL;
> +
> +	component_unbind_all(dev, NULL);
> +}
> +
> +static const struct component_master_ops lwmi_cd_sub_master_ops = {
> +	.bind = lwmi_cd_sub_master_bind,
> +	.unbind = lwmi_cd_sub_master_unbind,
> +};
> +
> +/**
> + * lwmi_cd_sub_master_add() - Register a sub-master with its sub-component
> + * @priv: Pointer to the sub-master capdata device private data.
> + * @sub_component_type: Type of the sub-component.
> + *
> + * Match the sub-component type and register the current capdata device as a
> + * sub-master. If the given sub-component type is CD_TYPE_NONE, mark the sub-
> + * component as non-existent without registering sub-master.
> + *
> + * Return: 0 on success, or an error code.
> + */
> +static int lwmi_cd_sub_master_add(struct lwmi_cd_priv *priv,
> +				  enum lwmi_cd_type sub_component_type)
> +{
> +	struct component_match *master_match = NULL;
> +	int ret;
> +
> +	priv->sub_master = devm_kzalloc(&priv->wdev->dev, sizeof(*priv->sub_master), GFP_KERNEL);
> +	if (!priv->sub_master)
> +		return -ENOMEM;
> +
> +	if (sub_component_type == CD_TYPE_NONE) {
> +		/* The master callback will be called with NULL on bind. */
> +		priv->sub_master->sub_component_list = ERR_PTR(-ENODEV);
> +		priv->sub_master->registered = false;
> +		return 0;
> +	}
> +
> +	/*
> +	 * lwmi_cd_match() needs a pointer to enum lwmi_cd_type, but on-stack
> +	 * data cannot be used here. Steal one from lwmi_cd_table.
> +	 */
> +	component_match_add(&priv->wdev->dev, &master_match, lwmi_cd_match,
> +			    (void *)&lwmi_cd_table[sub_component_type].type);

Is void * cast required?

> +	if (IS_ERR(master_match))
> +		return PTR_ERR(master_match);
> +
> +	ret = component_master_add_with_match(&priv->wdev->dev, &lwmi_cd_sub_master_ops,
> +					      master_match);
> +	if (ret)
> +		return ret;
> +
> +	priv->sub_master->registered = true;
> +	return 0;
> +}
> +
> +/**
> + * lwmi_cd_sub_master_del() - Unregister a sub-master if it's registered
> + * @priv: Pointer to the sub-master capdata device private data.
> + */
> +static void lwmi_cd_sub_master_del(struct lwmi_cd_priv *priv)
> +{
> +	if (priv->sub_master->registered) {

Reverse the logic and return early instead.

> +		component_master_del(&priv->wdev->dev, &lwmi_cd_sub_master_ops);
> +		priv->sub_master->registered = false;
> +	}
> +}
> +
> +/**
> + * lwmi_cd_sub_component_bind() - Bind sub-component to sub-master device.
> + * @sc_dev: Pointer to the sub-component capdata parent device.
> + * @sm_dev: Pointer to the sub-master capdata parent device.
> + * @data: Pointer used to return the capability data list pointer.
> + *
> + * On sub-master's bind, provide a pointer to the local capdata list.
> + * This is used by the sub-master to call the master callback.
> + *
> + * Return: 0
> + */
> +static int lwmi_cd_sub_component_bind(struct device *sc_dev,
> +				      struct device *sm_dev, void *data)
> +{
> +	struct lwmi_cd_priv *priv = dev_get_drvdata(sc_dev);
> +	struct cd_list **listp = data;
> +
> +	*listp = priv->list;
> +
> +	return 0;
> +}
> +
> +static const struct component_ops lwmi_cd_sub_component_ops = {
> +	.bind = lwmi_cd_sub_component_bind,
>  };
>  
>  /**
> @@ -470,9 +708,28 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
>  		goto out;
>  
>  	switch (info->type) {
> -	case LENOVO_CAPABILITY_DATA_00:
> +	case LENOVO_CAPABILITY_DATA_00: {
> +		enum lwmi_cd_type sub_component_type = LENOVO_FAN_TEST_DATA;
> +		struct capdata00 capdata00;
> +
> +		ret = lwmi_cd00_get_data(priv->list, LWMI_ATTR_ID_FAN_TEST, &capdata00);
> +		if (ret || !(capdata00.supported & LWMI_SUPP_VALID)) {
> +			dev_dbg(&wdev->dev, "capdata00 declares no fan test support\n");
> +			sub_component_type = CD_TYPE_NONE;
> +		}
> +
> +		/* Sub-master (capdata00) <-> sub-component (capdata_fan) */
> +		ret = lwmi_cd_sub_master_add(priv, sub_component_type);
> +		if (ret)
> +			goto out;
> +
> +		/* Master (lenovo-wmi-other) <-> sub-master (capdata00) */
>  		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
> +		if (ret)
> +			lwmi_cd_sub_master_del(priv);
> +
>  		goto out;
> +	}
>  	case LENOVO_CAPABILITY_DATA_01:
>  		priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
>  
> @@ -488,6 +745,7 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
>  		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
>  		goto out;
>  	case LENOVO_FAN_TEST_DATA:
> +		ret = component_add(&wdev->dev, &lwmi_cd_sub_component_ops);
>  		goto out;
>  	default:
>  		return -EINVAL;
> @@ -509,10 +767,13 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
>  
>  	switch (priv->list->type) {
>  	case LENOVO_CAPABILITY_DATA_00:
> +		lwmi_cd_sub_master_del(priv);
> +		fallthrough;
>  	case LENOVO_CAPABILITY_DATA_01:
>  		component_del(&wdev->dev, &lwmi_cd_component_ops);
>  		break;
>  	case LENOVO_FAN_TEST_DATA:
> +		component_del(&wdev->dev, &lwmi_cd_sub_component_ops);
>  		break;
>  	default:
>  		WARN_ON(1);
> diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
> index 38af4c4e4ef4..59ca3b3e5760 100644
> --- a/drivers/platform/x86/lenovo/wmi-capdata.h
> +++ b/drivers/platform/x86/lenovo/wmi-capdata.h
> @@ -5,8 +5,20 @@
>  #ifndef _LENOVO_WMI_CAPDATA_H_
>  #define _LENOVO_WMI_CAPDATA_H_
>  
> +#include <linux/bits.h>
>  #include <linux/types.h>
>  
> +#define LWMI_SUPP_VALID		BIT(0)
> +#define LWMI_SUPP_MAY_GET	(LWMI_SUPP_VALID | BIT(1))
> +#define LWMI_SUPP_MAY_SET	(LWMI_SUPP_VALID | BIT(2))
> +
> +#define LWMI_ATTR_DEV_ID_MASK	GENMASK(31, 24)
> +#define LWMI_ATTR_FEAT_ID_MASK	GENMASK(23, 16)
> +#define LWMI_ATTR_MODE_ID_MASK	GENMASK(15, 8)
> +#define LWMI_ATTR_TYPE_ID_MASK	GENMASK(7, 0)
> +
> +#define LWMI_DEVICE_ID_FAN	0x04
> +
>  struct component_match;
>  struct device;
>  struct cd_list;
> @@ -32,9 +44,17 @@ struct capdata_fan {
>  	u32 max_rpm;
>  };
>  
> +typedef void (*cd_list_cb_t)(struct device *master_dev, struct cd_list *cd_list);
> +
>  struct lwmi_cd_binder {
>  	struct cd_list *cd00_list;
>  	struct cd_list *cd01_list;
> +	/*
> +	 * May be called during or after the bind callback.
> +	 * Will be called with NULL if capdata_fan does not exist.
> +	 * The pointer is only valid in the callback; never keep it for later use!
> +	 */
> +	cd_list_cb_t cd_fan_list_cb;
>  };
>  
>  void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
> diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> index f2e1e34d58a9..b3adcc2804fa 100644
> --- a/drivers/platform/x86/lenovo/wmi-other.c
> +++ b/drivers/platform/x86/lenovo/wmi-other.c
> @@ -54,11 +54,6 @@
>  #define LWMI_FEATURE_VALUE_GET 17
>  #define LWMI_FEATURE_VALUE_SET 18
>  
> -#define LWMI_ATTR_DEV_ID_MASK GENMASK(31, 24)
> -#define LWMI_ATTR_FEAT_ID_MASK GENMASK(23, 16)
> -#define LWMI_ATTR_MODE_ID_MASK GENMASK(15, 8)
> -#define LWMI_ATTR_TYPE_ID_MASK GENMASK(7, 0)
> -
>  #define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other"
>  
>  static BLOCKING_NOTIFIER_HEAD(om_chain_head);
> 

-- 
 i.


^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 7/7] platform/x86: lenovo-wmi-other: Add HWMON for fan reporting/tuning
  2025-11-22 18:44 ` [PATCH v6 7/7] platform/x86: lenovo-wmi-other: Add HWMON for fan reporting/tuning Rong Zhang
@ 2025-11-24 16:58   ` Ilpo Järvinen
  2025-11-24 20:12     ` Rong Zhang
  0 siblings, 1 reply; 22+ messages in thread
From: Ilpo Järvinen @ 2025-11-24 16:58 UTC (permalink / raw)
  To: Rong Zhang
  Cc: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Guenter Roeck, platform-driver-x86, LKML, linux-hwmon

On Sun, 23 Nov 2025, Rong Zhang wrote:

> Register an HWMON device for fan reporting/tuning according to
> Capability Data 00 (capdata00) and Fan Test Data (capdata_fan) provided
> by lenovo-wmi-capdata. The corresponding HWMON nodes are:
> 
>  - fanX_enable: enable/disable the fan (tunable)
>  - fanX_input: current RPM
>  - fanX_max: maximum RPM
>  - fanX_min: minimum RPM
>  - fanX_target: target RPM (tunable)
> 
> Information from capdata00 and capdata_fan are used to control the
> visibility and constraints of HWMON attributes. Fan info from capdata00
> is collected on bind, while fan info from capdata_fan is collected in a
> callback. Once all fan info is collected, register the HWMON device.
> 
> Signed-off-by: Rong Zhang <i@rong.moe>
> ---
> Changes in v4:
> - Rework HWMON registration due to the introduction of [PATCH v4 6/7]
>   - Collect fan info from capdata00 and capdata_fan separately
>   - Use a callback to collect fan info from capdata_fan
>   - Trigger HWMON registration only if all fan info is collected
>   - Do not check 0x04050000.supported, implied by the presense of
>     capdata_fan
> - Drop Reviewed-by & Tested-by due to the changes, please review & test
> 
> Changes in v3:
> - Reword documentation (thanks Derek J. Clark)
> 
> Changes in v2:
> - Define 4 fan channels instead of 2 (thanks Derek J. Clark)
> - Squash min/max reporting patch into this one (ditto)
> - Query 0x04050000 for interface availability (ditto)
>   - New parameter "expose_all_fans" to skip this check
> - Enforce min/max RPM constraint on set (ditto)
>   - New parameter "relax_fan_constraint" to disable this behavior
>   - Drop parameter "ignore_fan_cap", superseded by the next one
>   - New parameter "expose_all_fans" to expose fans w/o such data
> - Assume auto mode on probe (ditto)
> - Reword documentation (ditto)
> - Do not register HWMON device if no fan can be exposed
> - fanX_target: Return -EBUSY instead of raw target value when fan stops
> ---
>  .../wmi/devices/lenovo-wmi-other.rst          |  11 +
>  drivers/platform/x86/lenovo/Kconfig           |   1 +
>  drivers/platform/x86/lenovo/wmi-other.c       | 485 +++++++++++++++++-
>  3 files changed, 487 insertions(+), 10 deletions(-)
> 
> diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
> index 821282e07d93..bd1d733ff286 100644
> --- a/Documentation/wmi/devices/lenovo-wmi-other.rst
> +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
> @@ -31,6 +31,8 @@ under the following path:
>  
>    /sys/class/firmware-attributes/lenovo-wmi-other/attributes/<attribute>/
>  
> +Additionally, this driver also exports attributes to HWMON.
> +
>  LENOVO_CAPABILITY_DATA_00
>  -------------------------
>  
> @@ -39,6 +41,11 @@ WMI GUID ``362A3AFE-3D96-4665-8530-96DAD5BB300E``
>  The LENOVO_CAPABILITY_DATA_00 interface provides various information that
>  does not rely on the gamezone thermal mode.
>  
> +The following HWMON attributes are implemented:
> + - fanX_enable: enable/disable the fan (tunable)
> + - fanX_input: current RPM
> + - fanX_target: target RPM (tunable)
> +
>  LENOVO_CAPABILITY_DATA_01
>  -------------------------
>  
> @@ -70,6 +77,10 @@ WMI GUID ``B642801B-3D21-45DE-90AE-6E86F164FB21``
>  The LENOVO_FAN_TEST_DATA interface provides reference data for self-test of
>  cooling fans.
>  
> +The following HWMON attributes are implemented:
> + - fanX_max: maximum RPM
> + - fanX_min: minimum RPM
> +
>  WMI interface description
>  =========================
>  
> diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
> index fb96a0f908f0..be9af0451146 100644
> --- a/drivers/platform/x86/lenovo/Kconfig
> +++ b/drivers/platform/x86/lenovo/Kconfig
> @@ -263,6 +263,7 @@ config LENOVO_WMI_GAMEZONE
>  config LENOVO_WMI_TUNING
>  	tristate "Lenovo Other Mode WMI Driver"
>  	depends on ACPI_WMI
> +	select HWMON
>  	select FW_ATTR_CLASS
>  	select LENOVO_WMI_DATA
>  	select LENOVO_WMI_EVENTS
> diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> index b3adcc2804fa..ce1ca13db4b5 100644
> --- a/drivers/platform/x86/lenovo/wmi-other.c
> +++ b/drivers/platform/x86/lenovo/wmi-other.c
> @@ -14,7 +14,16 @@
>   * These attributes typically don't fit anywhere else in the sysfs and are set
>   * in Windows using one of Lenovo's multiple user applications.
>   *
> + * Additionally, this driver also exports tunable fan speed RPM to HWMON.
> + * Min/max RPM are also provided for reference.
> + *
>   * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
> + *   - fw_attributes
> + *   - binding to Capability Data 01
> + *
> + * Copyright (C) 2025 Rong Zhang <i@rong.moe>
> + *   - HWMON
> + *   - binding to Capability Data 00 and Fan
>   */
>  
>  #include <linux/acpi.h>
> @@ -25,6 +34,7 @@
>  #include <linux/device.h>
>  #include <linux/export.h>
>  #include <linux/gfp_types.h>
> +#include <linux/hwmon.h>
>  #include <linux/idr.h>
>  #include <linux/kdev_t.h>
>  #include <linux/kobject.h>
> @@ -49,12 +59,26 @@
>  #define LWMI_FEATURE_ID_CPU_SPL 0x02
>  #define LWMI_FEATURE_ID_CPU_FPPT 0x03
>  
> +#define LWMI_FEATURE_ID_FAN_RPM 0x03
> +
>  #define LWMI_TYPE_ID_NONE 0x00
>  
>  #define LWMI_FEATURE_VALUE_GET 17
>  #define LWMI_FEATURE_VALUE_SET 18
>  
> +#define LWMI_FAN_ID_BASE 1
> +#define LWMI_FAN_NR 4
> +#define LWMI_FAN_ID(x) ((x) + LWMI_FAN_ID_BASE)
> +
> +#define LWMI_ATTR_ID_FAN_RPM(x)						\
> +	(FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) |	\
> +	 FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_RPM) |	\
> +	 FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, LWMI_FAN_ID(x)))
> +
> +#define LWMI_FAN_STOP_RPM 1
> +
>  #define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other"
> +#define LWMI_OM_HWMON_NAME "lenovo_wmi_other"
>  
>  static BLOCKING_NOTIFIER_HEAD(om_chain_head);
>  static DEFINE_IDA(lwmi_om_ida);
> @@ -71,15 +95,439 @@ struct lwmi_om_priv {
>  	struct component_master_ops *ops;
>  
>  	/* only valid after capdata bind */
> +	struct cd_list *cd00_list;
>  	struct cd_list *cd01_list;
>  
> +	struct device *hwmon_dev;
>  	struct device *fw_attr_dev;
>  	struct kset *fw_attr_kset;
>  	struct notifier_block nb;
>  	struct wmi_device *wdev;
>  	int ida_id;
> +
> +	struct fan_info {
> +		u32 supported;
> +		u32 last_target;
> +		long min_rpm;
> +		long max_rpm;
> +	} fan_info[LWMI_FAN_NR];

I personally don't like this style at all because it makes finding the 
type of the variable harder with grep.

Add an empty line.

> +	struct {
> +		bool capdata00_collected : 1;
> +		bool capdata_fan_collected : 1;
> +	} fan_flags;
> +};
> +
> +/*
> + * Visibility of fan channels:
> + *
> + * +-------------------+---------+------------------+-----------------------+------------+
> + * |                   | default | +expose_all_fans | +relax_fan_constraint | +both      |
> + * +-------------------+---------+------------------+-----------------------+------------+
> + * | canonical         | RW      | RW               | RW+relaxed            | RW+relaxed |
> + * +-------------------+---------+------------------+-----------------------+------------+
> + * | -capdata_fan[idx] | N       | RO               | N                     | RW+relaxed |
> + * +-------------------+---------+------------------+-----------------------+------------+
> + *
> + * Note:
> + * 1. LWMI_ATTR_ID_FAN_RPM[idx].supported is always checked before exposing a channel.
> + * 2. -capdata_fan implies -capdata_fan[idx].
> + */
> +static bool expose_all_fans;
> +module_param(expose_all_fans, bool, 0444);
> +MODULE_PARM_DESC(expose_all_fans,
> +	"This option skips some capability checks and solely relies on per-channel ones "
> +	"to expose fan attributes. Use with caution.");
> +
> +static bool relax_fan_constraint;
> +module_param(relax_fan_constraint, bool, 0444);
> +MODULE_PARM_DESC(relax_fan_constraint,
> +	"Do not enforce fan RPM constraint (min/max RPM) "
> +	"and enables fan tuning when such data is missing. "
> +	"Enabling this may results in HWMON attributes being out-of-sync. Use with caution.");
> +
> +/* ======== HWMON (component: lenovo-wmi-capdata 00 & fan) ======== */
> +
> +/**
> + * lwmi_om_fan_get_set() - Get or set fan RPM value of specified fan
> + * @priv: Driver private data structure
> + * @channel: Fan channel index (0-based)
> + * @val: Pointer to value (input for set, output for get)
> + * @set: True to set value, false to get value
> + *
> + * Communicates with WMI interface to either retrieve current fan RPM
> + * or set target fan RPM.
> + *
> + * Return: 0 on success, or an error code.
> + */
> +static int lwmi_om_fan_get_set(struct lwmi_om_priv *priv, int channel, u32 *val, bool set)
> +{
> +	struct wmi_method_args_32 args;
> +	u32 method_id, retval;
> +	int err;
> +
> +	method_id = set ? LWMI_FEATURE_VALUE_SET : LWMI_FEATURE_VALUE_GET;
> +	args.arg0 = LWMI_ATTR_ID_FAN_RPM(channel);
> +	args.arg1 = set ? *val : 0;
> +
> +	err = lwmi_dev_evaluate_int(priv->wdev, 0x0, method_id,
> +				    (unsigned char *)&args, sizeof(args), &retval);
> +	if (err)
> +		return err;
> +
> +	if (!set)
> +		*val = retval;
> +	else if (retval != 1)
> +		return -EIO;
> +
> +	return 0;
> +}
> +
> +/**
> + * lwmi_om_hwmon_is_visible() - Determine visibility of HWMON attributes
> + * @drvdata: Driver private data
> + * @type: Sensor type
> + * @attr: Attribute identifier
> + * @channel: Channel index
> + *
> + * Determines whether an HWMON attribute should be visible in sysfs
> + * based on hardware capabilities and current configuration.
> + *
> + * Return: permission mode, or 0 if invisible.
> + */
> +static umode_t lwmi_om_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type,
> +					u32 attr, int channel)
> +{
> +	struct lwmi_om_priv *priv = (struct lwmi_om_priv *)drvdata;
> +	bool visible = false;
> +
> +	if (type == hwmon_fan) {
> +		if (!(priv->fan_info[channel].supported & LWMI_SUPP_VALID))
> +			return 0;
> +
> +		switch (attr) {
> +		case hwmon_fan_enable:
> +		case hwmon_fan_target:
> +			if (!(priv->fan_info[channel].supported & LWMI_SUPP_MAY_SET))
> +				return 0;
> +
> +			if (relax_fan_constraint ||
> +			    (priv->fan_info[channel].min_rpm >= 0 &&
> +			     priv->fan_info[channel].max_rpm >= 0))
> +				return 0644;
> +
> +			/*
> +			 * Reaching here implies expose_all_fans is set.
> +			 * See lwmi_om_hwmon_add().
> +			 */
> +			dev_warn_once(&priv->wdev->dev,
> +				      "fan tuning disabled due to missing RPM constraint\n");
> +			return 0;
> +		case hwmon_fan_input:
> +			visible = priv->fan_info[channel].supported & LWMI_SUPP_MAY_GET;
> +			break;
> +		case hwmon_fan_min:
> +			visible = priv->fan_info[channel].min_rpm >= 0;
> +			break;
> +		case hwmon_fan_max:
> +			visible = priv->fan_info[channel].max_rpm >= 0;
> +			break;
> +		}
> +	}
> +
> +	return visible ? 0444 : 0;
> +}
> +
> +/**
> + * lwmi_om_hwmon_read() - Read HWMON sensor data
> + * @dev: Device pointer
> + * @type: Sensor type
> + * @attr: Attribute identifier
> + * @channel: Channel index
> + * @val: Pointer to store value
> + *
> + * Reads current sensor values from hardware through WMI interface.
> + *
> + * Return: 0 on success, or an error code.
> + */
> +static int lwmi_om_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
> +			      u32 attr, int channel, long *val)
> +{
> +	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> +	u32 retval = 0;
> +	int err;
> +
> +	if (type == hwmon_fan) {
> +		switch (attr) {
> +		case hwmon_fan_input:
> +			err = lwmi_om_fan_get_set(priv, channel, &retval, false);
> +			if (err)
> +				return err;
> +
> +			*val = retval;
> +			return 0;
> +		case hwmon_fan_enable:
> +			*val = priv->fan_info[channel].last_target != LWMI_FAN_STOP_RPM;
> +			return 0;
> +		case hwmon_fan_target:
> +			if (priv->fan_info[channel].last_target == LWMI_FAN_STOP_RPM)
> +				return -EBUSY;
> +
> +			*val = priv->fan_info[channel].last_target;
> +			return 0;
> +		case hwmon_fan_min:
> +			*val = priv->fan_info[channel].min_rpm;
> +			return 0;
> +		case hwmon_fan_max:
> +			*val = priv->fan_info[channel].max_rpm;
> +			return 0;
> +		}
> +	}
> +
> +	return -EOPNOTSUPP;
> +}
> +
> +/**
> + * lwmi_om_hwmon_write() - Write HWMON sensor data
> + * @dev: Device pointer
> + * @type: Sensor type
> + * @attr: Attribute identifier
> + * @channel: Channel index
> + * @val: Value to write
> + *
> + * Writes configuration values to hardware through WMI interface.
> + *
> + * Return: 0 on success, or an error code.
> + */
> +static int lwmi_om_hwmon_write(struct device *dev, enum hwmon_sensor_types type,
> +			       u32 attr, int channel, long val)
> +{
> +	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> +	u32 raw, min_rpm, max_rpm;
> +	int err;
> +
> +	if (type == hwmon_fan) {
> +		switch (attr) {
> +		case hwmon_fan_enable:
> +			if (val == 0)
> +				raw = LWMI_FAN_STOP_RPM;
> +			else if (val == 1)
> +				raw = 0; /* auto */
> +			else
> +				return -EINVAL;
> +
> +			goto fan_set;
> +		case hwmon_fan_target:
> +			if (val == 0) {
> +				raw = 0;
> +				goto fan_set;
> +			}
> +
> +			min_rpm = relax_fan_constraint
> +					? LWMI_FAN_STOP_RPM + 1
> +					: priv->fan_info[channel].min_rpm;
> +			max_rpm = relax_fan_constraint
> +					? U16_MAX
> +					: priv->fan_info[channel].max_rpm;
> +
> +			if (val < min_rpm || val > max_rpm)
> +				return -EDOM;
> +
> +			raw = val;
> +fan_set:
> +			err = lwmi_om_fan_get_set(priv, channel, &raw, true);
> +			if (err)
> +				return err;
> +
> +			priv->fan_info[channel].last_target = raw;
> +			return 0;
> +		}
> +	}
> +
> +	return -EOPNOTSUPP;
> +}
> +
> +static const struct hwmon_channel_info * const lwmi_om_hwmon_info[] = {
> +	/* Must match LWMI_FAN_NR. */
> +	HWMON_CHANNEL_INFO(fan,
> +			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
> +			   HWMON_F_MIN | HWMON_F_MAX,
> +			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
> +			   HWMON_F_MIN | HWMON_F_MAX,
> +			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
> +			   HWMON_F_MIN | HWMON_F_MAX,
> +			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
> +			   HWMON_F_MIN | HWMON_F_MAX),
> +	NULL
>  };
>  
> +static const struct hwmon_ops lwmi_om_hwmon_ops = {
> +	.is_visible = lwmi_om_hwmon_is_visible,
> +	.read = lwmi_om_hwmon_read,
> +	.write = lwmi_om_hwmon_write,
> +};
> +
> +static const struct hwmon_chip_info lwmi_om_hwmon_chip_info = {
> +	.ops = &lwmi_om_hwmon_ops,
> +	.info = lwmi_om_hwmon_info,
> +};
> +
> +/**
> + * lwmi_om_hwmon_add() - Register HWMON device if all info is collected
> + * @priv: Driver private data
> + */
> +static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv)
> +{
> +	int i, valid;
> +
> +	if (WARN_ON(priv->hwmon_dev))
> +		return;
> +
> +	if (!priv->fan_flags.capdata00_collected || !priv->fan_flags.capdata_fan_collected) {
> +		dev_dbg(&priv->wdev->dev, "HWMON registration pending (00: %d, fan: %d)\n",
> +			priv->fan_flags.capdata00_collected,
> +			priv->fan_flags.capdata_fan_collected);
> +		return;
> +	}
> +
> +	if (expose_all_fans)
> +		dev_warn(&priv->wdev->dev, "all fans exposed. Use with caution\n");
> +
> +	if (relax_fan_constraint)
> +		dev_warn(&priv->wdev->dev, "fan RPM constraint relaxed. Use with caution\n");
> +
> +	valid = 0;
> +	for (i = 0; i < LWMI_FAN_NR; i++) {
> +		if (!(priv->fan_info[i].supported & LWMI_SUPP_VALID))
> +			continue;
> +
> +		valid++;
> +
> +		if (!expose_all_fans &&
> +		    (priv->fan_info[i].min_rpm < 0 || priv->fan_info[i].max_rpm < 0)) {
> +			dev_dbg(&priv->wdev->dev, "missing RPM constraint for fan%d, hiding\n",
> +				LWMI_FAN_ID(i));
> +			priv->fan_info[i].supported = 0;
> +			valid--;
> +		}
> +	}
> +
> +	if (valid == 0) {
> +		dev_warn(&priv->wdev->dev,
> +			 "fan reporting/tuning is unsupported on this device\n");
> +		return;
> +	}
> +
> +	priv->hwmon_dev = hwmon_device_register_with_info(&priv->wdev->dev,
> +							  LWMI_OM_HWMON_NAME, priv,
> +							  &lwmi_om_hwmon_chip_info,
> +							  NULL);
> +	if (IS_ERR(priv->hwmon_dev)) {
> +		dev_warn(&priv->wdev->dev, "failed to register HWMON device: %ld\n",
> +			 PTR_ERR(priv->hwmon_dev));
> +		priv->hwmon_dev = NULL;
> +		return;
> +	}
> +
> +	dev_dbg(&priv->wdev->dev, "registered HWMON device\n");
> +}
> +
> +/**
> + * lwmi_om_hwmon_remove() - Unregister HWMON device
> + * @priv: Driver private data
> + *
> + * Unregisters the HWMON device if applicable.
> + */
> +static void lwmi_om_hwmon_remove(struct lwmi_om_priv *priv)
> +{
> +	if (!priv->hwmon_dev)
> +		return;
> +
> +	hwmon_device_unregister(priv->hwmon_dev);
> +	priv->hwmon_dev = NULL;
> +}
> +
> +/**
> + * lwmi_om_fan_info_init() - Initialzie fan info
> + * @priv: Driver private data
> + *
> + * lwmi_om_fan_info_collect_cd00() and lwmi_om_fan_info_collect_cd_fan() may be
> + * called in an arbitrary order. Hence, initializion must be done before.
> + */
> +static void lwmi_om_fan_info_init(struct lwmi_om_priv *priv)
> +{
> +	int i;
> +
> +	for (i = 0; i < LWMI_FAN_NR; i++) {
> +		priv->fan_info[i] = (struct fan_info) {
> +			.supported = 0,
> +			/*
> +			 * Assume 0 on probe as the EC resets all fans to auto mode on (re)boot.
> +			 *
> +			 * Note that S0ix (s2idle) preserves the RPM target, so we don't need
> +			 * suspend/resume callbacks. This behavior has not been tested on S3-
> +			 * capable devices, but I doubt if such devices even have this interface.
> +			 */
> +			.last_target = 0,
> +			.min_rpm = -ENODATA,
> +			.max_rpm = -ENODATA,
> +		};
> +	}
> +
> +	priv->fan_flags.capdata00_collected = false;
> +	priv->fan_flags.capdata_fan_collected = false;
> +}
> +
> +/**
> + * lwmi_om_fan_info_collect_cd00() - Collect fan info from capdata 00
> + * @priv: Driver private data
> + */
> +static void lwmi_om_fan_info_collect_cd00(struct lwmi_om_priv *priv)
> +{
> +	struct capdata00 capdata00;
> +	int i, err;
> +
> +	dev_dbg(&priv->wdev->dev, "Collecting fan info from capdata00\n");
> +
> +	for (i = 0; i < LWMI_FAN_NR; i++) {
> +		err = lwmi_cd00_get_data(priv->cd00_list, LWMI_ATTR_ID_FAN_RPM(i), &capdata00);
> +		priv->fan_info[i].supported = err ? 0 : capdata00.supported;
> +	}
> +
> +	priv->fan_flags.capdata00_collected = true;
> +	lwmi_om_hwmon_add(priv);
> +}
> +
> +/**
> + * lwmi_om_fan_info_collect_cd_fan() - Collect fan info from capdata fan
> + * @dev: Pointer to the lenovo-wmi-other device
> + * @cd_fan_list: Pointer to the capdata fan list
> + */
> +static void lwmi_om_fan_info_collect_cd_fan(struct device *dev, struct cd_list *cd_fan_list)
> +{
> +	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> +	struct capdata_fan capdata_fan;
> +	int i, err;
> +
> +	dev_dbg(dev, "Collecting fan info from capdata_fan\n");
> +	if (!cd_fan_list)
> +		goto out;
> +
> +	for (i = 0; i < LWMI_FAN_NR; i++) {
> +		err = lwmi_cd_fan_get_data(cd_fan_list, LWMI_FAN_ID(i), &capdata_fan);
> +		if (err)
> +			continue;
> +
> +		priv->fan_info[i].min_rpm = capdata_fan.min_rpm;
> +		priv->fan_info[i].max_rpm = capdata_fan.max_rpm;
> +	}

Add an empty line.

> +out:
> +	priv->fan_flags.capdata_fan_collected = true;
> +	lwmi_om_hwmon_add(priv);
> +}
> +
> +/* ======== fw_attributes (component: lenovo-wmi-capdata 01) ======== */
> +
>  struct tunable_attr_01 {
>  	struct capdata01 *capdata;
>  	struct device *dev;
> @@ -559,32 +1007,45 @@ static void lwmi_om_fw_attr_remove(struct lwmi_om_priv *priv)
>  	device_unregister(priv->fw_attr_dev);
>  }
>  
> +/* ======== Self (master: lenovo-wmi-other) ======== */
> +
>  /**
>   * lwmi_om_master_bind() - Bind all components of the other mode driver
>   * @dev: The lenovo-wmi-other driver basic device.
>   *
> - * Call component_bind_all to bind the lenovo-wmi-capdata01 driver to the
> - * lenovo-wmi-other master driver. On success, assign the capability data 01
> - * list pointer to the driver data struct for later access. This pointer
> - * is only valid while the capdata01 interface exists. Finally, register all
> - * firmware attribute groups.
> + * Call component_bind_all to bind the lenovo-wmi-capdata devices to the
> + * lenovo-wmi-other master driver, with a callback to collect fan info from
> + * capdata_fan. On success, assign the capability data list pointers to the
> + * driver data struct for later access. These pointers are only valid while the
> + * capdata interfaces exist. Finally, collect fan info from capdata00 and
> + * register all firmware attribute groups. Note that the HWMON device is
> + * registered only if all fan info is collected. Hence, it is not registered
> + * here. See lwmi_om_fan_info_collect_cd00() and
> + * lwmi_om_fan_info_collect_cd_fan().
>   *
>   * Return: 0 on success, or an error code.
>   */
>  static int lwmi_om_master_bind(struct device *dev)
>  {
>  	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> -	struct lwmi_cd_binder binder = { 0 };
> +	struct lwmi_cd_binder binder = {
> +		.cd_fan_list_cb = lwmi_om_fan_info_collect_cd_fan,
> +	};
>  	int ret;
>  
> +	lwmi_om_fan_info_init(priv);
> +
>  	ret = component_bind_all(dev, &binder);
>  	if (ret)
>  		return ret;
>  
> +	priv->cd00_list = binder.cd00_list;
>  	priv->cd01_list = binder.cd01_list;
> -	if (!priv->cd01_list)
> +	if (!priv->cd00_list || !priv->cd01_list)
>  		return -ENODEV;
>  
> +	lwmi_om_fan_info_collect_cd00(priv);
> +
>  	return lwmi_om_fw_attr_add(priv);
>  }
>  
> @@ -592,15 +1053,18 @@ static int lwmi_om_master_bind(struct device *dev)
>   * lwmi_om_master_unbind() - Unbind all components of the other mode driver
>   * @dev: The lenovo-wmi-other driver basic device
>   *
> - * Unregister all capability data attribute groups. Then call
> - * component_unbind_all to unbind the lenovo-wmi-capdata01 driver from the
> - * lenovo-wmi-other master driver. Finally, free the IDA for this device.
> + * Unregister all firmware attribute groups and the HWMON device. Then call
> + * component_unbind_all to unbind lenovo-wmi-capdata devices from the
> + * lenovo-wmi-other master driver.
>   */
>  static void lwmi_om_master_unbind(struct device *dev)
>  {
>  	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
>  
>  	lwmi_om_fw_attr_remove(priv);
> +
> +	lwmi_om_hwmon_remove(priv);
> +
>  	component_unbind_all(dev, NULL);
>  }
>  
> @@ -665,5 +1129,6 @@ MODULE_IMPORT_NS("LENOVO_WMI_CD");
>  MODULE_IMPORT_NS("LENOVO_WMI_HELPERS");
>  MODULE_DEVICE_TABLE(wmi, lwmi_other_id_table);
>  MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
> +MODULE_AUTHOR("Rong Zhang <i@rong.moe>");
>  MODULE_DESCRIPTION("Lenovo Other Mode WMI Driver");
>  MODULE_LICENSE("GPL");
> 

-- 
 i.


^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 3/7] platform/x86: lenovo-wmi-{capdata,other}: Support multiple Capability Data
  2025-11-24 16:30   ` Ilpo Järvinen
@ 2025-11-24 20:03     ` Rong Zhang
  0 siblings, 0 replies; 22+ messages in thread
From: Rong Zhang @ 2025-11-24 20:03 UTC (permalink / raw)
  To: Ilpo Järvinen
  Cc: Rong Zhang, Mark Pearson, Derek J. Clark, Armin Wolf,
	Hans de Goede, Guenter Roeck, platform-driver-x86, LKML,
	linux-hwmon

Hi Ilpo,

Thanks for your review.

On Mon, 2025-11-24 at 18:30 +0200, Ilpo Järvinen wrote:
> On Sun, 23 Nov 2025, Rong Zhang wrote:
> 
> > The current implementation are heavily bound to capdata01. Rewrite it so
> > that it is suitable to utilize other Capability Data as well.
> > 
> > No functional change intended.
> > 
> > Signed-off-by: Rong Zhang <i@rong.moe>
> > Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
> > Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
> > ---
> > Changes in v6:
> > - Fix the error path of lwmi_cd_match_add_all()
> >   - IS_ERR(matchptr) => IS_ERR(*matchptr)
> > 
> > Changes in v5:
> > - Do not cast pointer to non-pointer or vice versa (thanks kernel test
> >   robot)
> > 
> > Changes in v4:
> > - Get rid of wmi_has_guid() (thanks Armin Wolf)
> >   - More changes in [PATCH v4 6/7]
> > - Prepare for [PATCH v4 6/7]
> >   - Move lwmi_cd_match*() forward
> >   - Use switch-case in lwmi_cd_remove()
> > 
> > Changes in v2:
> > - Fix function parameter 'type' not described in 'lwmi_cd_match' (thanks
> >   kernel test bot)
> > ---
> >  drivers/platform/x86/lenovo/wmi-capdata.c | 225 +++++++++++++++++-----
> >  drivers/platform/x86/lenovo/wmi-capdata.h |   7 +-
> >  drivers/platform/x86/lenovo/wmi-other.c   |  16 +-
> >  3 files changed, 189 insertions(+), 59 deletions(-)
> > 
> > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
> > index c5e74b2bfeb3..e0710354278a 100644
> > --- a/drivers/platform/x86/lenovo/wmi-capdata.c
> > +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
> > @@ -12,8 +12,13 @@
> >   *
> >   * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
> >   *   - Initial implementation (formerly named lenovo-wmi-capdata01)
> > + *
> > + * Copyright (C) 2025 Rong Zhang <i@rong.moe>
> > + *   - Unified implementation
> >   */
> >  
> > +#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> > +
> >  #include <linux/acpi.h>
> >  #include <linux/cleanup.h>
> >  #include <linux/component.h>
> > @@ -36,6 +41,23 @@
> >  #define ACPI_AC_CLASS "ac_adapter"
> >  #define ACPI_AC_NOTIFY_STATUS 0x80
> >  
> > +enum lwmi_cd_type {
> > +	LENOVO_CAPABILITY_DATA_01,
> > +};
> > +
> > +#define LWMI_CD_TABLE_ITEM(_type)		\
> > +	[_type] = {				\
> > +		.name = #_type,			\
> > +		.type = _type,			\
> > +	}
> > +
> > +static const struct lwmi_cd_info {
> > +	const char *name;
> > +	enum lwmi_cd_type type;
> > +} lwmi_cd_table[] = {
> > +	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
> > +};
> > +
> >  struct lwmi_cd_priv {
> >  	struct notifier_block acpi_nb; /* ACPI events */
> >  	struct wmi_device *wdev;
> > @@ -44,15 +66,63 @@ struct lwmi_cd_priv {
> >  
> >  struct cd_list {
> >  	struct mutex list_mutex; /* list R/W mutex */
> > +	enum lwmi_cd_type type;
> >  	u8 count;
> > -	struct capdata01 data[];
> > +
> > +	union {
> > +		DECLARE_FLEX_ARRAY(struct capdata01, cd01);
> 
> Add #include

It's embarrassing that I always forget #include... Sorry for that.

Will fix this and the following missing #include. Thanks.

> > +	};
> >  };
> >  
> > +static struct wmi_driver lwmi_cd_driver;
> > +
> > +/**
> > + * lwmi_cd_match() - Match rule for the master driver.
> > + * @dev: Pointer to the capability data parent device.
> > + * @type: Pointer to capability data type (enum lwmi_cd_type *) to match.
> > + *
> > + * Return: int.
> > + */
> > +static int lwmi_cd_match(struct device *dev, void *type)
> > +{
> > +	struct lwmi_cd_priv *priv;
> > +
> > +	if (dev->driver != &lwmi_cd_driver.driver)
> > +		return false;
> > +
> > +	priv = dev_get_drvdata(dev);
> > +	return priv->list->type == *(enum lwmi_cd_type *)type;
> > +}
> > +
> > +/**
> > + * lwmi_cd_match_add_all() - Add all match rule for the master driver.
> > + * @master: Pointer to the master device.
> > + * @matchptr: Pointer to the returned component_match pointer.
> > + *
> > + * Adds all component matches to the list stored in @matchptr for the @master
> > + * device. @matchptr must be initialized to NULL.
> > + */
> > +void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr)
> > +{
> > +	int i;
> > +
> > +	if (WARN_ON(*matchptr))
> 
> Add #include
> 
> > +		return;
> > +
> > +	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
> > +		component_match_add(master, matchptr, lwmi_cd_match,
> > +				    (void *)&lwmi_cd_table[i].type);
> 
> As the cast to void pointer necessary here? Usually pointer can be cast to 
> void * implicitly (but I'm not sure about this case type seems enum).

lwmi_cd_table is a const array, its const qualifier infects the
pointer. The cast here is to drop the const qualifier from `const enum
lwmi_cd_type *' as component_match_add() expects plain `void *'.

   warning: passing argument 4 of ‘component_match_add’ discards ‘const’
   qualifier from pointer target type [-Wdiscarded-qualifiers]
   
   note: expected ‘void *’ but argument is of type ‘const enum
   lwmi_cd_type *’

> > +		if (IS_ERR(*matchptr))
> 
> Add #include
> 
> > +			return;
> > +	}
> > +}
> > +EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
> > +
> >  /**
> >   * lwmi_cd_component_bind() - Bind component to master device.
> >   * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
> >   * @om_dev: Pointer to the lenovo-wmi-other driver parent device.
> > - * @data: cd_list object pointer used to return the capability data.
> > + * @data: lwmi_cd_binder object pointer used to return the capability data.
> >   *
> >   * On lenovo-wmi-other's master bind, provide a pointer to the local capdata
> >   * list. This is used to call lwmi_cd*_get_data to look up attribute data
> > @@ -64,9 +134,15 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
> >  				  struct device *om_dev, void *data)
> >  {
> >  	struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
> > -	struct cd_list **cd_list = data;
> > +	struct lwmi_cd_binder *binder = data;
> >  
> > -	*cd_list = priv->list;
> > +	switch (priv->list->type) {
> > +	case LENOVO_CAPABILITY_DATA_01:
> > +		binder->cd01_list = priv->list;
> > +		break;
> > +	default:
> > +		return -EINVAL;
> > +	}
> >  
> >  	return 0;
> >  }
> > @@ -76,30 +152,33 @@ static const struct component_ops lwmi_cd_component_ops = {
> >  };
> >  
> >  /**
> > - * lwmi_cd01_get_data - Get the data of the specified attribute
> > + * lwmi_cd*_get_data - Get the data of the specified attribute
> >   * @list: The lenovo-wmi-capdata pointer to its cd_list struct.
> >   * @attribute_id: The capdata attribute ID to be found.
> > - * @output: Pointer to a capdata01 struct to return the data.
> > + * @output: Pointer to a capdata* struct to return the data.
> >   *
> > - * Retrieves the capability data 01 struct pointer for the given
> > - * attribute for its specified thermal mode.
> > + * Retrieves the capability data struct pointer for the given
> > + * attribute.
> >   *
> >   * Return: 0 on success, or -EINVAL.
> >   */
> > -int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output)
> > -{
> > -	u8 idx;
> > -
> > -	guard(mutex)(&list->list_mutex);
> > -	for (idx = 0; idx < list->count; idx++) {
> > -		if (list->data[idx].id != attribute_id)
> > -			continue;
> > -		memcpy(output, &list->data[idx], sizeof(list->data[idx]));
> > -		return 0;
> > +#define DEF_LWMI_CDXX_GET_DATA(_cdxx, _cd_type, _output_t)					\
> > +	int lwmi_##_cdxx##_get_data(struct cd_list *list, u32 attribute_id, _output_t *output)	\
> > +	{											\
> > +		u8 idx;										\
> 
> Please follow the normal conversions, so add an empty line here (with the
> backslash contination, obviusly).

Will fix this and the following formatting issues (empty line, comma,
{0} => {}). Thanks.

> > +		if (WARN_ON(list->type != _cd_type))						\
> > +			return -EINVAL;								\
> 
> Empty line here too to help readability?
> 
> > +		guard(mutex)(&list->list_mutex);						\
> > +		for (idx = 0; idx < list->count; idx++) {					\
> > +			if (list->_cdxx[idx].id != attribute_id)				\
> > +				continue;							\
> > +			memcpy(output, &list->_cdxx[idx], sizeof(list->_cdxx[idx]));		\
> > +			return 0;								\
> > +		}										\
> > +		return -EINVAL;									\
> >  	}
> >  
> > -	return -EINVAL;
> > -}
> > +DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
> >  EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
> >  
> >  /**
> > @@ -112,10 +191,21 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
> >   */
> >  static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
> >  {
> > +	size_t size;
> >  	int idx;
> > +	void *p;
> > +
> > +	switch (priv->list->type) {
> > +	case LENOVO_CAPABILITY_DATA_01:
> > +		p = &priv->list->cd01[0];
> > +		size = sizeof(priv->list->cd01[0]);
> > +		break;
> > +	default:
> > +		return -EINVAL;
> > +	}
> >  
> >  	guard(mutex)(&priv->list->list_mutex);
> > -	for (idx = 0; idx < priv->list->count; idx++) {
> > +	for (idx = 0; idx < priv->list->count; idx++, p += size) {
> >  		union acpi_object *ret_obj __free(kfree) = NULL;
> >  
> >  		ret_obj = wmidev_block_query(priv->wdev, idx);
> > @@ -123,11 +213,10 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
> >  			return -ENODEV;
> >  
> >  		if (ret_obj->type != ACPI_TYPE_BUFFER ||
> > -		    ret_obj->buffer.length < sizeof(priv->list->data[idx]))
> > +		    ret_obj->buffer.length < size)
> >  			continue;
> >  
> > -		memcpy(&priv->list->data[idx], ret_obj->buffer.pointer,
> > -		       ret_obj->buffer.length);
> > +		memcpy(p, ret_obj->buffer.pointer, size);
> >  	}
> >  
> >  	return 0;
> > @@ -136,20 +225,28 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
> >  /**
> >   * lwmi_cd_alloc() - Allocate a cd_list struct in drvdata
> >   * @priv: lenovo-wmi-capdata driver data.
> > + * @type: The type of capability data.
> >   *
> >   * Allocate a cd_list struct large enough to contain data from all WMI data
> >   * blocks provided by the interface.
> >   *
> >   * Return: 0 on success, or an error.
> >   */
> > -static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
> > +static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
> >  {
> >  	struct cd_list *list;
> >  	size_t list_size;
> >  	int count, ret;
> >  
> >  	count = wmidev_instance_count(priv->wdev);
> > -	list_size = struct_size(list, data, count);
> > +
> > +	switch (type) {
> > +	case LENOVO_CAPABILITY_DATA_01:
> > +		list_size = struct_size(list, cd01, count);
> > +		break;
> > +	default:
> > +		return -EINVAL;
> > +	}
> >  
> >  	list = devm_kzalloc(&priv->wdev->dev, list_size, GFP_KERNEL);
> >  	if (!list)
> > @@ -159,6 +256,7 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
> >  	if (ret)
> >  		return ret;
> >  
> > +	list->type = type;
> >  	list->count = count;
> >  	priv->list = list;
> >  
> > @@ -168,6 +266,7 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
> >  /**
> >   * lwmi_cd_setup() - Cache all WMI data block information
> >   * @priv: lenovo-wmi-capdata driver data.
> > + * @type: The type of capability data.
> >   *
> >   * Allocate a cd_list struct large enough to contain data from all WMI data
> >   * blocks provided by the interface. Then loop through each data block and
> > @@ -175,11 +274,11 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv)
> >   *
> >   * Return: 0 on success, or an error code.
> >   */
> > -static int lwmi_cd_setup(struct lwmi_cd_priv *priv)
> > +static int lwmi_cd_setup(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
> >  {
> >  	int ret;
> >  
> > -	ret = lwmi_cd_alloc(priv);
> > +	ret = lwmi_cd_alloc(priv, type);
> >  	if (ret)
> >  		return ret;
> >  
> > @@ -235,9 +334,13 @@ static void lwmi_cd01_unregister(void *data)
> >  
> >  static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
> >  {
> > +	const struct lwmi_cd_info *info = context;
> >  	struct lwmi_cd_priv *priv;
> >  	int ret;
> >  
> > +	if (!info)
> > +		return -EINVAL;
> > +
> >  	priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
> >  	if (!priv)
> >  		return -ENOMEM;
> > @@ -245,30 +348,58 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
> >  	priv->wdev = wdev;
> >  	dev_set_drvdata(&wdev->dev, priv);
> >  
> > -	ret = lwmi_cd_setup(priv);
> > +	ret = lwmi_cd_setup(priv, info->type);
> >  	if (ret)
> > -		return ret;
> > +		goto out;
> >  
> > -	priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
> > +	switch (info->type) {
> > +	case LENOVO_CAPABILITY_DATA_01:
> > +		priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
> >  
> > -	ret = register_acpi_notifier(&priv->acpi_nb);
> > -	if (ret)
> > -		return ret;
> > +		ret = register_acpi_notifier(&priv->acpi_nb);
> > +		if (ret)
> > +			goto out;
> >  
> > -	ret = devm_add_action_or_reset(&wdev->dev, lwmi_cd01_unregister, &priv->acpi_nb);
> > -	if (ret)
> > -		return ret;
> > +		ret = devm_add_action_or_reset(&wdev->dev, lwmi_cd01_unregister,
> > +					       &priv->acpi_nb);
> > +		if (ret)
> > +			goto out;
> >  
> > -	return component_add(&wdev->dev, &lwmi_cd_component_ops);
> > +		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
> > +		goto out;
> > +	default:
> > +		return -EINVAL;
> > +	}
> > +out:
> > +	if (ret) {
> > +		dev_err(&wdev->dev, "failed to register %s: %d\n",
> 
> Include?
> 
> > +			info->name, ret);
> > +	} else {
> > +		dev_info(&wdev->dev, "registered %s with %u items\n",
> > +			 info->name, priv->list->count);
> 
> Success path should be quiet.

Will turn it into dev_dbg(). Thanks.

Thanks,
Rong

> > +	}
> > +	return ret;
> >  }
> >  
> >  static void lwmi_cd_remove(struct wmi_device *wdev)
> >  {
> > -	component_del(&wdev->dev, &lwmi_cd_component_ops);
> > +	struct lwmi_cd_priv *priv = dev_get_drvdata(&wdev->dev);
> > +
> > +	switch (priv->list->type) {
> > +	case LENOVO_CAPABILITY_DATA_01:
> > +		component_del(&wdev->dev, &lwmi_cd_component_ops);
> > +		break;
> > +	default:
> > +		WARN_ON(1);
> > +	}
> >  }
> >  
> > +#define LWMI_CD_WDEV_ID(_type)				\
> > +	.guid_string = _type##_GUID,			\
> > +	.context = &lwmi_cd_table[_type]
> 
> Add comma.
> 
> > +
> >  static const struct wmi_device_id lwmi_cd_id_table[] = {
> > -	{ LENOVO_CAPABILITY_DATA_01_GUID, NULL },
> > +	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_01) },
> >  	{}
> >  };
> >  
> > @@ -283,22 +414,10 @@ static struct wmi_driver lwmi_cd_driver = {
> >  	.no_singleton = true,
> >  };
> >  
> > -/**
> > - * lwmi_cd01_match() - Match rule for the master driver.
> > - * @dev: Pointer to the capability data 01 parent device.
> > - * @data: Unused void pointer for passing match criteria.
> > - *
> > - * Return: int.
> > - */
> > -int lwmi_cd01_match(struct device *dev, void *data)
> > -{
> > -	return dev->driver == &lwmi_cd_driver.driver;
> > -}
> > -EXPORT_SYMBOL_NS_GPL(lwmi_cd01_match, "LENOVO_WMI_CD");
> > -
> >  module_wmi_driver(lwmi_cd_driver);
> >  
> >  MODULE_DEVICE_TABLE(wmi, lwmi_cd_id_table);
> >  MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
> > +MODULE_AUTHOR("Rong Zhang <i@rong.moe>");
> >  MODULE_DESCRIPTION("Lenovo Capability Data WMI Driver");
> >  MODULE_LICENSE("GPL");
> > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
> > index 2a4746e38ad4..d326f9d2d165 100644
> > --- a/drivers/platform/x86/lenovo/wmi-capdata.h
> > +++ b/drivers/platform/x86/lenovo/wmi-capdata.h
> > @@ -7,6 +7,7 @@
> >  
> >  #include <linux/types.h>
> >  
> > +struct component_match;
> >  struct device;
> >  struct cd_list;
> >  
> > @@ -19,7 +20,11 @@ struct capdata01 {
> >  	u32 max_value;
> >  };
> >  
> > +struct lwmi_cd_binder {
> > +	struct cd_list *cd01_list;
> > +};
> > +
> > +void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
> >  int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output);
> > -int lwmi_cd01_match(struct device *dev, void *data);
> >  
> >  #endif /* !_LENOVO_WMI_CAPDATA_H_ */
> > diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> > index c6dc1b4cff84..f2e1e34d58a9 100644
> > --- a/drivers/platform/x86/lenovo/wmi-other.c
> > +++ b/drivers/platform/x86/lenovo/wmi-other.c
> > @@ -579,14 +579,14 @@ static void lwmi_om_fw_attr_remove(struct lwmi_om_priv *priv)
> >  static int lwmi_om_master_bind(struct device *dev)
> >  {
> >  	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> > -	struct cd_list *tmp_list;
> > +	struct lwmi_cd_binder binder = { 0 };
> 
> = {} is enough to initialize to default values.
> 
> >  	int ret;
> >  
> > -	ret = component_bind_all(dev, &tmp_list);
> > +	ret = component_bind_all(dev, &binder);
> >  	if (ret)
> >  		return ret;
> >  
> > -	priv->cd01_list = tmp_list;
> > +	priv->cd01_list = binder.cd01_list;
> >  	if (!priv->cd01_list)
> >  		return -ENODEV;
> >  
> > @@ -623,10 +623,13 @@ static int lwmi_other_probe(struct wmi_device *wdev, const void *context)
> >  	if (!priv)
> >  		return -ENOMEM;
> >  
> > +	/* Sentinel for on-demand ida_free(). */
> > +	priv->ida_id = -EIDRM;
> > +
> >  	priv->wdev = wdev;
> >  	dev_set_drvdata(&wdev->dev, priv);
> >  
> > -	component_match_add(&wdev->dev, &master_match, lwmi_cd01_match, NULL);
> > +	lwmi_cd_match_add_all(&wdev->dev, &master_match);
> >  	if (IS_ERR(master_match))
> >  		return PTR_ERR(master_match);
> >  
> > @@ -639,7 +642,10 @@ static void lwmi_other_remove(struct wmi_device *wdev)
> >  	struct lwmi_om_priv *priv = dev_get_drvdata(&wdev->dev);
> >  
> >  	component_master_del(&wdev->dev, &lwmi_om_master_ops);
> > -	ida_free(&lwmi_om_ida, priv->ida_id);
> > +
> > +	/* No IDA to free if the driver is never bound to its components. */
> > +	if (priv->ida_id >= 0)
> > +		ida_free(&lwmi_om_ida, priv->ida_id);
> >  }
> >  
> >  static const struct wmi_device_id lwmi_other_id_table[] = {
> > 

^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 5/7] platform/x86: lenovo-wmi-capdata: Add support for Fan Test Data
  2025-11-24 16:45   ` Ilpo Järvinen
@ 2025-11-24 20:07     ` Rong Zhang
  2025-11-26  7:44       ` Ilpo Järvinen
  0 siblings, 1 reply; 22+ messages in thread
From: Rong Zhang @ 2025-11-24 20:07 UTC (permalink / raw)
  To: Ilpo Järvinen
  Cc: Rong Zhang, Mark Pearson, Derek J. Clark, Armin Wolf,
	Hans de Goede, Guenter Roeck, platform-driver-x86, LKML,
	linux-hwmon

Hi Ilpo,

Thanks for your review.

On Mon, 2025-11-24 at 18:45 +0200, Ilpo Järvinen wrote:
> On Sun, 23 Nov 2025, Rong Zhang wrote:
> 
> > Add support for LENOVO_FAN_TEST_DATA WMI data block. Provides an
> > interface for querying the min/max fan speed RPM (reference data) of a
> > given fan ID.
> > 
> > This interface is optional. Hence, it does not bind to lenovo-wmi-other
> > and is not registered as a component for the moment. Appropriate binding
> > will be implemented in the subsequent patch.
> > 
> > Signed-off-by: Rong Zhang <i@rong.moe>
> > Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
> > Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
> > ---
> > Changes in v4:
> > - Rebase on top of changes made to [PATCH v4 3/7]
> > - Do not register it as a component until [PATCH v4 6/7]
> > 
> > Changes in v2:
> > - Reword documentation
> > ---
> >  .../wmi/devices/lenovo-wmi-other.rst          |  17 +++
> >  drivers/platform/x86/lenovo/wmi-capdata.c     | 102 ++++++++++++++++++
> >  drivers/platform/x86/lenovo/wmi-capdata.h     |   7 ++
> >  3 files changed, 126 insertions(+)
> > 
> > diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
> > index fcad595d49af..821282e07d93 100644
> > --- a/Documentation/wmi/devices/lenovo-wmi-other.rst
> > +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
> > @@ -62,6 +62,13 @@ The following firmware-attributes are implemented:
> >   - ppt_pl2_sppt: Platform Profile Tracking Slow Package Power Tracking
> >   - ppt_pl3_fppt: Platform Profile Tracking Fast Package Power Tracking
> >  
> > +LENOVO_FAN_TEST_DATA
> > +-------------------------
> > +
> > +WMI GUID ``B642801B-3D21-45DE-90AE-6E86F164FB21``
> > +
> > +The LENOVO_FAN_TEST_DATA interface provides reference data for self-test of
> > +cooling fans.
> >  
> >  WMI interface description
> >  =========================
> > @@ -115,3 +122,13 @@ data using the `bmfdec <https://github.com/pali/bmfdec>`_ utility:
> >      [WmiDataId(3), read, Description("Data Size.")] uint32 DataSize;
> >      [WmiDataId(4), read, Description("Default Value"), WmiSizeIs("DataSize")] uint8 DefaultValue[];
> >    };
> > +
> > +  [WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"), Description("Definition of Fan Test Data"), guid("{B642801B-3D21-45DE-90AE-6E86F164FB21}")]
> > +  class LENOVO_FAN_TEST_DATA {
> > +    [key, read] string InstanceName;
> > +    [read] boolean Active;
> > +    [WmiDataId(1), read, Description("Mode.")] uint32 NumOfFans;
> > +    [WmiDataId(2), read, Description("Fan ID."), WmiSizeIs("NumOfFans")] uint32 FanId[];
> > +    [WmiDataId(3), read, Description("Maximum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMaxSpeed[];
> > +    [WmiDataId(4), read, Description("Minumum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMinSpeed[];
> > +  };
> > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
> > index 29267c373ab3..e6392357395c 100644
> > --- a/drivers/platform/x86/lenovo/wmi-capdata.c
> > +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
> > @@ -13,6 +13,10 @@
> >   * attribute has multiple pages, one for each of the thermal modes managed by
> >   * the Gamezone interface.
> >   *
> > + * Fan Test Data includes the max/min fan speed RPM for each fan. This is
> > + * reference data for self-test. If the fan is in good condition, it is capable
> > + * to spin faster than max RPM or slower than min RPM.
> > + *
> >   * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
> >   *   - Initial implementation (formerly named lenovo-wmi-capdata01)
> >   *
> > @@ -41,6 +45,7 @@
> >  
> >  #define LENOVO_CAPABILITY_DATA_00_GUID "362A3AFE-3D96-4665-8530-96DAD5BB300E"
> >  #define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154"
> > +#define LENOVO_FAN_TEST_DATA_GUID "B642801B-3D21-45DE-90AE-6E86F164FB21"
> >  
> >  #define ACPI_AC_CLASS "ac_adapter"
> >  #define ACPI_AC_NOTIFY_STATUS 0x80
> > @@ -48,6 +53,7 @@
> >  enum lwmi_cd_type {
> >  	LENOVO_CAPABILITY_DATA_00,
> >  	LENOVO_CAPABILITY_DATA_01,
> > +	LENOVO_FAN_TEST_DATA,
> >  };
> >  
> >  #define LWMI_CD_TABLE_ITEM(_type)		\
> > @@ -62,6 +68,7 @@ static const struct lwmi_cd_info {
> >  } lwmi_cd_table[] = {
> >  	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_00),
> >  	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
> > +	LWMI_CD_TABLE_ITEM(LENOVO_FAN_TEST_DATA),
> >  };
> >  
> >  struct lwmi_cd_priv {
> > @@ -78,6 +85,7 @@ struct cd_list {
> >  	union {
> >  		DECLARE_FLEX_ARRAY(struct capdata00, cd00);
> >  		DECLARE_FLEX_ARRAY(struct capdata01, cd01);
> > +		DECLARE_FLEX_ARRAY(struct capdata_fan, cd_fan);
> >  	};
> >  };
> >  
> > @@ -117,6 +125,10 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
> >  		return;
> >  
> >  	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
> > +		/* Skip optional interfaces. */
> > +		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
> > +			continue;
> > +
> >  		component_match_add(master, matchptr, lwmi_cd_match,
> >  				    (void *)&lwmi_cd_table[i].type);
> >  		if (IS_ERR(*matchptr))
> > @@ -194,6 +206,9 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd00_get_data, "LENOVO_WMI_CD");
> >  DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
> >  EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
> >  
> > +DEF_LWMI_CDXX_GET_DATA(cd_fan, LENOVO_FAN_TEST_DATA, struct capdata_fan);
> > +EXPORT_SYMBOL_NS_GPL(lwmi_cd_fan_get_data, "LENOVO_WMI_CD");
> > +
> >  /**
> >   * lwmi_cd_cache() - Cache all WMI data block information
> >   * @priv: lenovo-wmi-capdata driver data.
> > @@ -217,6 +232,9 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
> >  		p = &priv->list->cd01[0];
> >  		size = sizeof(priv->list->cd01[0]);
> >  		break;
> > +	case LENOVO_FAN_TEST_DATA:
> > +		/* Done by lwmi_cd_alloc() => lwmi_cd_fan_list_alloc_cache(). */
> > +		return 0;
> >  	default:
> >  		return -EINVAL;
> >  	}
> > @@ -239,6 +257,78 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
> >  	return 0;
> >  }
> >  
> > +/**
> > + * lwmi_cd_fan_list_alloc_cache() - Alloc and cache Fan Test Data list
> > + * @priv: lenovo-wmi-capdata driver data.
> > + * @listptr: Pointer to returned cd_list pointer.
> > + *
> > + * Return: count of fans found, or an error.
> > + */
> > +static int lwmi_cd_fan_list_alloc_cache(struct lwmi_cd_priv *priv, struct cd_list **listptr)
> > +{
> > +	u32 count, *fan_ids, *fan_min_rpms, *fan_max_rpms;
> > +	union acpi_object *ret_obj __free(kfree) = NULL;
> 
> Since you're using __free(), please move this to where you assign the 
> value. This is to create a pattern with cleanup helpers. The cleanup 
> order depends on the order the variables are introduced which in some 
> other cases may be significant.

Make sense. Will move it to the last declaration lines with its
assignment. Thanks.

> > +	struct block { u32 nr; u32 data[]; } *block;
> 
> This is the first time I see this style anywhere in the kernel's context, 
> has there been some general discussion about this style somewhere?
> 
> At least it seems immediately obvious to me that this style will have a 
> negative impact on documentability due to (too) concise use of space.

Make sense. Will break it into multiple lines. Thanks.

> > +	struct cd_list *list;
> > +	size_t size;
> > +	int idx;
> > +
> > +	ret_obj = wmidev_block_query(priv->wdev, 0);
> > +	if (!ret_obj)
> > +		return -ENODEV;
> > +
> > +	/*
> > +	 * This is usually caused by a dummy ACPI method. Do not return an error
> > +	 * as failing to probe this device will result in master driver being
> > +	 * unbound - this behavior aligns with lwmi_cd_cache().
> > +	 */
> > +	if (ret_obj->type != ACPI_TYPE_BUFFER) {
> > +		count = 0;
> > +		goto alloc;
> > +	}
> > +
> > +	size = ret_obj->buffer.length;
> > +	block = (struct block *)ret_obj->buffer.pointer;
> 
> void * can be cast implicitly.

`ret_obj->buffer.pointer' is a `u8 *' pointer so the cast is mandatory.

Hmmm, this reminds me that `struct block' probably needs a `__packed'
to generate unaligned access to it.

> > +
> > +	count = size >= sizeof(*block) ? block->nr : 0;
> > +	if (size < struct_size(block, data, count * 3)) {
> > +		dev_warn(&priv->wdev->dev,
> > +			 "incomplete fan test data block: %zu < %zu, ignoring\n",
> > +			 size, struct_size(block, data, count * 3));
> > +		count = 0;
> > +	}
> > +
> > +	if (count == 0)
> > +		goto alloc;
> > +
> > +	if (count > U8_MAX) {
> > +		dev_warn(&priv->wdev->dev,
> > +			 "too many fans reported: %u > %u, truncating\n",
> > +			 count, U8_MAX);
> > +		count = U8_MAX;
> > +	}
> > +
> > +	fan_ids = &block->data[0];
> > +	fan_max_rpms = &block->data[count];
> > +	fan_min_rpms = &block->data[count * 2];
> 
> Please rearrange so you can remove the goto:
> 
> 	1. limit count
> 	if (count) {
> 		...
> 	}

Nice catch! Will rearrange. Thanks.

Thanks,
Rong

> > +
> > +alloc:
> > +	list = devm_kzalloc(&priv->wdev->dev, struct_size(list, cd_fan, count), GFP_KERNEL);
> > +	if (!list)
> > +		return -ENOMEM;
> > +
> > +	for (idx = 0; idx < count; idx++) {
> > +		list->cd_fan[idx] = (struct capdata_fan) {
> > +			.id = fan_ids[idx],
> > +			.min_rpm = fan_min_rpms[idx],
> > +			.max_rpm = fan_max_rpms[idx],
> > +		};
> > +	}
> > +
> > +	*listptr = list;
> > +	return count;
> > +}
> > +
> >  /**
> >   * lwmi_cd_alloc() - Allocate a cd_list struct in drvdata
> >   * @priv: lenovo-wmi-capdata driver data.
> > @@ -264,6 +354,12 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
> >  	case LENOVO_CAPABILITY_DATA_01:
> >  		list_size = struct_size(list, cd01, count);
> >  		break;
> > +	case LENOVO_FAN_TEST_DATA:
> > +		count = lwmi_cd_fan_list_alloc_cache(priv, &list);
> > +		if (count < 0)
> > +			return count;
> > +
> > +		goto got_list;
> >  	default:
> >  		return -EINVAL;
> >  	}
> > @@ -272,6 +368,7 @@ static int lwmi_cd_alloc(struct lwmi_cd_priv *priv, enum lwmi_cd_type type)
> >  	if (!list)
> >  		return -ENOMEM;
> >  
> > +got_list:
> >  	ret = devm_mutex_init(&priv->wdev->dev, &list->list_mutex);
> >  	if (ret)
> >  		return ret;
> > @@ -390,6 +487,8 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
> >  
> >  		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
> >  		goto out;
> > +	case LENOVO_FAN_TEST_DATA:
> > +		goto out;
> >  	default:
> >  		return -EINVAL;
> >  	}
> > @@ -413,6 +512,8 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
> >  	case LENOVO_CAPABILITY_DATA_01:
> >  		component_del(&wdev->dev, &lwmi_cd_component_ops);
> >  		break;
> > +	case LENOVO_FAN_TEST_DATA:
> > +		break;
> >  	default:
> >  		WARN_ON(1);
> >  	}
> > @@ -425,6 +526,7 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
> >  static const struct wmi_device_id lwmi_cd_id_table[] = {
> >  	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_00) },
> >  	{ LWMI_CD_WDEV_ID(LENOVO_CAPABILITY_DATA_01) },
> > +	{ LWMI_CD_WDEV_ID(LENOVO_FAN_TEST_DATA) },
> >  	{}
> >  };
> >  
> > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
> > index a6d006ef458f..38af4c4e4ef4 100644
> > --- a/drivers/platform/x86/lenovo/wmi-capdata.h
> > +++ b/drivers/platform/x86/lenovo/wmi-capdata.h
> > @@ -26,6 +26,12 @@ struct capdata01 {
> >  	u32 max_value;
> >  };
> >  
> > +struct capdata_fan {
> > +	u32 id;
> > +	u32 min_rpm;
> > +	u32 max_rpm;
> > +};
> > +
> >  struct lwmi_cd_binder {
> >  	struct cd_list *cd00_list;
> >  	struct cd_list *cd01_list;
> > @@ -34,5 +40,6 @@ struct lwmi_cd_binder {
> >  void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
> >  int lwmi_cd00_get_data(struct cd_list *list, u32 attribute_id, struct capdata00 *output);
> >  int lwmi_cd01_get_data(struct cd_list *list, u32 attribute_id, struct capdata01 *output);
> > +int lwmi_cd_fan_get_data(struct cd_list *list, u32 attribute_id, struct capdata_fan *output);
> >  
> >  #endif /* !_LENOVO_WMI_CAPDATA_H_ */
> > 

^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up Fan Test Data
  2025-11-24 16:49   ` Ilpo Järvinen
@ 2025-11-24 20:10     ` Rong Zhang
  0 siblings, 0 replies; 22+ messages in thread
From: Rong Zhang @ 2025-11-24 20:10 UTC (permalink / raw)
  To: Ilpo Järvinen
  Cc: Rong Zhang, Mark Pearson, Derek J. Clark, Armin Wolf,
	Hans de Goede, Guenter Roeck, platform-driver-x86, LKML,
	linux-hwmon

Hi Ilpo,

Thanks for your review.

On Mon, 2025-11-24 at 18:49 +0200, Ilpo Järvinen wrote:
> On Sun, 23 Nov 2025, Rong Zhang wrote:
> 
> > A capdata00 attribute (0x04050000) describes the presence of Fan Test
> > Data. Query it, and bind Fan Test Data as a component of capdata00
> > accordingly. The component master of capdata00 may pass a callback while
> > binding to retrieve fan info from Fan Test Data.
> > 
> > Summarizing this scheme:
> > 
> > 	lenovo-wmi-other <-> capdata00 <-> capdata_fan
> > 	|- master            |- component
> > 	                     |- sub-master
> > 	                                   |- sub-component
> > 
> > The callback will be called once both the master and the sub-component
> > are bound to the sub-master (component).
> > 
> > This scheme is essential to solve these issues:
> > - The component framework only supports one aggregation per master
> > - A binding is only established until all components are found
> > - The Fan Test Data interface may be missing on some devices
> > - To get rid of queries for the presense of WMI GUIDs
> > 
> > capdata00 is registered as a component and a sub-master on probe,
> > instead of chaining the registerations in one's bind callback. This is
> > because calling (un)registration methods of the component framework
> > causes deadlock in (un)bind callbacks, i.e., it's impossible to register
> > capdata00 as a sub-master/component in its component/sub-master bind
> > callback, and vice versa.
> > 
> > Signed-off-by: Rong Zhang <i@rong.moe>
> > ---
> > Changes in v6:
> > - Fix the error path of component_add(capdata00)
> > - Elaborate the design in commit message
> > 
> > Changes in v5:
> > - Fix missing include (thanks kernel test robot)
> > 
> > Changes in v4:
> > - New patch in the series (thanks Armin Wolf's inspiration)
> >   - Get rid of wmi_has_guid() (see also [PATCH v4 3/7])
> > ---
> >  drivers/platform/x86/lenovo/wmi-capdata.c | 265 +++++++++++++++++++++-
> >  drivers/platform/x86/lenovo/wmi-capdata.h |  20 ++
> >  drivers/platform/x86/lenovo/wmi-other.c   |   5 -
> >  3 files changed, 283 insertions(+), 7 deletions(-)
> > 
> > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
> > index e6392357395c..8760f8c071ca 100644
> > --- a/drivers/platform/x86/lenovo/wmi-capdata.c
> > +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
> > @@ -27,6 +27,7 @@
> >  #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
> >  
> >  #include <linux/acpi.h>
> > +#include <linux/bitfield.h>
> >  #include <linux/cleanup.h>
> >  #include <linux/component.h>
> >  #include <linux/container_of.h>
> > @@ -50,10 +51,17 @@
> >  #define ACPI_AC_CLASS "ac_adapter"
> >  #define ACPI_AC_NOTIFY_STATUS 0x80
> >  
> > +#define LWMI_FEATURE_ID_FAN_TEST 0x05
> > +
> > +#define LWMI_ATTR_ID_FAN_TEST							\
> > +	(FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) |		\
> > +	 FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_TEST))
> > +
> >  enum lwmi_cd_type {
> >  	LENOVO_CAPABILITY_DATA_00,
> >  	LENOVO_CAPABILITY_DATA_01,
> >  	LENOVO_FAN_TEST_DATA,
> > +	CD_TYPE_NONE = -1,
> >  };
> >  
> >  #define LWMI_CD_TABLE_ITEM(_type)		\
> > @@ -75,6 +83,20 @@ struct lwmi_cd_priv {
> >  	struct notifier_block acpi_nb; /* ACPI events */
> >  	struct wmi_device *wdev;
> >  	struct cd_list *list;
> > +
> > +	/*
> > +	 * A capdata device may be a component master of another capdata device.
> > +	 * E.g., lenovo-wmi-other <-> capdata00 <-> capdata_fan
> > +	 *       |- master            |- component
> > +	 *                            |- sub-master
> > +	 *                                          |- sub-component
> > +	 */
> > +	struct lwmi_cd_sub_master_priv {
> > +		struct device *master_dev;
> > +		cd_list_cb_t master_cb;
> > +		struct cd_list *sub_component_list; /* ERR_PTR(-ENODEV) implies no sub-component. */
> > +		bool registered;                    /* Has the sub-master been registered? */
> > +	} *sub_master;
> >  };
> >  
> >  struct cd_list {
> > @@ -125,7 +147,7 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
> >  		return;
> >  
> >  	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
> > -		/* Skip optional interfaces. */
> > +		/* Skip sub-components. */
> 
> Didn't you just introduce it? If that's the case, please try to avoid such 
> back-and-forth changes within the series.

Ahh yes. Will rearrange the change. Thanks.

> >  		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
> >  			continue;
> >  
> > @@ -137,6 +159,56 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
> >  }
> >  EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
> >  
> > +/**
> > + * lwmi_cd_call_master_cb() - Call the master callback for the sub-component.
> > + * @priv: Pointer to the capability data private data.
> > + *
> > + * Call the master callback and pass the sub-component list to it if the
> > + * dependency chain (master <-> sub-master <-> sub-component) is complete.
> > + */
> > +static void lwmi_cd_call_master_cb(struct lwmi_cd_priv *priv)
> > +{
> > +	struct cd_list *sub_component_list = priv->sub_master->sub_component_list;
> > +
> > +	/*
> > +	 * Call the callback only if the dependency chain is ready:
> > +	 * - Binding between master and sub-master: fills master_dev and master_cb
> > +	 * - Binding between sub-master and sub-component: fills sub_component_list
> > +	 *
> > +	 * If a binding has been unbound before the other binding is bound, the
> > +	 * corresponding members filled by the former are guaranteed to be cleared.
> > +	 *
> > +	 * This function is only called in bind callbacks, and the component
> > +	 * framework guarantees bind/unbind callbacks may never execute
> > +	 * simultaneously, which implies that it's impossible to have a race
> > +	 * condition.
> > +	 *
> > +	 * Hence, this check is sufficient to ensure that the callback is called
> > +	 * at most once and with the correct state, without relying on a specific
> > +	 * sequence of binding establishment.
> > +	 */
> > +	if (!sub_component_list ||
> > +	    !priv->sub_master->master_dev ||
> > +	    !priv->sub_master->master_cb)
> > +		return;
> > +
> > +	if (PTR_ERR(sub_component_list) == -ENODEV)
> 
> Include?
> 
> > +		sub_component_list = NULL;
> > +	else if (WARN_ON(IS_ERR(sub_component_list)))
> 
> Include?
> 
> (Some earlier patch may already need to add these, it probably was 
> relevant there already)

*Embarrassed voice* Thanks for all the #include reminders!

> > +		return;
> > +
> > +	priv->sub_master->master_cb(priv->sub_master->master_dev,
> > +				    sub_component_list);
> > +
> > +	/*
> > +	 * Prevent "unbind and rebind" sequences from userspace from calling the
> > +	 * callback twice.
> > +	 */
> > +	priv->sub_master->master_cb = NULL;
> > +	priv->sub_master->master_dev = NULL;
> > +	priv->sub_master->sub_component_list = NULL;
> > +}
> > +
> >  /**
> >   * lwmi_cd_component_bind() - Bind component to master device.
> >   * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
> > @@ -147,6 +219,8 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
> >   * list. This is used to call lwmi_cd*_get_data to look up attribute data
> >   * from the lenovo-wmi-other driver.
> >   *
> > + * If cd_dev is a sub-master, try to call the master callback.
> > + *
> >   * Return: 0
> >   */
> >  static int lwmi_cd_component_bind(struct device *cd_dev,
> > @@ -158,6 +232,11 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
> >  	switch (priv->list->type) {
> >  	case LENOVO_CAPABILITY_DATA_00:
> >  		binder->cd00_list = priv->list;
> > +
> > +		priv->sub_master->master_dev = om_dev;
> > +		priv->sub_master->master_cb = binder->cd_fan_list_cb;
> > +		lwmi_cd_call_master_cb(priv);
> > +
> >  		break;
> >  	case LENOVO_CAPABILITY_DATA_01:
> >  		binder->cd01_list = priv->list;
> > @@ -169,8 +248,167 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
> >  	return 0;
> >  }
> >  
> > +/**
> > + * lwmi_cd_component_unbind() - Unbind component to master device.
> > + * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
> > + * @om_dev: Pointer to the lenovo-wmi-other driver parent device.
> > + * @data: Unused.
> > + *
> > + * If cd_dev is a sub-master, clear the collected data from the master device to
> > + * prevent the binding establishment between the sub-master and the sub-
> > + * component (if it's about to happen) from calling the master callback.
> > + */
> > +static void lwmi_cd_component_unbind(struct device *cd_dev,
> > +				     struct device *om_dev, void *data)
> > +{
> > +	struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
> > +
> > +	switch (priv->list->type) {
> > +	case LENOVO_CAPABILITY_DATA_00:
> > +		priv->sub_master->master_dev = NULL;
> > +		priv->sub_master->master_cb = NULL;
> > +		return;
> > +	default:
> > +		return;
> > +	}
> > +}
> > +
> >  static const struct component_ops lwmi_cd_component_ops = {
> >  	.bind = lwmi_cd_component_bind,
> > +	.unbind = lwmi_cd_component_unbind,
> > +};
> > +
> > +/**
> > + * lwmi_cd_sub_master_bind() - Bind sub-component of sub-master device
> > + * @dev: The sub-master capdata basic device.
> > + *
> > + * Call component_bind_all to bind the sub-component device to the sub-master
> > + * device. On success, collect the pointer to the sub-component list and try
> > + * to call the master callback.
> > + *
> > + * Return: 0 on success, or an error code.
> > + */
> > +static int lwmi_cd_sub_master_bind(struct device *dev)
> > +{
> > +	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
> > +	struct cd_list *sub_component_list;
> > +	int ret;
> > +
> > +	ret = component_bind_all(dev, &sub_component_list);
> > +	if (ret)
> > +		return ret;
> > +
> > +	priv->sub_master->sub_component_list = sub_component_list;
> > +	lwmi_cd_call_master_cb(priv);
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * lwmi_cd_sub_master_unbind() - Unbind sub-component of sub-master device
> > + * @dev: The sub-master capdata basic device
> > + *
> > + * Clear the collected pointer to the sub-component list to prevent the binding
> > + * establishment between the sub-master and the sub-component (if it's about to
> > + * happen) from calling the master callback. Then, call component_unbind_all to
> > + * unbind the sub-component device from the sub-master device.
> > + */
> > +static void lwmi_cd_sub_master_unbind(struct device *dev)
> > +{
> > +	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
> > +
> > +	priv->sub_master->sub_component_list = NULL;
> > +
> > +	component_unbind_all(dev, NULL);
> > +}
> > +
> > +static const struct component_master_ops lwmi_cd_sub_master_ops = {
> > +	.bind = lwmi_cd_sub_master_bind,
> > +	.unbind = lwmi_cd_sub_master_unbind,
> > +};
> > +
> > +/**
> > + * lwmi_cd_sub_master_add() - Register a sub-master with its sub-component
> > + * @priv: Pointer to the sub-master capdata device private data.
> > + * @sub_component_type: Type of the sub-component.
> > + *
> > + * Match the sub-component type and register the current capdata device as a
> > + * sub-master. If the given sub-component type is CD_TYPE_NONE, mark the sub-
> > + * component as non-existent without registering sub-master.
> > + *
> > + * Return: 0 on success, or an error code.
> > + */
> > +static int lwmi_cd_sub_master_add(struct lwmi_cd_priv *priv,
> > +				  enum lwmi_cd_type sub_component_type)
> > +{
> > +	struct component_match *master_match = NULL;
> > +	int ret;
> > +
> > +	priv->sub_master = devm_kzalloc(&priv->wdev->dev, sizeof(*priv->sub_master), GFP_KERNEL);
> > +	if (!priv->sub_master)
> > +		return -ENOMEM;
> > +
> > +	if (sub_component_type == CD_TYPE_NONE) {
> > +		/* The master callback will be called with NULL on bind. */
> > +		priv->sub_master->sub_component_list = ERR_PTR(-ENODEV);
> > +		priv->sub_master->registered = false;
> > +		return 0;
> > +	}
> > +
> > +	/*
> > +	 * lwmi_cd_match() needs a pointer to enum lwmi_cd_type, but on-stack
> > +	 * data cannot be used here. Steal one from lwmi_cd_table.
> > +	 */
> > +	component_match_add(&priv->wdev->dev, &master_match, lwmi_cd_match,
> > +			    (void *)&lwmi_cd_table[sub_component_type].type);
> 
> Is void * cast required?

Yes, this is to drop the const qualifier inherited from the array. See
my reply to [PATCH v6 3/7].

> > +	if (IS_ERR(master_match))
> > +		return PTR_ERR(master_match);
> > +
> > +	ret = component_master_add_with_match(&priv->wdev->dev, &lwmi_cd_sub_master_ops,
> > +					      master_match);
> > +	if (ret)
> > +		return ret;
> > +
> > +	priv->sub_master->registered = true;
> > +	return 0;
> > +}
> > +
> > +/**
> > + * lwmi_cd_sub_master_del() - Unregister a sub-master if it's registered
> > + * @priv: Pointer to the sub-master capdata device private data.
> > + */
> > +static void lwmi_cd_sub_master_del(struct lwmi_cd_priv *priv)
> > +{
> > +	if (priv->sub_master->registered) {
> 
> Reverse the logic and return early instead.

Will reverse it. Thanks.

Thanks,
Rong

> > +		component_master_del(&priv->wdev->dev, &lwmi_cd_sub_master_ops);
> > +		priv->sub_master->registered = false;
> > +	}
> > +}
> > +
> > +/**
> > + * lwmi_cd_sub_component_bind() - Bind sub-component to sub-master device.
> > + * @sc_dev: Pointer to the sub-component capdata parent device.
> > + * @sm_dev: Pointer to the sub-master capdata parent device.
> > + * @data: Pointer used to return the capability data list pointer.
> > + *
> > + * On sub-master's bind, provide a pointer to the local capdata list.
> > + * This is used by the sub-master to call the master callback.
> > + *
> > + * Return: 0
> > + */
> > +static int lwmi_cd_sub_component_bind(struct device *sc_dev,
> > +				      struct device *sm_dev, void *data)
> > +{
> > +	struct lwmi_cd_priv *priv = dev_get_drvdata(sc_dev);
> > +	struct cd_list **listp = data;
> > +
> > +	*listp = priv->list;
> > +
> > +	return 0;
> > +}
> > +
> > +static const struct component_ops lwmi_cd_sub_component_ops = {
> > +	.bind = lwmi_cd_sub_component_bind,
> >  };
> >  
> >  /**
> > @@ -470,9 +708,28 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
> >  		goto out;
> >  
> >  	switch (info->type) {
> > -	case LENOVO_CAPABILITY_DATA_00:
> > +	case LENOVO_CAPABILITY_DATA_00: {
> > +		enum lwmi_cd_type sub_component_type = LENOVO_FAN_TEST_DATA;
> > +		struct capdata00 capdata00;
> > +
> > +		ret = lwmi_cd00_get_data(priv->list, LWMI_ATTR_ID_FAN_TEST, &capdata00);
> > +		if (ret || !(capdata00.supported & LWMI_SUPP_VALID)) {
> > +			dev_dbg(&wdev->dev, "capdata00 declares no fan test support\n");
> > +			sub_component_type = CD_TYPE_NONE;
> > +		}
> > +
> > +		/* Sub-master (capdata00) <-> sub-component (capdata_fan) */
> > +		ret = lwmi_cd_sub_master_add(priv, sub_component_type);
> > +		if (ret)
> > +			goto out;
> > +
> > +		/* Master (lenovo-wmi-other) <-> sub-master (capdata00) */
> >  		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
> > +		if (ret)
> > +			lwmi_cd_sub_master_del(priv);
> > +
> >  		goto out;
> > +	}
> >  	case LENOVO_CAPABILITY_DATA_01:
> >  		priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
> >  
> > @@ -488,6 +745,7 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
> >  		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
> >  		goto out;
> >  	case LENOVO_FAN_TEST_DATA:
> > +		ret = component_add(&wdev->dev, &lwmi_cd_sub_component_ops);
> >  		goto out;
> >  	default:
> >  		return -EINVAL;
> > @@ -509,10 +767,13 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
> >  
> >  	switch (priv->list->type) {
> >  	case LENOVO_CAPABILITY_DATA_00:
> > +		lwmi_cd_sub_master_del(priv);
> > +		fallthrough;
> >  	case LENOVO_CAPABILITY_DATA_01:
> >  		component_del(&wdev->dev, &lwmi_cd_component_ops);
> >  		break;
> >  	case LENOVO_FAN_TEST_DATA:
> > +		component_del(&wdev->dev, &lwmi_cd_sub_component_ops);
> >  		break;
> >  	default:
> >  		WARN_ON(1);
> > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
> > index 38af4c4e4ef4..59ca3b3e5760 100644
> > --- a/drivers/platform/x86/lenovo/wmi-capdata.h
> > +++ b/drivers/platform/x86/lenovo/wmi-capdata.h
> > @@ -5,8 +5,20 @@
> >  #ifndef _LENOVO_WMI_CAPDATA_H_
> >  #define _LENOVO_WMI_CAPDATA_H_
> >  
> > +#include <linux/bits.h>
> >  #include <linux/types.h>
> >  
> > +#define LWMI_SUPP_VALID		BIT(0)
> > +#define LWMI_SUPP_MAY_GET	(LWMI_SUPP_VALID | BIT(1))
> > +#define LWMI_SUPP_MAY_SET	(LWMI_SUPP_VALID | BIT(2))
> > +
> > +#define LWMI_ATTR_DEV_ID_MASK	GENMASK(31, 24)
> > +#define LWMI_ATTR_FEAT_ID_MASK	GENMASK(23, 16)
> > +#define LWMI_ATTR_MODE_ID_MASK	GENMASK(15, 8)
> > +#define LWMI_ATTR_TYPE_ID_MASK	GENMASK(7, 0)
> > +
> > +#define LWMI_DEVICE_ID_FAN	0x04
> > +
> >  struct component_match;
> >  struct device;
> >  struct cd_list;
> > @@ -32,9 +44,17 @@ struct capdata_fan {
> >  	u32 max_rpm;
> >  };
> >  
> > +typedef void (*cd_list_cb_t)(struct device *master_dev, struct cd_list *cd_list);
> > +
> >  struct lwmi_cd_binder {
> >  	struct cd_list *cd00_list;
> >  	struct cd_list *cd01_list;
> > +	/*
> > +	 * May be called during or after the bind callback.
> > +	 * Will be called with NULL if capdata_fan does not exist.
> > +	 * The pointer is only valid in the callback; never keep it for later use!
> > +	 */
> > +	cd_list_cb_t cd_fan_list_cb;
> >  };
> >  
> >  void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
> > diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> > index f2e1e34d58a9..b3adcc2804fa 100644
> > --- a/drivers/platform/x86/lenovo/wmi-other.c
> > +++ b/drivers/platform/x86/lenovo/wmi-other.c
> > @@ -54,11 +54,6 @@
> >  #define LWMI_FEATURE_VALUE_GET 17
> >  #define LWMI_FEATURE_VALUE_SET 18
> >  
> > -#define LWMI_ATTR_DEV_ID_MASK GENMASK(31, 24)
> > -#define LWMI_ATTR_FEAT_ID_MASK GENMASK(23, 16)
> > -#define LWMI_ATTR_MODE_ID_MASK GENMASK(15, 8)
> > -#define LWMI_ATTR_TYPE_ID_MASK GENMASK(7, 0)
> > -
> >  #define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other"
> >  
> >  static BLOCKING_NOTIFIER_HEAD(om_chain_head);
> > 

^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 7/7] platform/x86: lenovo-wmi-other: Add HWMON for fan reporting/tuning
  2025-11-24 16:58   ` Ilpo Järvinen
@ 2025-11-24 20:12     ` Rong Zhang
  2025-11-26  8:04       ` Ilpo Järvinen
  0 siblings, 1 reply; 22+ messages in thread
From: Rong Zhang @ 2025-11-24 20:12 UTC (permalink / raw)
  To: Ilpo Järvinen
  Cc: Rong Zhang, Mark Pearson, Derek J. Clark, Armin Wolf,
	Hans de Goede, Guenter Roeck, platform-driver-x86, LKML,
	linux-hwmon

Hi Ilpo,

Thanks for your review.

On Mon, 2025-11-24 at 18:58 +0200, Ilpo Järvinen wrote:
> On Sun, 23 Nov 2025, Rong Zhang wrote:
> 
> > Register an HWMON device for fan reporting/tuning according to
> > Capability Data 00 (capdata00) and Fan Test Data (capdata_fan) provided
> > by lenovo-wmi-capdata. The corresponding HWMON nodes are:
> > 
> >  - fanX_enable: enable/disable the fan (tunable)
> >  - fanX_input: current RPM
> >  - fanX_max: maximum RPM
> >  - fanX_min: minimum RPM
> >  - fanX_target: target RPM (tunable)
> > 
> > Information from capdata00 and capdata_fan are used to control the
> > visibility and constraints of HWMON attributes. Fan info from capdata00
> > is collected on bind, while fan info from capdata_fan is collected in a
> > callback. Once all fan info is collected, register the HWMON device.
> > 
> > Signed-off-by: Rong Zhang <i@rong.moe>
> > ---
> > Changes in v4:
> > - Rework HWMON registration due to the introduction of [PATCH v4 6/7]
> >   - Collect fan info from capdata00 and capdata_fan separately
> >   - Use a callback to collect fan info from capdata_fan
> >   - Trigger HWMON registration only if all fan info is collected
> >   - Do not check 0x04050000.supported, implied by the presense of
> >     capdata_fan
> > - Drop Reviewed-by & Tested-by due to the changes, please review & test
> > 
> > Changes in v3:
> > - Reword documentation (thanks Derek J. Clark)
> > 
> > Changes in v2:
> > - Define 4 fan channels instead of 2 (thanks Derek J. Clark)
> > - Squash min/max reporting patch into this one (ditto)
> > - Query 0x04050000 for interface availability (ditto)
> >   - New parameter "expose_all_fans" to skip this check
> > - Enforce min/max RPM constraint on set (ditto)
> >   - New parameter "relax_fan_constraint" to disable this behavior
> >   - Drop parameter "ignore_fan_cap", superseded by the next one
> >   - New parameter "expose_all_fans" to expose fans w/o such data
> > - Assume auto mode on probe (ditto)
> > - Reword documentation (ditto)
> > - Do not register HWMON device if no fan can be exposed
> > - fanX_target: Return -EBUSY instead of raw target value when fan stops
> > ---
> >  .../wmi/devices/lenovo-wmi-other.rst          |  11 +
> >  drivers/platform/x86/lenovo/Kconfig           |   1 +
> >  drivers/platform/x86/lenovo/wmi-other.c       | 485 +++++++++++++++++-
> >  3 files changed, 487 insertions(+), 10 deletions(-)
> > 
> > diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
> > index 821282e07d93..bd1d733ff286 100644
> > --- a/Documentation/wmi/devices/lenovo-wmi-other.rst
> > +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
> > @@ -31,6 +31,8 @@ under the following path:
> >  
> >    /sys/class/firmware-attributes/lenovo-wmi-other/attributes/<attribute>/
> >  
> > +Additionally, this driver also exports attributes to HWMON.
> > +
> >  LENOVO_CAPABILITY_DATA_00
> >  -------------------------
> >  
> > @@ -39,6 +41,11 @@ WMI GUID ``362A3AFE-3D96-4665-8530-96DAD5BB300E``
> >  The LENOVO_CAPABILITY_DATA_00 interface provides various information that
> >  does not rely on the gamezone thermal mode.
> >  
> > +The following HWMON attributes are implemented:
> > + - fanX_enable: enable/disable the fan (tunable)
> > + - fanX_input: current RPM
> > + - fanX_target: target RPM (tunable)
> > +
> >  LENOVO_CAPABILITY_DATA_01
> >  -------------------------
> >  
> > @@ -70,6 +77,10 @@ WMI GUID ``B642801B-3D21-45DE-90AE-6E86F164FB21``
> >  The LENOVO_FAN_TEST_DATA interface provides reference data for self-test of
> >  cooling fans.
> >  
> > +The following HWMON attributes are implemented:
> > + - fanX_max: maximum RPM
> > + - fanX_min: minimum RPM
> > +
> >  WMI interface description
> >  =========================
> >  
> > diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
> > index fb96a0f908f0..be9af0451146 100644
> > --- a/drivers/platform/x86/lenovo/Kconfig
> > +++ b/drivers/platform/x86/lenovo/Kconfig
> > @@ -263,6 +263,7 @@ config LENOVO_WMI_GAMEZONE
> >  config LENOVO_WMI_TUNING
> >  	tristate "Lenovo Other Mode WMI Driver"
> >  	depends on ACPI_WMI
> > +	select HWMON
> >  	select FW_ATTR_CLASS
> >  	select LENOVO_WMI_DATA
> >  	select LENOVO_WMI_EVENTS
> > diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> > index b3adcc2804fa..ce1ca13db4b5 100644
> > --- a/drivers/platform/x86/lenovo/wmi-other.c
> > +++ b/drivers/platform/x86/lenovo/wmi-other.c
> > @@ -14,7 +14,16 @@
> >   * These attributes typically don't fit anywhere else in the sysfs and are set
> >   * in Windows using one of Lenovo's multiple user applications.
> >   *
> > + * Additionally, this driver also exports tunable fan speed RPM to HWMON.
> > + * Min/max RPM are also provided for reference.
> > + *
> >   * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
> > + *   - fw_attributes
> > + *   - binding to Capability Data 01
> > + *
> > + * Copyright (C) 2025 Rong Zhang <i@rong.moe>
> > + *   - HWMON
> > + *   - binding to Capability Data 00 and Fan
> >   */
> >  
> >  #include <linux/acpi.h>
> > @@ -25,6 +34,7 @@
> >  #include <linux/device.h>
> >  #include <linux/export.h>
> >  #include <linux/gfp_types.h>
> > +#include <linux/hwmon.h>
> >  #include <linux/idr.h>
> >  #include <linux/kdev_t.h>
> >  #include <linux/kobject.h>
> > @@ -49,12 +59,26 @@
> >  #define LWMI_FEATURE_ID_CPU_SPL 0x02
> >  #define LWMI_FEATURE_ID_CPU_FPPT 0x03
> >  
> > +#define LWMI_FEATURE_ID_FAN_RPM 0x03
> > +
> >  #define LWMI_TYPE_ID_NONE 0x00
> >  
> >  #define LWMI_FEATURE_VALUE_GET 17
> >  #define LWMI_FEATURE_VALUE_SET 18
> >  
> > +#define LWMI_FAN_ID_BASE 1
> > +#define LWMI_FAN_NR 4
> > +#define LWMI_FAN_ID(x) ((x) + LWMI_FAN_ID_BASE)
> > +
> > +#define LWMI_ATTR_ID_FAN_RPM(x)						\
> > +	(FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) |	\
> > +	 FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_RPM) |	\
> > +	 FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, LWMI_FAN_ID(x)))
> > +
> > +#define LWMI_FAN_STOP_RPM 1
> > +
> >  #define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other"
> > +#define LWMI_OM_HWMON_NAME "lenovo_wmi_other"
> >  
> >  static BLOCKING_NOTIFIER_HEAD(om_chain_head);
> >  static DEFINE_IDA(lwmi_om_ida);
> > @@ -71,15 +95,439 @@ struct lwmi_om_priv {
> >  	struct component_master_ops *ops;
> >  
> >  	/* only valid after capdata bind */
> > +	struct cd_list *cd00_list;
> >  	struct cd_list *cd01_list;
> >  
> > +	struct device *hwmon_dev;
> >  	struct device *fw_attr_dev;
> >  	struct kset *fw_attr_kset;
> >  	struct notifier_block nb;
> >  	struct wmi_device *wdev;
> >  	int ida_id;
> > +
> > +	struct fan_info {
> > +		u32 supported;
> > +		u32 last_target;
> > +		long min_rpm;
> > +		long max_rpm;
> > +	} fan_info[LWMI_FAN_NR];
> 
> I personally don't like this style at all because it makes finding the 
> type of the variable harder with grep.

Make sense. Will add a `lwmi_' prefix to the struct name. Thanks.

> Add an empty line.

Will fix this and the following ones. Thanks.

Thanks,
Rong

> > +	struct {
> > +		bool capdata00_collected : 1;
> > +		bool capdata_fan_collected : 1;
> > +	} fan_flags;
> > +};
> > +
> > +/*
> > + * Visibility of fan channels:
> > + *
> > + * +-------------------+---------+------------------+-----------------------+------------+
> > + * |                   | default | +expose_all_fans | +relax_fan_constraint | +both      |
> > + * +-------------------+---------+------------------+-----------------------+------------+
> > + * | canonical         | RW      | RW               | RW+relaxed            | RW+relaxed |
> > + * +-------------------+---------+------------------+-----------------------+------------+
> > + * | -capdata_fan[idx] | N       | RO               | N                     | RW+relaxed |
> > + * +-------------------+---------+------------------+-----------------------+------------+
> > + *
> > + * Note:
> > + * 1. LWMI_ATTR_ID_FAN_RPM[idx].supported is always checked before exposing a channel.
> > + * 2. -capdata_fan implies -capdata_fan[idx].
> > + */
> > +static bool expose_all_fans;
> > +module_param(expose_all_fans, bool, 0444);
> > +MODULE_PARM_DESC(expose_all_fans,
> > +	"This option skips some capability checks and solely relies on per-channel ones "
> > +	"to expose fan attributes. Use with caution.");
> > +
> > +static bool relax_fan_constraint;
> > +module_param(relax_fan_constraint, bool, 0444);
> > +MODULE_PARM_DESC(relax_fan_constraint,
> > +	"Do not enforce fan RPM constraint (min/max RPM) "
> > +	"and enables fan tuning when such data is missing. "
> > +	"Enabling this may results in HWMON attributes being out-of-sync. Use with caution.");
> > +
> > +/* ======== HWMON (component: lenovo-wmi-capdata 00 & fan) ======== */
> > +
> > +/**
> > + * lwmi_om_fan_get_set() - Get or set fan RPM value of specified fan
> > + * @priv: Driver private data structure
> > + * @channel: Fan channel index (0-based)
> > + * @val: Pointer to value (input for set, output for get)
> > + * @set: True to set value, false to get value
> > + *
> > + * Communicates with WMI interface to either retrieve current fan RPM
> > + * or set target fan RPM.
> > + *
> > + * Return: 0 on success, or an error code.
> > + */
> > +static int lwmi_om_fan_get_set(struct lwmi_om_priv *priv, int channel, u32 *val, bool set)
> > +{
> > +	struct wmi_method_args_32 args;
> > +	u32 method_id, retval;
> > +	int err;
> > +
> > +	method_id = set ? LWMI_FEATURE_VALUE_SET : LWMI_FEATURE_VALUE_GET;
> > +	args.arg0 = LWMI_ATTR_ID_FAN_RPM(channel);
> > +	args.arg1 = set ? *val : 0;
> > +
> > +	err = lwmi_dev_evaluate_int(priv->wdev, 0x0, method_id,
> > +				    (unsigned char *)&args, sizeof(args), &retval);
> > +	if (err)
> > +		return err;
> > +
> > +	if (!set)
> > +		*val = retval;
> > +	else if (retval != 1)
> > +		return -EIO;
> > +
> > +	return 0;
> > +}
> > +
> > +/**
> > + * lwmi_om_hwmon_is_visible() - Determine visibility of HWMON attributes
> > + * @drvdata: Driver private data
> > + * @type: Sensor type
> > + * @attr: Attribute identifier
> > + * @channel: Channel index
> > + *
> > + * Determines whether an HWMON attribute should be visible in sysfs
> > + * based on hardware capabilities and current configuration.
> > + *
> > + * Return: permission mode, or 0 if invisible.
> > + */
> > +static umode_t lwmi_om_hwmon_is_visible(const void *drvdata, enum hwmon_sensor_types type,
> > +					u32 attr, int channel)
> > +{
> > +	struct lwmi_om_priv *priv = (struct lwmi_om_priv *)drvdata;
> > +	bool visible = false;
> > +
> > +	if (type == hwmon_fan) {
> > +		if (!(priv->fan_info[channel].supported & LWMI_SUPP_VALID))
> > +			return 0;
> > +
> > +		switch (attr) {
> > +		case hwmon_fan_enable:
> > +		case hwmon_fan_target:
> > +			if (!(priv->fan_info[channel].supported & LWMI_SUPP_MAY_SET))
> > +				return 0;
> > +
> > +			if (relax_fan_constraint ||
> > +			    (priv->fan_info[channel].min_rpm >= 0 &&
> > +			     priv->fan_info[channel].max_rpm >= 0))
> > +				return 0644;
> > +
> > +			/*
> > +			 * Reaching here implies expose_all_fans is set.
> > +			 * See lwmi_om_hwmon_add().
> > +			 */
> > +			dev_warn_once(&priv->wdev->dev,
> > +				      "fan tuning disabled due to missing RPM constraint\n");
> > +			return 0;
> > +		case hwmon_fan_input:
> > +			visible = priv->fan_info[channel].supported & LWMI_SUPP_MAY_GET;
> > +			break;
> > +		case hwmon_fan_min:
> > +			visible = priv->fan_info[channel].min_rpm >= 0;
> > +			break;
> > +		case hwmon_fan_max:
> > +			visible = priv->fan_info[channel].max_rpm >= 0;
> > +			break;
> > +		}
> > +	}
> > +
> > +	return visible ? 0444 : 0;
> > +}
> > +
> > +/**
> > + * lwmi_om_hwmon_read() - Read HWMON sensor data
> > + * @dev: Device pointer
> > + * @type: Sensor type
> > + * @attr: Attribute identifier
> > + * @channel: Channel index
> > + * @val: Pointer to store value
> > + *
> > + * Reads current sensor values from hardware through WMI interface.
> > + *
> > + * Return: 0 on success, or an error code.
> > + */
> > +static int lwmi_om_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
> > +			      u32 attr, int channel, long *val)
> > +{
> > +	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> > +	u32 retval = 0;
> > +	int err;
> > +
> > +	if (type == hwmon_fan) {
> > +		switch (attr) {
> > +		case hwmon_fan_input:
> > +			err = lwmi_om_fan_get_set(priv, channel, &retval, false);
> > +			if (err)
> > +				return err;
> > +
> > +			*val = retval;
> > +			return 0;
> > +		case hwmon_fan_enable:
> > +			*val = priv->fan_info[channel].last_target != LWMI_FAN_STOP_RPM;
> > +			return 0;
> > +		case hwmon_fan_target:
> > +			if (priv->fan_info[channel].last_target == LWMI_FAN_STOP_RPM)
> > +				return -EBUSY;
> > +
> > +			*val = priv->fan_info[channel].last_target;
> > +			return 0;
> > +		case hwmon_fan_min:
> > +			*val = priv->fan_info[channel].min_rpm;
> > +			return 0;
> > +		case hwmon_fan_max:
> > +			*val = priv->fan_info[channel].max_rpm;
> > +			return 0;
> > +		}
> > +	}
> > +
> > +	return -EOPNOTSUPP;
> > +}
> > +
> > +/**
> > + * lwmi_om_hwmon_write() - Write HWMON sensor data
> > + * @dev: Device pointer
> > + * @type: Sensor type
> > + * @attr: Attribute identifier
> > + * @channel: Channel index
> > + * @val: Value to write
> > + *
> > + * Writes configuration values to hardware through WMI interface.
> > + *
> > + * Return: 0 on success, or an error code.
> > + */
> > +static int lwmi_om_hwmon_write(struct device *dev, enum hwmon_sensor_types type,
> > +			       u32 attr, int channel, long val)
> > +{
> > +	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> > +	u32 raw, min_rpm, max_rpm;
> > +	int err;
> > +
> > +	if (type == hwmon_fan) {
> > +		switch (attr) {
> > +		case hwmon_fan_enable:
> > +			if (val == 0)
> > +				raw = LWMI_FAN_STOP_RPM;
> > +			else if (val == 1)
> > +				raw = 0; /* auto */
> > +			else
> > +				return -EINVAL;
> > +
> > +			goto fan_set;
> > +		case hwmon_fan_target:
> > +			if (val == 0) {
> > +				raw = 0;
> > +				goto fan_set;
> > +			}
> > +
> > +			min_rpm = relax_fan_constraint
> > +					? LWMI_FAN_STOP_RPM + 1
> > +					: priv->fan_info[channel].min_rpm;
> > +			max_rpm = relax_fan_constraint
> > +					? U16_MAX
> > +					: priv->fan_info[channel].max_rpm;
> > +
> > +			if (val < min_rpm || val > max_rpm)
> > +				return -EDOM;
> > +
> > +			raw = val;
> > +fan_set:
> > +			err = lwmi_om_fan_get_set(priv, channel, &raw, true);
> > +			if (err)
> > +				return err;
> > +
> > +			priv->fan_info[channel].last_target = raw;
> > +			return 0;
> > +		}
> > +	}
> > +
> > +	return -EOPNOTSUPP;
> > +}
> > +
> > +static const struct hwmon_channel_info * const lwmi_om_hwmon_info[] = {
> > +	/* Must match LWMI_FAN_NR. */
> > +	HWMON_CHANNEL_INFO(fan,
> > +			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
> > +			   HWMON_F_MIN | HWMON_F_MAX,
> > +			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
> > +			   HWMON_F_MIN | HWMON_F_MAX,
> > +			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
> > +			   HWMON_F_MIN | HWMON_F_MAX,
> > +			   HWMON_F_ENABLE | HWMON_F_INPUT | HWMON_F_TARGET |
> > +			   HWMON_F_MIN | HWMON_F_MAX),
> > +	NULL
> >  };
> >  
> > +static const struct hwmon_ops lwmi_om_hwmon_ops = {
> > +	.is_visible = lwmi_om_hwmon_is_visible,
> > +	.read = lwmi_om_hwmon_read,
> > +	.write = lwmi_om_hwmon_write,
> > +};
> > +
> > +static const struct hwmon_chip_info lwmi_om_hwmon_chip_info = {
> > +	.ops = &lwmi_om_hwmon_ops,
> > +	.info = lwmi_om_hwmon_info,
> > +};
> > +
> > +/**
> > + * lwmi_om_hwmon_add() - Register HWMON device if all info is collected
> > + * @priv: Driver private data
> > + */
> > +static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv)
> > +{
> > +	int i, valid;
> > +
> > +	if (WARN_ON(priv->hwmon_dev))
> > +		return;
> > +
> > +	if (!priv->fan_flags.capdata00_collected || !priv->fan_flags.capdata_fan_collected) {
> > +		dev_dbg(&priv->wdev->dev, "HWMON registration pending (00: %d, fan: %d)\n",
> > +			priv->fan_flags.capdata00_collected,
> > +			priv->fan_flags.capdata_fan_collected);
> > +		return;
> > +	}
> > +
> > +	if (expose_all_fans)
> > +		dev_warn(&priv->wdev->dev, "all fans exposed. Use with caution\n");
> > +
> > +	if (relax_fan_constraint)
> > +		dev_warn(&priv->wdev->dev, "fan RPM constraint relaxed. Use with caution\n");
> > +
> > +	valid = 0;
> > +	for (i = 0; i < LWMI_FAN_NR; i++) {
> > +		if (!(priv->fan_info[i].supported & LWMI_SUPP_VALID))
> > +			continue;
> > +
> > +		valid++;
> > +
> > +		if (!expose_all_fans &&
> > +		    (priv->fan_info[i].min_rpm < 0 || priv->fan_info[i].max_rpm < 0)) {
> > +			dev_dbg(&priv->wdev->dev, "missing RPM constraint for fan%d, hiding\n",
> > +				LWMI_FAN_ID(i));
> > +			priv->fan_info[i].supported = 0;
> > +			valid--;
> > +		}
> > +	}
> > +
> > +	if (valid == 0) {
> > +		dev_warn(&priv->wdev->dev,
> > +			 "fan reporting/tuning is unsupported on this device\n");
> > +		return;
> > +	}
> > +
> > +	priv->hwmon_dev = hwmon_device_register_with_info(&priv->wdev->dev,
> > +							  LWMI_OM_HWMON_NAME, priv,
> > +							  &lwmi_om_hwmon_chip_info,
> > +							  NULL);
> > +	if (IS_ERR(priv->hwmon_dev)) {
> > +		dev_warn(&priv->wdev->dev, "failed to register HWMON device: %ld\n",
> > +			 PTR_ERR(priv->hwmon_dev));
> > +		priv->hwmon_dev = NULL;
> > +		return;
> > +	}
> > +
> > +	dev_dbg(&priv->wdev->dev, "registered HWMON device\n");
> > +}
> > +
> > +/**
> > + * lwmi_om_hwmon_remove() - Unregister HWMON device
> > + * @priv: Driver private data
> > + *
> > + * Unregisters the HWMON device if applicable.
> > + */
> > +static void lwmi_om_hwmon_remove(struct lwmi_om_priv *priv)
> > +{
> > +	if (!priv->hwmon_dev)
> > +		return;
> > +
> > +	hwmon_device_unregister(priv->hwmon_dev);
> > +	priv->hwmon_dev = NULL;
> > +}
> > +
> > +/**
> > + * lwmi_om_fan_info_init() - Initialzie fan info
> > + * @priv: Driver private data
> > + *
> > + * lwmi_om_fan_info_collect_cd00() and lwmi_om_fan_info_collect_cd_fan() may be
> > + * called in an arbitrary order. Hence, initializion must be done before.
> > + */
> > +static void lwmi_om_fan_info_init(struct lwmi_om_priv *priv)
> > +{
> > +	int i;
> > +
> > +	for (i = 0; i < LWMI_FAN_NR; i++) {
> > +		priv->fan_info[i] = (struct fan_info) {
> > +			.supported = 0,
> > +			/*
> > +			 * Assume 0 on probe as the EC resets all fans to auto mode on (re)boot.
> > +			 *
> > +			 * Note that S0ix (s2idle) preserves the RPM target, so we don't need
> > +			 * suspend/resume callbacks. This behavior has not been tested on S3-
> > +			 * capable devices, but I doubt if such devices even have this interface.
> > +			 */
> > +			.last_target = 0,
> > +			.min_rpm = -ENODATA,
> > +			.max_rpm = -ENODATA,
> > +		};
> > +	}
> > +
> > +	priv->fan_flags.capdata00_collected = false;
> > +	priv->fan_flags.capdata_fan_collected = false;
> > +}
> > +
> > +/**
> > + * lwmi_om_fan_info_collect_cd00() - Collect fan info from capdata 00
> > + * @priv: Driver private data
> > + */
> > +static void lwmi_om_fan_info_collect_cd00(struct lwmi_om_priv *priv)
> > +{
> > +	struct capdata00 capdata00;
> > +	int i, err;
> > +
> > +	dev_dbg(&priv->wdev->dev, "Collecting fan info from capdata00\n");
> > +
> > +	for (i = 0; i < LWMI_FAN_NR; i++) {
> > +		err = lwmi_cd00_get_data(priv->cd00_list, LWMI_ATTR_ID_FAN_RPM(i), &capdata00);
> > +		priv->fan_info[i].supported = err ? 0 : capdata00.supported;
> > +	}
> > +
> > +	priv->fan_flags.capdata00_collected = true;
> > +	lwmi_om_hwmon_add(priv);
> > +}
> > +
> > +/**
> > + * lwmi_om_fan_info_collect_cd_fan() - Collect fan info from capdata fan
> > + * @dev: Pointer to the lenovo-wmi-other device
> > + * @cd_fan_list: Pointer to the capdata fan list
> > + */
> > +static void lwmi_om_fan_info_collect_cd_fan(struct device *dev, struct cd_list *cd_fan_list)
> > +{
> > +	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> > +	struct capdata_fan capdata_fan;
> > +	int i, err;
> > +
> > +	dev_dbg(dev, "Collecting fan info from capdata_fan\n");
> > +	if (!cd_fan_list)
> > +		goto out;
> > +
> > +	for (i = 0; i < LWMI_FAN_NR; i++) {
> > +		err = lwmi_cd_fan_get_data(cd_fan_list, LWMI_FAN_ID(i), &capdata_fan);
> > +		if (err)
> > +			continue;
> > +
> > +		priv->fan_info[i].min_rpm = capdata_fan.min_rpm;
> > +		priv->fan_info[i].max_rpm = capdata_fan.max_rpm;
> > +	}
> 
> Add an empty line.
> 
> > +out:
> > +	priv->fan_flags.capdata_fan_collected = true;
> > +	lwmi_om_hwmon_add(priv);
> > +}
> > +
> > +/* ======== fw_attributes (component: lenovo-wmi-capdata 01) ======== */
> > +
> >  struct tunable_attr_01 {
> >  	struct capdata01 *capdata;
> >  	struct device *dev;
> > @@ -559,32 +1007,45 @@ static void lwmi_om_fw_attr_remove(struct lwmi_om_priv *priv)
> >  	device_unregister(priv->fw_attr_dev);
> >  }
> >  
> > +/* ======== Self (master: lenovo-wmi-other) ======== */
> > +
> >  /**
> >   * lwmi_om_master_bind() - Bind all components of the other mode driver
> >   * @dev: The lenovo-wmi-other driver basic device.
> >   *
> > - * Call component_bind_all to bind the lenovo-wmi-capdata01 driver to the
> > - * lenovo-wmi-other master driver. On success, assign the capability data 01
> > - * list pointer to the driver data struct for later access. This pointer
> > - * is only valid while the capdata01 interface exists. Finally, register all
> > - * firmware attribute groups.
> > + * Call component_bind_all to bind the lenovo-wmi-capdata devices to the
> > + * lenovo-wmi-other master driver, with a callback to collect fan info from
> > + * capdata_fan. On success, assign the capability data list pointers to the
> > + * driver data struct for later access. These pointers are only valid while the
> > + * capdata interfaces exist. Finally, collect fan info from capdata00 and
> > + * register all firmware attribute groups. Note that the HWMON device is
> > + * registered only if all fan info is collected. Hence, it is not registered
> > + * here. See lwmi_om_fan_info_collect_cd00() and
> > + * lwmi_om_fan_info_collect_cd_fan().
> >   *
> >   * Return: 0 on success, or an error code.
> >   */
> >  static int lwmi_om_master_bind(struct device *dev)
> >  {
> >  	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> > -	struct lwmi_cd_binder binder = { 0 };
> > +	struct lwmi_cd_binder binder = {
> > +		.cd_fan_list_cb = lwmi_om_fan_info_collect_cd_fan,
> > +	};
> >  	int ret;
> >  
> > +	lwmi_om_fan_info_init(priv);
> > +
> >  	ret = component_bind_all(dev, &binder);
> >  	if (ret)
> >  		return ret;
> >  
> > +	priv->cd00_list = binder.cd00_list;
> >  	priv->cd01_list = binder.cd01_list;
> > -	if (!priv->cd01_list)
> > +	if (!priv->cd00_list || !priv->cd01_list)
> >  		return -ENODEV;
> >  
> > +	lwmi_om_fan_info_collect_cd00(priv);
> > +
> >  	return lwmi_om_fw_attr_add(priv);
> >  }
> >  
> > @@ -592,15 +1053,18 @@ static int lwmi_om_master_bind(struct device *dev)
> >   * lwmi_om_master_unbind() - Unbind all components of the other mode driver
> >   * @dev: The lenovo-wmi-other driver basic device
> >   *
> > - * Unregister all capability data attribute groups. Then call
> > - * component_unbind_all to unbind the lenovo-wmi-capdata01 driver from the
> > - * lenovo-wmi-other master driver. Finally, free the IDA for this device.
> > + * Unregister all firmware attribute groups and the HWMON device. Then call
> > + * component_unbind_all to unbind lenovo-wmi-capdata devices from the
> > + * lenovo-wmi-other master driver.
> >   */
> >  static void lwmi_om_master_unbind(struct device *dev)
> >  {
> >  	struct lwmi_om_priv *priv = dev_get_drvdata(dev);
> >  
> >  	lwmi_om_fw_attr_remove(priv);
> > +
> > +	lwmi_om_hwmon_remove(priv);
> > +
> >  	component_unbind_all(dev, NULL);
> >  }
> >  
> > @@ -665,5 +1129,6 @@ MODULE_IMPORT_NS("LENOVO_WMI_CD");
> >  MODULE_IMPORT_NS("LENOVO_WMI_HELPERS");
> >  MODULE_DEVICE_TABLE(wmi, lwmi_other_id_table);
> >  MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
> > +MODULE_AUTHOR("Rong Zhang <i@rong.moe>");
> >  MODULE_DESCRIPTION("Lenovo Other Mode WMI Driver");
> >  MODULE_LICENSE("GPL");
> > 

^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up Fan Test Data
  2025-11-23 15:02     ` Rong Zhang
@ 2025-11-25  2:08       ` Armin Wolf
  0 siblings, 0 replies; 22+ messages in thread
From: Armin Wolf @ 2025-11-25  2:08 UTC (permalink / raw)
  To: Rong Zhang, Mark Pearson, Derek J. Clark, Hans de Goede,
	Ilpo Järvinen
  Cc: Guenter Roeck, platform-driver-x86, linux-kernel, linux-hwmon

Am 23.11.25 um 16:02 schrieb Rong Zhang:

> Hi Armin,
>
> On Sun, 2025-11-23 at 04:36 +0100, Armin Wolf wrote:
>> Am 22.11.25 um 19:44 schrieb Rong Zhang:
>>
>>> A capdata00 attribute (0x04050000) describes the presence of Fan Test
>>> Data. Query it, and bind Fan Test Data as a component of capdata00
>>> accordingly. The component master of capdata00 may pass a callback while
>>> binding to retrieve fan info from Fan Test Data.
>>>
>>> Summarizing this scheme:
>>>
>>> 	lenovo-wmi-other <-> capdata00 <-> capdata_fan
>>> 	|- master            |- component
>>> 	                     |- sub-master
>>> 	                                   |- sub-component
>>>
>>> The callback will be called once both the master and the sub-component
>>> are bound to the sub-master (component).
>>>
>>> This scheme is essential to solve these issues:
>>> - The component framework only supports one aggregation per master
>>> - A binding is only established until all components are found
>>> - The Fan Test Data interface may be missing on some devices
>>> - To get rid of queries for the presense of WMI GUIDs
>>>
>>> capdata00 is registered as a component and a sub-master on probe,
>>> instead of chaining the registerations in one's bind callback. This is
>>> because calling (un)registration methods of the component framework
>>> causes deadlock in (un)bind callbacks, i.e., it's impossible to register
>>> capdata00 as a sub-master/component in its component/sub-master bind
>>> callback, and vice versa.
>> I spend some time trying to understand all of this, and i have come to the conclusion
>> that while passing a callback between both component masters, it still is not exactly
>> clean.
> Could you elaborate? Thanks.
>
>> Can you instead use devm_lwmi_om_register_notifier() and define another event for querying
>> the fan data? This way we could ditch the component submaster with all of the callback madness.
> Thanks for your review and suggestion.
>
> At first I thought the notifier approach was nice, however...
>
> There are two directions to use the notifier framework:
> (1) lenovo-wmi-other calls blocking_notifier_call_chain() to query
>      capdata_fan
> (2) capdata_fan calls blocking_notifier_call_chain() to provide data
>      to lenovo-wmi-other
>
> Both requires some assumptions on probing/binding sequence:
> (1) capdata_fan must be probed before lenovo-wmi-other calling
>      blocking_notifier_call_chain(), otherwise lenovo-wmi-other will
>      mistakenly believe capdata_fan is missing
> (2) lenovo-wmi-other must be probed before capdata_fan calling
>      blocking_notifier_call_chain(), otherwise lenovo-wmi-other will
>      wait for fan info forever
>
> So eventually we need *both directions* to break the assumptions on
> probing/binding sequence. Moreover, some extra synchronization
> primitives may be required to make sure the binding between lenovo-wmi-
> other and capdata00&01 won't be unbind while registering the HWMON
> device.
>
>> Sorry for being the one for suggesting the submaster approach in the first place, i did not
>> know that the component framework is so limited when it comes to nested component masters :/
> I'd say the sub-master approach is still the least ugly one.
>
> The callback is always called in a bind callback. Hence, the component
> framework protects our sanity, so we don't need to worry about the
> other side of our component chain being messed up while handling one
> side. The approach doesn't need any assumption on binding sequence as
> the corresponding bind/unbind callbacks effectively create a state
> machine.

Fine, if you prefer the submaster approach, then ignore my suggestion. As long as
you do not use wmi_has_guid(), i am happy.

Thanks,
Armin Wolf

>> If Mark is OK with using the submaster approach then you can ignore the above suggestion.
>>
>> Thanks,
>> Armin Wolf
> Thanks,
> Rong
>
>>> Signed-off-by: Rong Zhang <i@rong.moe>
>>> ---
>>> Changes in v6:
>>> - Fix the error path of component_add(capdata00)
>>> - Elaborate the design in commit message
>>>
>>> Changes in v5:
>>> - Fix missing include (thanks kernel test robot)
>>>
>>> Changes in v4:
>>> - New patch in the series (thanks Armin Wolf's inspiration)
>>>     - Get rid of wmi_has_guid() (see also [PATCH v4 3/7])
>>> ---
>>>    drivers/platform/x86/lenovo/wmi-capdata.c | 265 +++++++++++++++++++++-
>>>    drivers/platform/x86/lenovo/wmi-capdata.h |  20 ++
>>>    drivers/platform/x86/lenovo/wmi-other.c   |   5 -
>>>    3 files changed, 283 insertions(+), 7 deletions(-)
>>>
>>> diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
>>> index e6392357395c..8760f8c071ca 100644
>>> --- a/drivers/platform/x86/lenovo/wmi-capdata.c
>>> +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
>>> @@ -27,6 +27,7 @@
>>>    #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
>>>    
>>>    #include <linux/acpi.h>
>>> +#include <linux/bitfield.h>
>>>    #include <linux/cleanup.h>
>>>    #include <linux/component.h>
>>>    #include <linux/container_of.h>
>>> @@ -50,10 +51,17 @@
>>>    #define ACPI_AC_CLASS "ac_adapter"
>>>    #define ACPI_AC_NOTIFY_STATUS 0x80
>>>    
>>> +#define LWMI_FEATURE_ID_FAN_TEST 0x05
>>> +
>>> +#define LWMI_ATTR_ID_FAN_TEST							\
>>> +	(FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) |		\
>>> +	 FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_TEST))
>>> +
>>>    enum lwmi_cd_type {
>>>    	LENOVO_CAPABILITY_DATA_00,
>>>    	LENOVO_CAPABILITY_DATA_01,
>>>    	LENOVO_FAN_TEST_DATA,
>>> +	CD_TYPE_NONE = -1,
>>>    };
>>>    
>>>    #define LWMI_CD_TABLE_ITEM(_type)		\
>>> @@ -75,6 +83,20 @@ struct lwmi_cd_priv {
>>>    	struct notifier_block acpi_nb; /* ACPI events */
>>>    	struct wmi_device *wdev;
>>>    	struct cd_list *list;
>>> +
>>> +	/*
>>> +	 * A capdata device may be a component master of another capdata device.
>>> +	 * E.g., lenovo-wmi-other <-> capdata00 <-> capdata_fan
>>> +	 *       |- master            |- component
>>> +	 *                            |- sub-master
>>> +	 *                                          |- sub-component
>>> +	 */
>>> +	struct lwmi_cd_sub_master_priv {
>>> +		struct device *master_dev;
>>> +		cd_list_cb_t master_cb;
>>> +		struct cd_list *sub_component_list; /* ERR_PTR(-ENODEV) implies no sub-component. */
>>> +		bool registered;                    /* Has the sub-master been registered? */
>>> +	} *sub_master;
>>>    };
>>>    
>>>    struct cd_list {
>>> @@ -125,7 +147,7 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
>>>    		return;
>>>    
>>>    	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
>>> -		/* Skip optional interfaces. */
>>> +		/* Skip sub-components. */
>>>    		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
>>>    			continue;
>>>    
>>> @@ -137,6 +159,56 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
>>>    }
>>>    EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
>>>    
>>> +/**
>>> + * lwmi_cd_call_master_cb() - Call the master callback for the sub-component.
>>> + * @priv: Pointer to the capability data private data.
>>> + *
>>> + * Call the master callback and pass the sub-component list to it if the
>>> + * dependency chain (master <-> sub-master <-> sub-component) is complete.
>>> + */
>>> +static void lwmi_cd_call_master_cb(struct lwmi_cd_priv *priv)
>>> +{
>>> +	struct cd_list *sub_component_list = priv->sub_master->sub_component_list;
>>> +
>>> +	/*
>>> +	 * Call the callback only if the dependency chain is ready:
>>> +	 * - Binding between master and sub-master: fills master_dev and master_cb
>>> +	 * - Binding between sub-master and sub-component: fills sub_component_list
>>> +	 *
>>> +	 * If a binding has been unbound before the other binding is bound, the
>>> +	 * corresponding members filled by the former are guaranteed to be cleared.
>>> +	 *
>>> +	 * This function is only called in bind callbacks, and the component
>>> +	 * framework guarantees bind/unbind callbacks may never execute
>>> +	 * simultaneously, which implies that it's impossible to have a race
>>> +	 * condition.
>>> +	 *
>>> +	 * Hence, this check is sufficient to ensure that the callback is called
>>> +	 * at most once and with the correct state, without relying on a specific
>>> +	 * sequence of binding establishment.
>>> +	 */
>>> +	if (!sub_component_list ||
>>> +	    !priv->sub_master->master_dev ||
>>> +	    !priv->sub_master->master_cb)
>>> +		return;
>>> +
>>> +	if (PTR_ERR(sub_component_list) == -ENODEV)
>>> +		sub_component_list = NULL;
>>> +	else if (WARN_ON(IS_ERR(sub_component_list)))
>>> +		return;
>>> +
>>> +	priv->sub_master->master_cb(priv->sub_master->master_dev,
>>> +				    sub_component_list);
>>> +
>>> +	/*
>>> +	 * Prevent "unbind and rebind" sequences from userspace from calling the
>>> +	 * callback twice.
>>> +	 */
>>> +	priv->sub_master->master_cb = NULL;
>>> +	priv->sub_master->master_dev = NULL;
>>> +	priv->sub_master->sub_component_list = NULL;
>>> +}
>>> +
>>>    /**
>>>     * lwmi_cd_component_bind() - Bind component to master device.
>>>     * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
>>> @@ -147,6 +219,8 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd_match_add_all, "LENOVO_WMI_CD");
>>>     * list. This is used to call lwmi_cd*_get_data to look up attribute data
>>>     * from the lenovo-wmi-other driver.
>>>     *
>>> + * If cd_dev is a sub-master, try to call the master callback.
>>> + *
>>>     * Return: 0
>>>     */
>>>    static int lwmi_cd_component_bind(struct device *cd_dev,
>>> @@ -158,6 +232,11 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
>>>    	switch (priv->list->type) {
>>>    	case LENOVO_CAPABILITY_DATA_00:
>>>    		binder->cd00_list = priv->list;
>>> +
>>> +		priv->sub_master->master_dev = om_dev;
>>> +		priv->sub_master->master_cb = binder->cd_fan_list_cb;
>>> +		lwmi_cd_call_master_cb(priv);
>>> +
>>>    		break;
>>>    	case LENOVO_CAPABILITY_DATA_01:
>>>    		binder->cd01_list = priv->list;
>>> @@ -169,8 +248,167 @@ static int lwmi_cd_component_bind(struct device *cd_dev,
>>>    	return 0;
>>>    }
>>>    
>>> +/**
>>> + * lwmi_cd_component_unbind() - Unbind component to master device.
>>> + * @cd_dev: Pointer to the lenovo-wmi-capdata driver parent device.
>>> + * @om_dev: Pointer to the lenovo-wmi-other driver parent device.
>>> + * @data: Unused.
>>> + *
>>> + * If cd_dev is a sub-master, clear the collected data from the master device to
>>> + * prevent the binding establishment between the sub-master and the sub-
>>> + * component (if it's about to happen) from calling the master callback.
>>> + */
>>> +static void lwmi_cd_component_unbind(struct device *cd_dev,
>>> +				     struct device *om_dev, void *data)
>>> +{
>>> +	struct lwmi_cd_priv *priv = dev_get_drvdata(cd_dev);
>>> +
>>> +	switch (priv->list->type) {
>>> +	case LENOVO_CAPABILITY_DATA_00:
>>> +		priv->sub_master->master_dev = NULL;
>>> +		priv->sub_master->master_cb = NULL;
>>> +		return;
>>> +	default:
>>> +		return;
>>> +	}
>>> +}
>>> +
>>>    static const struct component_ops lwmi_cd_component_ops = {
>>>    	.bind = lwmi_cd_component_bind,
>>> +	.unbind = lwmi_cd_component_unbind,
>>> +};
>>> +
>>> +/**
>>> + * lwmi_cd_sub_master_bind() - Bind sub-component of sub-master device
>>> + * @dev: The sub-master capdata basic device.
>>> + *
>>> + * Call component_bind_all to bind the sub-component device to the sub-master
>>> + * device. On success, collect the pointer to the sub-component list and try
>>> + * to call the master callback.
>>> + *
>>> + * Return: 0 on success, or an error code.
>>> + */
>>> +static int lwmi_cd_sub_master_bind(struct device *dev)
>>> +{
>>> +	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
>>> +	struct cd_list *sub_component_list;
>>> +	int ret;
>>> +
>>> +	ret = component_bind_all(dev, &sub_component_list);
>>> +	if (ret)
>>> +		return ret;
>>> +
>>> +	priv->sub_master->sub_component_list = sub_component_list;
>>> +	lwmi_cd_call_master_cb(priv);
>>> +
>>> +	return 0;
>>> +}
>>> +
>>> +/**
>>> + * lwmi_cd_sub_master_unbind() - Unbind sub-component of sub-master device
>>> + * @dev: The sub-master capdata basic device
>>> + *
>>> + * Clear the collected pointer to the sub-component list to prevent the binding
>>> + * establishment between the sub-master and the sub-component (if it's about to
>>> + * happen) from calling the master callback. Then, call component_unbind_all to
>>> + * unbind the sub-component device from the sub-master device.
>>> + */
>>> +static void lwmi_cd_sub_master_unbind(struct device *dev)
>>> +{
>>> +	struct lwmi_cd_priv *priv = dev_get_drvdata(dev);
>>> +
>>> +	priv->sub_master->sub_component_list = NULL;
>>> +
>>> +	component_unbind_all(dev, NULL);
>>> +}
>>> +
>>> +static const struct component_master_ops lwmi_cd_sub_master_ops = {
>>> +	.bind = lwmi_cd_sub_master_bind,
>>> +	.unbind = lwmi_cd_sub_master_unbind,
>>> +};
>>> +
>>> +/**
>>> + * lwmi_cd_sub_master_add() - Register a sub-master with its sub-component
>>> + * @priv: Pointer to the sub-master capdata device private data.
>>> + * @sub_component_type: Type of the sub-component.
>>> + *
>>> + * Match the sub-component type and register the current capdata device as a
>>> + * sub-master. If the given sub-component type is CD_TYPE_NONE, mark the sub-
>>> + * component as non-existent without registering sub-master.
>>> + *
>>> + * Return: 0 on success, or an error code.
>>> + */
>>> +static int lwmi_cd_sub_master_add(struct lwmi_cd_priv *priv,
>>> +				  enum lwmi_cd_type sub_component_type)
>>> +{
>>> +	struct component_match *master_match = NULL;
>>> +	int ret;
>>> +
>>> +	priv->sub_master = devm_kzalloc(&priv->wdev->dev, sizeof(*priv->sub_master), GFP_KERNEL);
>>> +	if (!priv->sub_master)
>>> +		return -ENOMEM;
>>> +
>>> +	if (sub_component_type == CD_TYPE_NONE) {
>>> +		/* The master callback will be called with NULL on bind. */
>>> +		priv->sub_master->sub_component_list = ERR_PTR(-ENODEV);
>>> +		priv->sub_master->registered = false;
>>> +		return 0;
>>> +	}
>>> +
>>> +	/*
>>> +	 * lwmi_cd_match() needs a pointer to enum lwmi_cd_type, but on-stack
>>> +	 * data cannot be used here. Steal one from lwmi_cd_table.
>>> +	 */
>>> +	component_match_add(&priv->wdev->dev, &master_match, lwmi_cd_match,
>>> +			    (void *)&lwmi_cd_table[sub_component_type].type);
>>> +	if (IS_ERR(master_match))
>>> +		return PTR_ERR(master_match);
>>> +
>>> +	ret = component_master_add_with_match(&priv->wdev->dev, &lwmi_cd_sub_master_ops,
>>> +					      master_match);
>>> +	if (ret)
>>> +		return ret;
>>> +
>>> +	priv->sub_master->registered = true;
>>> +	return 0;
>>> +}
>>> +
>>> +/**
>>> + * lwmi_cd_sub_master_del() - Unregister a sub-master if it's registered
>>> + * @priv: Pointer to the sub-master capdata device private data.
>>> + */
>>> +static void lwmi_cd_sub_master_del(struct lwmi_cd_priv *priv)
>>> +{
>>> +	if (priv->sub_master->registered) {
>>> +		component_master_del(&priv->wdev->dev, &lwmi_cd_sub_master_ops);
>>> +		priv->sub_master->registered = false;
>>> +	}
>>> +}
>>> +
>>> +/**
>>> + * lwmi_cd_sub_component_bind() - Bind sub-component to sub-master device.
>>> + * @sc_dev: Pointer to the sub-component capdata parent device.
>>> + * @sm_dev: Pointer to the sub-master capdata parent device.
>>> + * @data: Pointer used to return the capability data list pointer.
>>> + *
>>> + * On sub-master's bind, provide a pointer to the local capdata list.
>>> + * This is used by the sub-master to call the master callback.
>>> + *
>>> + * Return: 0
>>> + */
>>> +static int lwmi_cd_sub_component_bind(struct device *sc_dev,
>>> +				      struct device *sm_dev, void *data)
>>> +{
>>> +	struct lwmi_cd_priv *priv = dev_get_drvdata(sc_dev);
>>> +	struct cd_list **listp = data;
>>> +
>>> +	*listp = priv->list;
>>> +
>>> +	return 0;
>>> +}
>>> +
>>> +static const struct component_ops lwmi_cd_sub_component_ops = {
>>> +	.bind = lwmi_cd_sub_component_bind,
>>>    };
>>>    
>>>    /**
>>> @@ -470,9 +708,28 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
>>>    		goto out;
>>>    
>>>    	switch (info->type) {
>>> -	case LENOVO_CAPABILITY_DATA_00:
>>> +	case LENOVO_CAPABILITY_DATA_00: {
>>> +		enum lwmi_cd_type sub_component_type = LENOVO_FAN_TEST_DATA;
>>> +		struct capdata00 capdata00;
>>> +
>>> +		ret = lwmi_cd00_get_data(priv->list, LWMI_ATTR_ID_FAN_TEST, &capdata00);
>>> +		if (ret || !(capdata00.supported & LWMI_SUPP_VALID)) {
>>> +			dev_dbg(&wdev->dev, "capdata00 declares no fan test support\n");
>>> +			sub_component_type = CD_TYPE_NONE;
>>> +		}
>>> +
>>> +		/* Sub-master (capdata00) <-> sub-component (capdata_fan) */
>>> +		ret = lwmi_cd_sub_master_add(priv, sub_component_type);
>>> +		if (ret)
>>> +			goto out;
>>> +
>>> +		/* Master (lenovo-wmi-other) <-> sub-master (capdata00) */
>>>    		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
>>> +		if (ret)
>>> +			lwmi_cd_sub_master_del(priv);
>>> +
>>>    		goto out;
>>> +	}
>>>    	case LENOVO_CAPABILITY_DATA_01:
>>>    		priv->acpi_nb.notifier_call = lwmi_cd01_notifier_call;
>>>    
>>> @@ -488,6 +745,7 @@ static int lwmi_cd_probe(struct wmi_device *wdev, const void *context)
>>>    		ret = component_add(&wdev->dev, &lwmi_cd_component_ops);
>>>    		goto out;
>>>    	case LENOVO_FAN_TEST_DATA:
>>> +		ret = component_add(&wdev->dev, &lwmi_cd_sub_component_ops);
>>>    		goto out;
>>>    	default:
>>>    		return -EINVAL;
>>> @@ -509,10 +767,13 @@ static void lwmi_cd_remove(struct wmi_device *wdev)
>>>    
>>>    	switch (priv->list->type) {
>>>    	case LENOVO_CAPABILITY_DATA_00:
>>> +		lwmi_cd_sub_master_del(priv);
>>> +		fallthrough;
>>>    	case LENOVO_CAPABILITY_DATA_01:
>>>    		component_del(&wdev->dev, &lwmi_cd_component_ops);
>>>    		break;
>>>    	case LENOVO_FAN_TEST_DATA:
>>> +		component_del(&wdev->dev, &lwmi_cd_sub_component_ops);
>>>    		break;
>>>    	default:
>>>    		WARN_ON(1);
>>> diff --git a/drivers/platform/x86/lenovo/wmi-capdata.h b/drivers/platform/x86/lenovo/wmi-capdata.h
>>> index 38af4c4e4ef4..59ca3b3e5760 100644
>>> --- a/drivers/platform/x86/lenovo/wmi-capdata.h
>>> +++ b/drivers/platform/x86/lenovo/wmi-capdata.h
>>> @@ -5,8 +5,20 @@
>>>    #ifndef _LENOVO_WMI_CAPDATA_H_
>>>    #define _LENOVO_WMI_CAPDATA_H_
>>>    
>>> +#include <linux/bits.h>
>>>    #include <linux/types.h>
>>>    
>>> +#define LWMI_SUPP_VALID		BIT(0)
>>> +#define LWMI_SUPP_MAY_GET	(LWMI_SUPP_VALID | BIT(1))
>>> +#define LWMI_SUPP_MAY_SET	(LWMI_SUPP_VALID | BIT(2))
>>> +
>>> +#define LWMI_ATTR_DEV_ID_MASK	GENMASK(31, 24)
>>> +#define LWMI_ATTR_FEAT_ID_MASK	GENMASK(23, 16)
>>> +#define LWMI_ATTR_MODE_ID_MASK	GENMASK(15, 8)
>>> +#define LWMI_ATTR_TYPE_ID_MASK	GENMASK(7, 0)
>>> +
>>> +#define LWMI_DEVICE_ID_FAN	0x04
>>> +
>>>    struct component_match;
>>>    struct device;
>>>    struct cd_list;
>>> @@ -32,9 +44,17 @@ struct capdata_fan {
>>>    	u32 max_rpm;
>>>    };
>>>    
>>> +typedef void (*cd_list_cb_t)(struct device *master_dev, struct cd_list *cd_list);
>>> +
>>>    struct lwmi_cd_binder {
>>>    	struct cd_list *cd00_list;
>>>    	struct cd_list *cd01_list;
>>> +	/*
>>> +	 * May be called during or after the bind callback.
>>> +	 * Will be called with NULL if capdata_fan does not exist.
>>> +	 * The pointer is only valid in the callback; never keep it for later use!
>>> +	 */
>>> +	cd_list_cb_t cd_fan_list_cb;
>>>    };
>>>    
>>>    void lwmi_cd_match_add_all(struct device *master, struct component_match **matchptr);
>>> diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
>>> index f2e1e34d58a9..b3adcc2804fa 100644
>>> --- a/drivers/platform/x86/lenovo/wmi-other.c
>>> +++ b/drivers/platform/x86/lenovo/wmi-other.c
>>> @@ -54,11 +54,6 @@
>>>    #define LWMI_FEATURE_VALUE_GET 17
>>>    #define LWMI_FEATURE_VALUE_SET 18
>>>    
>>> -#define LWMI_ATTR_DEV_ID_MASK GENMASK(31, 24)
>>> -#define LWMI_ATTR_FEAT_ID_MASK GENMASK(23, 16)
>>> -#define LWMI_ATTR_MODE_ID_MASK GENMASK(15, 8)
>>> -#define LWMI_ATTR_TYPE_ID_MASK GENMASK(7, 0)
>>> -
>>>    #define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other"
>>>    
>>>    static BLOCKING_NOTIFIER_HEAD(om_chain_head);

^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 5/7] platform/x86: lenovo-wmi-capdata: Add support for Fan Test Data
  2025-11-24 20:07     ` Rong Zhang
@ 2025-11-26  7:44       ` Ilpo Järvinen
  2025-11-26 13:57         ` Rong Zhang
  0 siblings, 1 reply; 22+ messages in thread
From: Ilpo Järvinen @ 2025-11-26  7:44 UTC (permalink / raw)
  To: Rong Zhang
  Cc: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Guenter Roeck, platform-driver-x86, LKML, linux-hwmon

[-- Attachment #1: Type: text/plain, Size: 9039 bytes --]

On Tue, 25 Nov 2025, Rong Zhang wrote:
> On Mon, 2025-11-24 at 18:45 +0200, Ilpo Järvinen wrote:
> > On Sun, 23 Nov 2025, Rong Zhang wrote:
> > 
> > > Add support for LENOVO_FAN_TEST_DATA WMI data block. Provides an
> > > interface for querying the min/max fan speed RPM (reference data) of a
> > > given fan ID.
> > > 
> > > This interface is optional. Hence, it does not bind to lenovo-wmi-other
> > > and is not registered as a component for the moment. Appropriate binding
> > > will be implemented in the subsequent patch.
> > > 
> > > Signed-off-by: Rong Zhang <i@rong.moe>
> > > Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
> > > Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
> > > ---
> > > Changes in v4:
> > > - Rebase on top of changes made to [PATCH v4 3/7]
> > > - Do not register it as a component until [PATCH v4 6/7]
> > > 
> > > Changes in v2:
> > > - Reword documentation
> > > ---
> > >  .../wmi/devices/lenovo-wmi-other.rst          |  17 +++
> > >  drivers/platform/x86/lenovo/wmi-capdata.c     | 102 ++++++++++++++++++
> > >  drivers/platform/x86/lenovo/wmi-capdata.h     |   7 ++
> > >  3 files changed, 126 insertions(+)
> > > 
> > > diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
> > > index fcad595d49af..821282e07d93 100644
> > > --- a/Documentation/wmi/devices/lenovo-wmi-other.rst
> > > +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
> > > @@ -62,6 +62,13 @@ The following firmware-attributes are implemented:
> > >   - ppt_pl2_sppt: Platform Profile Tracking Slow Package Power Tracking
> > >   - ppt_pl3_fppt: Platform Profile Tracking Fast Package Power Tracking
> > >  
> > > +LENOVO_FAN_TEST_DATA
> > > +-------------------------
> > > +
> > > +WMI GUID ``B642801B-3D21-45DE-90AE-6E86F164FB21``
> > > +
> > > +The LENOVO_FAN_TEST_DATA interface provides reference data for self-test of
> > > +cooling fans.
> > >  
> > >  WMI interface description
> > >  =========================
> > > @@ -115,3 +122,13 @@ data using the `bmfdec <https://github.com/pali/bmfdec>`_ utility:
> > >      [WmiDataId(3), read, Description("Data Size.")] uint32 DataSize;
> > >      [WmiDataId(4), read, Description("Default Value"), WmiSizeIs("DataSize")] uint8 DefaultValue[];
> > >    };
> > > +
> > > +  [WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"), Description("Definition of Fan Test Data"), guid("{B642801B-3D21-45DE-90AE-6E86F164FB21}")]
> > > +  class LENOVO_FAN_TEST_DATA {
> > > +    [key, read] string InstanceName;
> > > +    [read] boolean Active;
> > > +    [WmiDataId(1), read, Description("Mode.")] uint32 NumOfFans;
> > > +    [WmiDataId(2), read, Description("Fan ID."), WmiSizeIs("NumOfFans")] uint32 FanId[];
> > > +    [WmiDataId(3), read, Description("Maximum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMaxSpeed[];
> > > +    [WmiDataId(4), read, Description("Minumum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMinSpeed[];
> > > +  };
> > > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
> > > index 29267c373ab3..e6392357395c 100644
> > > --- a/drivers/platform/x86/lenovo/wmi-capdata.c
> > > +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
> > > @@ -13,6 +13,10 @@
> > >   * attribute has multiple pages, one for each of the thermal modes managed by
> > >   * the Gamezone interface.
> > >   *
> > > + * Fan Test Data includes the max/min fan speed RPM for each fan. This is
> > > + * reference data for self-test. If the fan is in good condition, it is capable
> > > + * to spin faster than max RPM or slower than min RPM.
> > > + *
> > >   * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
> > >   *   - Initial implementation (formerly named lenovo-wmi-capdata01)
> > >   *
> > > @@ -41,6 +45,7 @@
> > >  
> > >  #define LENOVO_CAPABILITY_DATA_00_GUID "362A3AFE-3D96-4665-8530-96DAD5BB300E"
> > >  #define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154"
> > > +#define LENOVO_FAN_TEST_DATA_GUID "B642801B-3D21-45DE-90AE-6E86F164FB21"
> > >  
> > >  #define ACPI_AC_CLASS "ac_adapter"
> > >  #define ACPI_AC_NOTIFY_STATUS 0x80
> > > @@ -48,6 +53,7 @@
> > >  enum lwmi_cd_type {
> > >  	LENOVO_CAPABILITY_DATA_00,
> > >  	LENOVO_CAPABILITY_DATA_01,
> > > +	LENOVO_FAN_TEST_DATA,
> > >  };
> > >  
> > >  #define LWMI_CD_TABLE_ITEM(_type)		\
> > > @@ -62,6 +68,7 @@ static const struct lwmi_cd_info {
> > >  } lwmi_cd_table[] = {
> > >  	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_00),
> > >  	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
> > > +	LWMI_CD_TABLE_ITEM(LENOVO_FAN_TEST_DATA),
> > >  };
> > >  
> > >  struct lwmi_cd_priv {
> > > @@ -78,6 +85,7 @@ struct cd_list {
> > >  	union {
> > >  		DECLARE_FLEX_ARRAY(struct capdata00, cd00);
> > >  		DECLARE_FLEX_ARRAY(struct capdata01, cd01);
> > > +		DECLARE_FLEX_ARRAY(struct capdata_fan, cd_fan);
> > >  	};
> > >  };
> > >  
> > > @@ -117,6 +125,10 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
> > >  		return;
> > >  
> > >  	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
> > > +		/* Skip optional interfaces. */
> > > +		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
> > > +			continue;
> > > +
> > >  		component_match_add(master, matchptr, lwmi_cd_match,
> > >  				    (void *)&lwmi_cd_table[i].type);
> > >  		if (IS_ERR(*matchptr))
> > > @@ -194,6 +206,9 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd00_get_data, "LENOVO_WMI_CD");
> > >  DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
> > >  EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
> > >  
> > > +DEF_LWMI_CDXX_GET_DATA(cd_fan, LENOVO_FAN_TEST_DATA, struct capdata_fan);
> > > +EXPORT_SYMBOL_NS_GPL(lwmi_cd_fan_get_data, "LENOVO_WMI_CD");
> > > +
> > >  /**
> > >   * lwmi_cd_cache() - Cache all WMI data block information
> > >   * @priv: lenovo-wmi-capdata driver data.
> > > @@ -217,6 +232,9 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
> > >  		p = &priv->list->cd01[0];
> > >  		size = sizeof(priv->list->cd01[0]);
> > >  		break;
> > > +	case LENOVO_FAN_TEST_DATA:
> > > +		/* Done by lwmi_cd_alloc() => lwmi_cd_fan_list_alloc_cache(). */
> > > +		return 0;
> > >  	default:
> > >  		return -EINVAL;
> > >  	}
> > > @@ -239,6 +257,78 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
> > >  	return 0;
> > >  }
> > >  
> > > +/**
> > > + * lwmi_cd_fan_list_alloc_cache() - Alloc and cache Fan Test Data list
> > > + * @priv: lenovo-wmi-capdata driver data.
> > > + * @listptr: Pointer to returned cd_list pointer.
> > > + *
> > > + * Return: count of fans found, or an error.
> > > + */
> > > +static int lwmi_cd_fan_list_alloc_cache(struct lwmi_cd_priv *priv, struct cd_list **listptr)
> > > +{
> > > +	u32 count, *fan_ids, *fan_min_rpms, *fan_max_rpms;
> > > +	union acpi_object *ret_obj __free(kfree) = NULL;
> > 
> > Since you're using __free(), please move this to where you assign the 
> > value. This is to create a pattern with cleanup helpers. The cleanup 
> > order depends on the order the variables are introduced which in some 
> > other cases may be significant.
> 
> Make sense. Will move it to the last declaration lines with its
> assignment. Thanks.
> 
> > > +	struct block { u32 nr; u32 data[]; } *block;
> >
> > This is the first time I see this style anywhere in the kernel's context, 
> > has there been some general discussion about this style somewhere?
> > 
> > At least it seems immediately obvious to me that this style will have a 
> > negative impact on documentability due to (too) concise use of space.
> 
> Make sense. Will break it into multiple lines. Thanks.
> 
> > > +	struct cd_list *list;
> > > +	size_t size;
> > > +	int idx;
> > > +
> > > +	ret_obj = wmidev_block_query(priv->wdev, 0);
> > > +	if (!ret_obj)
> > > +		return -ENODEV;
> > > +
> > > +	/*
> > > +	 * This is usually caused by a dummy ACPI method. Do not return an error
> > > +	 * as failing to probe this device will result in master driver being
> > > +	 * unbound - this behavior aligns with lwmi_cd_cache().
> > > +	 */
> > > +	if (ret_obj->type != ACPI_TYPE_BUFFER) {
> > > +		count = 0;
> > > +		goto alloc;
> > > +	}
> > > +
> > > +	size = ret_obj->buffer.length;
> > > +	block = (struct block *)ret_obj->buffer.pointer;
> > 
> > void * can be cast implicitly.
> 
> `ret_obj->buffer.pointer' is a `u8 *' pointer so the cast is mandatory.

Ah, right. I think I even tried to check it but probably picked up
the void *pointer from struct acpi_buffer instead of the correct one.

> Hmmm, this reminds me that `struct block' probably needs a `__packed'
> to generate unaligned access to it.

In all u32 struct, I don't think __packed is required. You can use pahole 
tool to check the layout if you want, but I don't think compiler adds 
any gaps with only u32s.

-- 
 i.

^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 7/7] platform/x86: lenovo-wmi-other: Add HWMON for fan reporting/tuning
  2025-11-24 20:12     ` Rong Zhang
@ 2025-11-26  8:04       ` Ilpo Järvinen
  0 siblings, 0 replies; 22+ messages in thread
From: Ilpo Järvinen @ 2025-11-26  8:04 UTC (permalink / raw)
  To: Rong Zhang
  Cc: Mark Pearson, Derek J. Clark, Armin Wolf, Hans de Goede,
	Guenter Roeck, platform-driver-x86, LKML, linux-hwmon

[-- Attachment #1: Type: text/plain, Size: 7915 bytes --]

On Tue, 25 Nov 2025, Rong Zhang wrote:

> Hi Ilpo,
> 
> Thanks for your review.
> 
> On Mon, 2025-11-24 at 18:58 +0200, Ilpo Järvinen wrote:
> > On Sun, 23 Nov 2025, Rong Zhang wrote:
> > 
> > > Register an HWMON device for fan reporting/tuning according to
> > > Capability Data 00 (capdata00) and Fan Test Data (capdata_fan) provided
> > > by lenovo-wmi-capdata. The corresponding HWMON nodes are:
> > > 
> > >  - fanX_enable: enable/disable the fan (tunable)
> > >  - fanX_input: current RPM
> > >  - fanX_max: maximum RPM
> > >  - fanX_min: minimum RPM
> > >  - fanX_target: target RPM (tunable)
> > > 
> > > Information from capdata00 and capdata_fan are used to control the
> > > visibility and constraints of HWMON attributes. Fan info from capdata00
> > > is collected on bind, while fan info from capdata_fan is collected in a
> > > callback. Once all fan info is collected, register the HWMON device.
> > > 
> > > Signed-off-by: Rong Zhang <i@rong.moe>
> > > ---
> > > Changes in v4:
> > > - Rework HWMON registration due to the introduction of [PATCH v4 6/7]
> > >   - Collect fan info from capdata00 and capdata_fan separately
> > >   - Use a callback to collect fan info from capdata_fan
> > >   - Trigger HWMON registration only if all fan info is collected
> > >   - Do not check 0x04050000.supported, implied by the presense of
> > >     capdata_fan
> > > - Drop Reviewed-by & Tested-by due to the changes, please review & test
> > > 
> > > Changes in v3:
> > > - Reword documentation (thanks Derek J. Clark)
> > > 
> > > Changes in v2:
> > > - Define 4 fan channels instead of 2 (thanks Derek J. Clark)
> > > - Squash min/max reporting patch into this one (ditto)
> > > - Query 0x04050000 for interface availability (ditto)
> > >   - New parameter "expose_all_fans" to skip this check
> > > - Enforce min/max RPM constraint on set (ditto)
> > >   - New parameter "relax_fan_constraint" to disable this behavior
> > >   - Drop parameter "ignore_fan_cap", superseded by the next one
> > >   - New parameter "expose_all_fans" to expose fans w/o such data
> > > - Assume auto mode on probe (ditto)
> > > - Reword documentation (ditto)
> > > - Do not register HWMON device if no fan can be exposed
> > > - fanX_target: Return -EBUSY instead of raw target value when fan stops
> > > ---
> > >  .../wmi/devices/lenovo-wmi-other.rst          |  11 +
> > >  drivers/platform/x86/lenovo/Kconfig           |   1 +
> > >  drivers/platform/x86/lenovo/wmi-other.c       | 485 +++++++++++++++++-
> > >  3 files changed, 487 insertions(+), 10 deletions(-)
> > > 
> > > diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
> > > index 821282e07d93..bd1d733ff286 100644
> > > --- a/Documentation/wmi/devices/lenovo-wmi-other.rst
> > > +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
> > > @@ -31,6 +31,8 @@ under the following path:
> > >  
> > >    /sys/class/firmware-attributes/lenovo-wmi-other/attributes/<attribute>/
> > >  
> > > +Additionally, this driver also exports attributes to HWMON.
> > > +
> > >  LENOVO_CAPABILITY_DATA_00
> > >  -------------------------
> > >  
> > > @@ -39,6 +41,11 @@ WMI GUID ``362A3AFE-3D96-4665-8530-96DAD5BB300E``
> > >  The LENOVO_CAPABILITY_DATA_00 interface provides various information that
> > >  does not rely on the gamezone thermal mode.
> > >  
> > > +The following HWMON attributes are implemented:
> > > + - fanX_enable: enable/disable the fan (tunable)
> > > + - fanX_input: current RPM
> > > + - fanX_target: target RPM (tunable)
> > > +
> > >  LENOVO_CAPABILITY_DATA_01
> > >  -------------------------
> > >  
> > > @@ -70,6 +77,10 @@ WMI GUID ``B642801B-3D21-45DE-90AE-6E86F164FB21``
> > >  The LENOVO_FAN_TEST_DATA interface provides reference data for self-test of
> > >  cooling fans.
> > >  
> > > +The following HWMON attributes are implemented:
> > > + - fanX_max: maximum RPM
> > > + - fanX_min: minimum RPM
> > > +
> > >  WMI interface description
> > >  =========================
> > >  
> > > diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
> > > index fb96a0f908f0..be9af0451146 100644
> > > --- a/drivers/platform/x86/lenovo/Kconfig
> > > +++ b/drivers/platform/x86/lenovo/Kconfig
> > > @@ -263,6 +263,7 @@ config LENOVO_WMI_GAMEZONE
> > >  config LENOVO_WMI_TUNING
> > >  	tristate "Lenovo Other Mode WMI Driver"
> > >  	depends on ACPI_WMI
> > > +	select HWMON
> > >  	select FW_ATTR_CLASS
> > >  	select LENOVO_WMI_DATA
> > >  	select LENOVO_WMI_EVENTS
> > > diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> > > index b3adcc2804fa..ce1ca13db4b5 100644
> > > --- a/drivers/platform/x86/lenovo/wmi-other.c
> > > +++ b/drivers/platform/x86/lenovo/wmi-other.c
> > > @@ -14,7 +14,16 @@
> > >   * These attributes typically don't fit anywhere else in the sysfs and are set
> > >   * in Windows using one of Lenovo's multiple user applications.
> > >   *
> > > + * Additionally, this driver also exports tunable fan speed RPM to HWMON.
> > > + * Min/max RPM are also provided for reference.
> > > + *
> > >   * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
> > > + *   - fw_attributes
> > > + *   - binding to Capability Data 01
> > > + *
> > > + * Copyright (C) 2025 Rong Zhang <i@rong.moe>
> > > + *   - HWMON
> > > + *   - binding to Capability Data 00 and Fan
> > >   */
> > >  
> > >  #include <linux/acpi.h>
> > > @@ -25,6 +34,7 @@
> > >  #include <linux/device.h>
> > >  #include <linux/export.h>
> > >  #include <linux/gfp_types.h>
> > > +#include <linux/hwmon.h>
> > >  #include <linux/idr.h>
> > >  #include <linux/kdev_t.h>
> > >  #include <linux/kobject.h>
> > > @@ -49,12 +59,26 @@
> > >  #define LWMI_FEATURE_ID_CPU_SPL 0x02
> > >  #define LWMI_FEATURE_ID_CPU_FPPT 0x03
> > >  
> > > +#define LWMI_FEATURE_ID_FAN_RPM 0x03
> > > +
> > >  #define LWMI_TYPE_ID_NONE 0x00
> > >  
> > >  #define LWMI_FEATURE_VALUE_GET 17
> > >  #define LWMI_FEATURE_VALUE_SET 18
> > >  
> > > +#define LWMI_FAN_ID_BASE 1
> > > +#define LWMI_FAN_NR 4
> > > +#define LWMI_FAN_ID(x) ((x) + LWMI_FAN_ID_BASE)
> > > +
> > > +#define LWMI_ATTR_ID_FAN_RPM(x)						\
> > > +	(FIELD_PREP(LWMI_ATTR_DEV_ID_MASK, LWMI_DEVICE_ID_FAN) |	\
> > > +	 FIELD_PREP(LWMI_ATTR_FEAT_ID_MASK, LWMI_FEATURE_ID_FAN_RPM) |	\
> > > +	 FIELD_PREP(LWMI_ATTR_TYPE_ID_MASK, LWMI_FAN_ID(x)))
> > > +
> > > +#define LWMI_FAN_STOP_RPM 1
> > > +
> > >  #define LWMI_OM_FW_ATTR_BASE_PATH "lenovo-wmi-other"
> > > +#define LWMI_OM_HWMON_NAME "lenovo_wmi_other"
> > >  
> > >  static BLOCKING_NOTIFIER_HEAD(om_chain_head);
> > >  static DEFINE_IDA(lwmi_om_ida);
> > > @@ -71,15 +95,439 @@ struct lwmi_om_priv {
> > >  	struct component_master_ops *ops;
> > >  
> > >  	/* only valid after capdata bind */
> > > +	struct cd_list *cd00_list;
> > >  	struct cd_list *cd01_list;
> > >  
> > > +	struct device *hwmon_dev;
> > >  	struct device *fw_attr_dev;
> > >  	struct kset *fw_attr_kset;
> > >  	struct notifier_block nb;
> > >  	struct wmi_device *wdev;
> > >  	int ida_id;
> > > +
> > > +	struct fan_info {
> > > +		u32 supported;
> > > +		u32 last_target;
> > > +		long min_rpm;
> > > +		long max_rpm;
> > > +	} fan_info[LWMI_FAN_NR];
> > 
> > I personally don't like this style at all because it makes finding the 
> > type of the variable harder with grep.
> 
> Make sense. Will add a `lwmi_' prefix to the struct name. Thanks.

While having prefix is good too, I was more referring to naming it at the 
closing brace line, I'd prefer to have it on a separate line (which shows 
type, staticness easily with grep):

struct lwmi_fan_info fan_info[LWMI_FAN_NR];

I see you placed it into priv in v7 which is better anyway.

-- 
 i.

^ permalink raw reply	[flat|nested] 22+ messages in thread

* Re: [PATCH v6 5/7] platform/x86: lenovo-wmi-capdata: Add support for Fan Test Data
  2025-11-26  7:44       ` Ilpo Järvinen
@ 2025-11-26 13:57         ` Rong Zhang
  0 siblings, 0 replies; 22+ messages in thread
From: Rong Zhang @ 2025-11-26 13:57 UTC (permalink / raw)
  To: Ilpo Järvinen
  Cc: Rong Zhang, Mark Pearson, Derek J. Clark, Armin Wolf,
	Hans de Goede, Guenter Roeck, platform-driver-x86, LKML,
	linux-hwmon

Hi Ilpo,

On Wed, 2025-11-26 at 09:44 +0200, Ilpo Järvinen wrote:
> On Tue, 25 Nov 2025, Rong Zhang wrote:
> > On Mon, 2025-11-24 at 18:45 +0200, Ilpo Järvinen wrote:
> > > On Sun, 23 Nov 2025, Rong Zhang wrote:
> > > 
> > > > Add support for LENOVO_FAN_TEST_DATA WMI data block. Provides an
> > > > interface for querying the min/max fan speed RPM (reference data) of a
> > > > given fan ID.
> > > > 
> > > > This interface is optional. Hence, it does not bind to lenovo-wmi-other
> > > > and is not registered as a component for the moment. Appropriate binding
> > > > will be implemented in the subsequent patch.
> > > > 
> > > > Signed-off-by: Rong Zhang <i@rong.moe>
> > > > Reviewed-by: Derek J. Clark <derekjohn.clark@gmail.com>
> > > > Tested-by: Derek J. Clark <derekjohn.clark@gmail.com>
> > > > ---
> > > > Changes in v4:
> > > > - Rebase on top of changes made to [PATCH v4 3/7]
> > > > - Do not register it as a component until [PATCH v4 6/7]
> > > > 
> > > > Changes in v2:
> > > > - Reword documentation
> > > > ---
> > > >  .../wmi/devices/lenovo-wmi-other.rst          |  17 +++
> > > >  drivers/platform/x86/lenovo/wmi-capdata.c     | 102 ++++++++++++++++++
> > > >  drivers/platform/x86/lenovo/wmi-capdata.h     |   7 ++
> > > >  3 files changed, 126 insertions(+)
> > > > 
> > > > diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
> > > > index fcad595d49af..821282e07d93 100644
> > > > --- a/Documentation/wmi/devices/lenovo-wmi-other.rst
> > > > +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
> > > > @@ -62,6 +62,13 @@ The following firmware-attributes are implemented:
> > > >   - ppt_pl2_sppt: Platform Profile Tracking Slow Package Power Tracking
> > > >   - ppt_pl3_fppt: Platform Profile Tracking Fast Package Power Tracking
> > > >  
> > > > +LENOVO_FAN_TEST_DATA
> > > > +-------------------------
> > > > +
> > > > +WMI GUID ``B642801B-3D21-45DE-90AE-6E86F164FB21``
> > > > +
> > > > +The LENOVO_FAN_TEST_DATA interface provides reference data for self-test of
> > > > +cooling fans.
> > > >  
> > > >  WMI interface description
> > > >  =========================
> > > > @@ -115,3 +122,13 @@ data using the `bmfdec <https://github.com/pali/bmfdec>`_ utility:
> > > >      [WmiDataId(3), read, Description("Data Size.")] uint32 DataSize;
> > > >      [WmiDataId(4), read, Description("Default Value"), WmiSizeIs("DataSize")] uint8 DefaultValue[];
> > > >    };
> > > > +
> > > > +  [WMI, Dynamic, Provider("WmiProv"), Locale("MS\\0x409"), Description("Definition of Fan Test Data"), guid("{B642801B-3D21-45DE-90AE-6E86F164FB21}")]
> > > > +  class LENOVO_FAN_TEST_DATA {
> > > > +    [key, read] string InstanceName;
> > > > +    [read] boolean Active;
> > > > +    [WmiDataId(1), read, Description("Mode.")] uint32 NumOfFans;
> > > > +    [WmiDataId(2), read, Description("Fan ID."), WmiSizeIs("NumOfFans")] uint32 FanId[];
> > > > +    [WmiDataId(3), read, Description("Maximum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMaxSpeed[];
> > > > +    [WmiDataId(4), read, Description("Minumum Fan Speed."), WmiSizeIs("NumOfFans")] uint32 FanMinSpeed[];
> > > > +  };
> > > > diff --git a/drivers/platform/x86/lenovo/wmi-capdata.c b/drivers/platform/x86/lenovo/wmi-capdata.c
> > > > index 29267c373ab3..e6392357395c 100644
> > > > --- a/drivers/platform/x86/lenovo/wmi-capdata.c
> > > > +++ b/drivers/platform/x86/lenovo/wmi-capdata.c
> > > > @@ -13,6 +13,10 @@
> > > >   * attribute has multiple pages, one for each of the thermal modes managed by
> > > >   * the Gamezone interface.
> > > >   *
> > > > + * Fan Test Data includes the max/min fan speed RPM for each fan. This is
> > > > + * reference data for self-test. If the fan is in good condition, it is capable
> > > > + * to spin faster than max RPM or slower than min RPM.
> > > > + *
> > > >   * Copyright (C) 2025 Derek J. Clark <derekjohn.clark@gmail.com>
> > > >   *   - Initial implementation (formerly named lenovo-wmi-capdata01)
> > > >   *
> > > > @@ -41,6 +45,7 @@
> > > >  
> > > >  #define LENOVO_CAPABILITY_DATA_00_GUID "362A3AFE-3D96-4665-8530-96DAD5BB300E"
> > > >  #define LENOVO_CAPABILITY_DATA_01_GUID "7A8F5407-CB67-4D6E-B547-39B3BE018154"
> > > > +#define LENOVO_FAN_TEST_DATA_GUID "B642801B-3D21-45DE-90AE-6E86F164FB21"
> > > >  
> > > >  #define ACPI_AC_CLASS "ac_adapter"
> > > >  #define ACPI_AC_NOTIFY_STATUS 0x80
> > > > @@ -48,6 +53,7 @@
> > > >  enum lwmi_cd_type {
> > > >  	LENOVO_CAPABILITY_DATA_00,
> > > >  	LENOVO_CAPABILITY_DATA_01,
> > > > +	LENOVO_FAN_TEST_DATA,
> > > >  };
> > > >  
> > > >  #define LWMI_CD_TABLE_ITEM(_type)		\
> > > > @@ -62,6 +68,7 @@ static const struct lwmi_cd_info {
> > > >  } lwmi_cd_table[] = {
> > > >  	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_00),
> > > >  	LWMI_CD_TABLE_ITEM(LENOVO_CAPABILITY_DATA_01),
> > > > +	LWMI_CD_TABLE_ITEM(LENOVO_FAN_TEST_DATA),
> > > >  };
> > > >  
> > > >  struct lwmi_cd_priv {
> > > > @@ -78,6 +85,7 @@ struct cd_list {
> > > >  	union {
> > > >  		DECLARE_FLEX_ARRAY(struct capdata00, cd00);
> > > >  		DECLARE_FLEX_ARRAY(struct capdata01, cd01);
> > > > +		DECLARE_FLEX_ARRAY(struct capdata_fan, cd_fan);
> > > >  	};
> > > >  };
> > > >  
> > > > @@ -117,6 +125,10 @@ void lwmi_cd_match_add_all(struct device *master, struct component_match **match
> > > >  		return;
> > > >  
> > > >  	for (i = 0; i < ARRAY_SIZE(lwmi_cd_table); i++) {
> > > > +		/* Skip optional interfaces. */
> > > > +		if (lwmi_cd_table[i].type == LENOVO_FAN_TEST_DATA)
> > > > +			continue;
> > > > +
> > > >  		component_match_add(master, matchptr, lwmi_cd_match,
> > > >  				    (void *)&lwmi_cd_table[i].type);
> > > >  		if (IS_ERR(*matchptr))
> > > > @@ -194,6 +206,9 @@ EXPORT_SYMBOL_NS_GPL(lwmi_cd00_get_data, "LENOVO_WMI_CD");
> > > >  DEF_LWMI_CDXX_GET_DATA(cd01, LENOVO_CAPABILITY_DATA_01, struct capdata01);
> > > >  EXPORT_SYMBOL_NS_GPL(lwmi_cd01_get_data, "LENOVO_WMI_CD");
> > > >  
> > > > +DEF_LWMI_CDXX_GET_DATA(cd_fan, LENOVO_FAN_TEST_DATA, struct capdata_fan);
> > > > +EXPORT_SYMBOL_NS_GPL(lwmi_cd_fan_get_data, "LENOVO_WMI_CD");
> > > > +
> > > >  /**
> > > >   * lwmi_cd_cache() - Cache all WMI data block information
> > > >   * @priv: lenovo-wmi-capdata driver data.
> > > > @@ -217,6 +232,9 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
> > > >  		p = &priv->list->cd01[0];
> > > >  		size = sizeof(priv->list->cd01[0]);
> > > >  		break;
> > > > +	case LENOVO_FAN_TEST_DATA:
> > > > +		/* Done by lwmi_cd_alloc() => lwmi_cd_fan_list_alloc_cache(). */
> > > > +		return 0;
> > > >  	default:
> > > >  		return -EINVAL;
> > > >  	}
> > > > @@ -239,6 +257,78 @@ static int lwmi_cd_cache(struct lwmi_cd_priv *priv)
> > > >  	return 0;
> > > >  }
> > > >  
> > > > +/**
> > > > + * lwmi_cd_fan_list_alloc_cache() - Alloc and cache Fan Test Data list
> > > > + * @priv: lenovo-wmi-capdata driver data.
> > > > + * @listptr: Pointer to returned cd_list pointer.
> > > > + *
> > > > + * Return: count of fans found, or an error.
> > > > + */
> > > > +static int lwmi_cd_fan_list_alloc_cache(struct lwmi_cd_priv *priv, struct cd_list **listptr)
> > > > +{
> > > > +	u32 count, *fan_ids, *fan_min_rpms, *fan_max_rpms;
> > > > +	union acpi_object *ret_obj __free(kfree) = NULL;
> > > 
> > > Since you're using __free(), please move this to where you assign the 
> > > value. This is to create a pattern with cleanup helpers. The cleanup 
> > > order depends on the order the variables are introduced which in some 
> > > other cases may be significant.
> > 
> > Make sense. Will move it to the last declaration lines with its
> > assignment. Thanks.
> > 
> > > > +	struct block { u32 nr; u32 data[]; } *block;
> > > 
> > > This is the first time I see this style anywhere in the kernel's context, 
> > > has there been some general discussion about this style somewhere?
> > > 
> > > At least it seems immediately obvious to me that this style will have a 
> > > negative impact on documentability due to (too) concise use of space.
> > 
> > Make sense. Will break it into multiple lines. Thanks.
> > 
> > > > +	struct cd_list *list;
> > > > +	size_t size;
> > > > +	int idx;
> > > > +
> > > > +	ret_obj = wmidev_block_query(priv->wdev, 0);
> > > > +	if (!ret_obj)
> > > > +		return -ENODEV;
> > > > +
> > > > +	/*
> > > > +	 * This is usually caused by a dummy ACPI method. Do not return an error
> > > > +	 * as failing to probe this device will result in master driver being
> > > > +	 * unbound - this behavior aligns with lwmi_cd_cache().
> > > > +	 */
> > > > +	if (ret_obj->type != ACPI_TYPE_BUFFER) {
> > > > +		count = 0;
> > > > +		goto alloc;
> > > > +	}
> > > > +
> > > > +	size = ret_obj->buffer.length;
> > > > +	block = (struct block *)ret_obj->buffer.pointer;
> > > 
> > > void * can be cast implicitly.
> > 
> > `ret_obj->buffer.pointer' is a `u8 *' pointer so the cast is mandatory.
> 
> Ah, right. I think I even tried to check it but probably picked up
> the void *pointer from struct acpi_buffer instead of the correct one.
> 
> > Hmmm, this reminds me that `struct block' probably needs a `__packed'
> > to generate unaligned access to it.
> 
> In all u32 struct, I don't think __packed is required. You can use pahole 
> tool to check the layout if you want, but I don't think compiler adds 
> any gaps with only u32s.

__packed is not used to compact the structure layout here, but to tell
the compiler to emit[1] unaligned access to the ACPI buffer. See also
the change to [PATCH v3 1/6] suggested by Armin:
https://lore.kernel.org/r/a0b6d30b-3cba-4760-81dc-099e8fada7c0@gmx.de/

[1]: If CONFIG_UBSAN_ALIGNMENT=n, the assembly code should be the same
on x86. Otherwise, __packed effectively suppresses the alignment check
as unaligned access is expected.

Thanks,
Rong

^ permalink raw reply	[flat|nested] 22+ messages in thread

end of thread, other threads:[~2025-11-26 14:03 UTC | newest]

Thread overview: 22+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-11-22 18:44 [PATCH v6 0/7] platform/x86: lenovo-wmi-{capdata,other}: Add HWMON for fan speed Rong Zhang
2025-11-22 18:44 ` [PATCH v6 1/7] platform/x86: lenovo-wmi-helpers: Convert returned buffer into u32 Rong Zhang
2025-11-22 18:44 ` [PATCH v6 2/7] platform/x86: Rename lenovo-wmi-capdata01 to lenovo-wmi-capdata Rong Zhang
2025-11-22 18:44 ` [PATCH v6 3/7] platform/x86: lenovo-wmi-{capdata,other}: Support multiple Capability Data Rong Zhang
2025-11-24 16:30   ` Ilpo Järvinen
2025-11-24 20:03     ` Rong Zhang
2025-11-22 18:44 ` [PATCH v6 4/7] platform/x86: lenovo-wmi-capdata: Add support for Capability Data 00 Rong Zhang
2025-11-22 18:44 ` [PATCH v6 5/7] platform/x86: lenovo-wmi-capdata: Add support for Fan Test Data Rong Zhang
2025-11-24 16:45   ` Ilpo Järvinen
2025-11-24 20:07     ` Rong Zhang
2025-11-26  7:44       ` Ilpo Järvinen
2025-11-26 13:57         ` Rong Zhang
2025-11-22 18:44 ` [PATCH v6 6/7] platform/x86: lenovo-wmi-capdata: Wire up " Rong Zhang
2025-11-23  3:36   ` Armin Wolf
2025-11-23 15:02     ` Rong Zhang
2025-11-25  2:08       ` Armin Wolf
2025-11-24 16:49   ` Ilpo Järvinen
2025-11-24 20:10     ` Rong Zhang
2025-11-22 18:44 ` [PATCH v6 7/7] platform/x86: lenovo-wmi-other: Add HWMON for fan reporting/tuning Rong Zhang
2025-11-24 16:58   ` Ilpo Järvinen
2025-11-24 20:12     ` Rong Zhang
2025-11-26  8:04       ` Ilpo Järvinen

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).