Linux Documentation
 help / color / mirror / Atom feed
From: "Ilpo Järvinen" <ilpo.jarvinen@linux.intel.com>
To: Aditya Dash <mradityadash@gmail.com>
Cc: "Derek J. Clark" <derekjohn.clark@gmail.com>,
	Armin Wolf <W_Armin@gmx.de>,  Hans de Goede <hansg@kernel.org>,
	Mark Pearson <mpearson-lenovo@squebb.ca>,
	 Guenter Roeck <linux@roeck-us.net>,
	Jonathan Corbet <corbet@lwn.net>,
	 Shuah Khan <skhan@linuxfoundation.org>,
	linux-doc@vger.kernel.org,  linux-hwmon@vger.kernel.org,
	LKML <linux-kernel@vger.kernel.org>,
	 platform-driver-x86@vger.kernel.org
Subject: Re: [RFC PATCH 3/3] platform/x86: lenovo: Add Legion Go Fan Method curve driver
Date: Mon, 24 Aug 2026 11:10:52 +0300 (EEST)	[thread overview]
Message-ID: <b8ca6b4e-a3d1-60bd-fc12-5057bc69856e@linux.intel.com> (raw)
In-Reply-To: <20260821214728.87773-4-mradityadash@gmail.com>

On Sat, 22 Aug 2026, Aditya Dash wrote:

> Legion Go firmware keeps its custom fan curve in a separate Fan Method WMI
> interface. Method 5 returns ten control values and ten temperatures, while
> Method 6 replaces the complete table.
> 
> The GUID also appears on Lenovo systems with a different table layout.
> Limit the driver to Legion Go 8APU1, Legion Go 8ASP2, Legion Go 8AHP2,
> Legion Go S 8ARP1, and Legion Go S 8APU1 products. These products use the
> same ten-point layout and Method 6 request.
> 
> The curve applies to the same fan as the existing Other Mode attributes.
> Add a separate WMI driver and attach the ten automatic-point pairs to the
> HWMON device that Other Mode already provides. Match devices from the same
> WMI provider. Other Mode continues to work without Fan Method. Probe does
> not invoke either method.
> 
> Validate both counts, every control value, and the complete temperature
> table before using a Method 5 reply. A point write holds the mutex while
> it reads the current table, changes one control value, and submits the
> complete Method 6 request. The other values remain unchanged.
> 
> The HWMON mapping is not clear. On the tested Legion Go 8APU1, the fan
> responds to values from 0 through 115. This is Lenovo's 0 through 115
> percent range. Lenovo software shows 0 through 100 percent to the user,
> and 115 reaches the RPM observed in Full Speed mode. Firmware accepts
> values through 255, but values above 115 caused no observed RPM increase.
> 
> This RFC exposes the firmware values unchanged. The fixed temperatures are
> read-only.
> 
> Assisted-by: Pi:gpt-5.6-sol
> Signed-off-by: Aditya Dash <mradityadash@gmail.com>
> ---
>  .../wmi/devices/lenovo-wmi-fan-method.rst     |  34 ++
>  .../wmi/devices/lenovo-wmi-other.rst          |   3 +
>  MAINTAINERS                                   |   1 +
>  drivers/platform/x86/lenovo/Kconfig           |  11 +
>  drivers/platform/x86/lenovo/Makefile          |   1 +
>  drivers/platform/x86/lenovo/wmi-fan-method.c  | 380 ++++++++++++++++++
>  drivers/platform/x86/lenovo/wmi-other.c       |  97 ++++-
>  7 files changed, 520 insertions(+), 7 deletions(-)
>  create mode 100644 Documentation/wmi/devices/lenovo-wmi-fan-method.rst
>  create mode 100644 drivers/platform/x86/lenovo/wmi-fan-method.c
> 
> diff --git a/Documentation/wmi/devices/lenovo-wmi-fan-method.rst b/Documentation/wmi/devices/lenovo-wmi-fan-method.rst
> new file mode 100644
> index 000000000000..67c9a92ed992
> --- /dev/null
> +++ b/Documentation/wmi/devices/lenovo-wmi-fan-method.rst
> @@ -0,0 +1,34 @@
> +.. SPDX-License-Identifier: GPL-2.0-or-later
> +
> +==================================
> +Lenovo Fan Method WMI Driver
> +==================================
> +
> +WMI GUID ``92549549-4BDE-4F06-AC04-CE8BF898DBAA``
> +
> +The Lenovo Fan Method interface provides a ten-point firmware fan table on
> +Legion Go 8APU1, Legion Go 8ASP2, Legion Go 8AHP2, Legion Go S 8ARP1, and
> +Legion Go S 8APU1 products.
> +
> +The driver adds ``pwm1_auto_point1_*`` through
> +``pwm1_auto_point10_*`` to the HWMON device that the Lenovo Other Mode driver
> +owns. The temperature attributes are fixed and read-only. They contain points
> +from 10 through 100 degrees Celsius in 10-degree steps.
> +
> +Each ``pwm1_auto_point*_pwm`` attribute is read-write and passes a firmware
> +control value from 0 through 255 without scaling. On the tested Legion Go
> +8APU1, the hardware responds from 0 through 115, corresponding to Lenovo's
> +0 through 115 percent range. Lenovo software shows 0 through 100 percent to
> +the user, and 115 percent reaches the RPM observed in Full Speed mode. The
> +firmware accepts values through 255, but values above 115 caused no observed
> +RPM increase.
> +
> +A control-value read validates both ten-entry tables before returning data. A
> +point write reads the current table, changes one control value, and submits the
> +complete request. It preserves the other nine control values and all returned
> +temperatures.
> +
> +The Fan Method and Other Mode drivers use separate modules and WMI devices.
> +The component framework associates devices that belong to the same WMI
> +provider. Fan Method curve attributes are absent when either interface is not
> +available.
> diff --git a/Documentation/wmi/devices/lenovo-wmi-other.rst b/Documentation/wmi/devices/lenovo-wmi-other.rst
> index c93e9e6f3fd0..34352dbd6fb9 100644
> --- a/Documentation/wmi/devices/lenovo-wmi-other.rst
> +++ b/Documentation/wmi/devices/lenovo-wmi-other.rst
> @@ -60,6 +60,9 @@ Some Legion Go firmware does not advertise fan 1 RPM through Capability
>  Data. In that case, the driver reads Other Mode feature ``0x04030001`` for
>  ``fan1_input``. Value ``0xffffffff`` means that RPM is unavailable.
>  
> +When the separate Lenovo Fan Method driver binds to the same WMI provider, it
> +adds ten automatic fan-curve points to this HWMON device.
> +
>  LENOVO_CAPABILITY_DATA_01
>  -------------------------
>  
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 3820b286f09f..8612c2323540 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -14689,6 +14689,7 @@ M:	Mark Pearson <mpearson-lenovo@squebb.ca>
>  M:	Derek J. Clark <derekjohn.clark@gmail.com>
>  L:	platform-driver-x86@vger.kernel.org
>  S:	Maintained
> +F:	Documentation/wmi/devices/lenovo-wmi-fan-method.rst
>  F:	Documentation/wmi/devices/lenovo-wmi-gamezone.rst
>  F:	Documentation/wmi/devices/lenovo-wmi-other.rst
>  F:	drivers/platform/x86/lenovo/*
> diff --git a/drivers/platform/x86/lenovo/Kconfig b/drivers/platform/x86/lenovo/Kconfig
> index 4443f40ef8aa..e69942ce3093 100644
> --- a/drivers/platform/x86/lenovo/Kconfig
> +++ b/drivers/platform/x86/lenovo/Kconfig
> @@ -276,3 +276,14 @@ config LENOVO_WMI_TUNING
>  
>  	  To compile this driver as a module, choose M here: the module will
>  	  be called lenovo-wmi-other.
> +
> +config LENOVO_WMI_FAN_METHOD
> +	tristate "Lenovo Fan Method WMI Driver"
> +	depends on DMI
> +	depends on LENOVO_WMI_TUNING
> +	help
> +	  Say Y here to add the firmware fan table on supported Lenovo
> +	  Legion Go products to the Lenovo Other Mode HWMON device.
> +
> +	  To compile this driver as a module, choose M here: the module will
> +	  be called lenovo-wmi-fan-method.
> diff --git a/drivers/platform/x86/lenovo/Makefile b/drivers/platform/x86/lenovo/Makefile
> index 91a9370f11b3..171a19e01479 100644
> --- a/drivers/platform/x86/lenovo/Makefile
> +++ b/drivers/platform/x86/lenovo/Makefile
> @@ -17,6 +17,7 @@ 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
>  lenovo-target-$(CONFIG_LENOVO_WMI_TUNING)	+= wmi-other.o
> +lenovo-target-$(CONFIG_LENOVO_WMI_FAN_METHOD)	+= wmi-fan-method.o
>  
>  # Add 'lenovo' prefix to each module listed in lenovo-target-*
>  define LENOVO_OBJ_TARGET
> diff --git a/drivers/platform/x86/lenovo/wmi-fan-method.c b/drivers/platform/x86/lenovo/wmi-fan-method.c
> new file mode 100644
> index 000000000000..4f29c4134caf
> --- /dev/null
> +++ b/drivers/platform/x86/lenovo/wmi-fan-method.c
> @@ -0,0 +1,380 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Lenovo Fan Method WMI interface driver.
> + *
> + * This driver exposes the firmware fan table through HWMON automatic-point
> + * attributes on selected Lenovo Legion Go products.
> + */
> +
> +#include <linux/cleanup.h>
> +#include <linux/component.h>
> +#include <linux/device.h>
> +#include <linux/dmi.h>
> +#include <linux/err.h>
> +#include <linux/kernel.h>
> +#include <linux/limits.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/slab.h>
> +#include <linux/string.h>
> +#include <linux/sysfs.h>
> +#include <linux/types.h>
> +#include <linux/unaligned.h>
> +#include <linux/wmi.h>
> +
> +#define LENOVO_FAN_METHOD_GUID "92549549-4BDE-4F06-AC04-CE8BF898DBAA"
> +
> +#define LENOVO_FAN_METHOD_GET_CURVE	5
> +#define LENOVO_FAN_METHOD_SET_CURVE	6
> +#define LENOVO_FAN_CURVE_POINTS		10
> +#define LENOVO_FAN_CURVE_REPLY_SIZE	88
> +#define LENOVO_FAN_CURVE_WRITE_SIZE	64
> +
> +#define LENOVO_FAN_REPLY_SPEED_COUNT_OFFSET	0
> +#define LENOVO_FAN_REPLY_SPEED_OFFSET		4
> +#define LENOVO_FAN_REPLY_TEMP_COUNT_OFFSET	44
> +#define LENOVO_FAN_REPLY_TEMP_OFFSET		48
> +
> +#define LENOVO_FAN_WRITE_SPEED_COUNT_OFFSET	2
> +#define LENOVO_FAN_WRITE_SPEED_OFFSET		6
> +#define LENOVO_FAN_WRITE_TEMP_TYPE_OFFSET	26
> +#define LENOVO_FAN_WRITE_TEMP_COUNT_OFFSET	27
> +#define LENOVO_FAN_WRITE_TEMP_OFFSET		31
> +#define LENOVO_FAN_WRITE_TRAILER_OFFSET		51
> +#define LENOVO_FAN_WRITE_TRAILER_VALUE_OFFSET	53
> +
> +#define LENOVO_FAN_WRITE_TEMP_TYPE		1
> +#define LENOVO_FAN_WRITE_TRAILER		0x5a
> +#define LENOVO_FAN_WRITE_TRAILER_VALUE		100
> +
> +static const struct dmi_system_id lwmi_fan_dmi_table[] = {
> +	{
> +		.ident = "Lenovo Legion Go 8APU1",
> +		.matches = {
> +			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
> +			DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8APU1"),
> +		},
> +	},
> +	{
> +		.ident = "Lenovo Legion Go S 8APU1",
> +		.matches = {
> +			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
> +			DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go S 8APU1"),
> +		},
> +	},
> +	{
> +		.ident = "Lenovo Legion Go S 8ARP1",
> +		.matches = {
> +			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
> +			DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go S 8ARP1"),
> +		},
> +	},
> +	{
> +		.ident = "Lenovo Legion Go 8ASP2",
> +		.matches = {
> +			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
> +			DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8ASP2"),
> +		},
> +	},
> +	{
> +		.ident = "Lenovo Legion Go 8AHP2",
> +		.matches = {
> +			DMI_MATCH(DMI_SYS_VENDOR, "LENOVO"),
> +			DMI_EXACT_MATCH(DMI_PRODUCT_VERSION, "Legion Go 8AHP2"),
> +		},
> +	},
> +	{}
> +};
> +
> +static const u16 lwmi_fan_temperatures[LENOVO_FAN_CURVE_POINTS] = {
> +	10, 20, 30, 40, 50, 60, 70, 80, 90, 100
> +};
> +
> +struct lwmi_fan_method_curve {
> +	u16 speed[LENOVO_FAN_CURVE_POINTS];
> +	u16 temperature[LENOVO_FAN_CURVE_POINTS];
> +};
> +
> +struct lwmi_fan_method_priv;
> +
> +struct lwmi_fan_method_attr {
> +	struct device_attribute dev_attr;
> +	struct lwmi_fan_method_priv *priv;
> +	u8 index;
> +};
> +
> +struct lwmi_fan_method_priv {
> +	struct wmi_device *wdev;
> +	struct mutex lock; /* Serializes all Fan Method calls. */

Please move the comment a bit right to separate code from comment 
visually (by 1 tab or so is enough as long as there's > 1 whitespace).

> +	struct lwmi_fan_method_attr point_attrs[LENOVO_FAN_CURVE_POINTS * 2];
> +	struct attribute *attrs[LENOVO_FAN_CURVE_POINTS * 2 + 1];
> +	struct attribute_group group;
> +};
> +
> +static int lwmi_fan_method_get_curve(struct lwmi_fan_method_priv *priv,
> +				     struct lwmi_fan_method_curve *curve)
> +{
> +	u8 input[] = { 1, 1 };
> +	struct wmi_buffer in = {
> +		.length = sizeof(input),
> +		.data = input,
> +	};
> +	struct wmi_buffer out = {};
> +	const u8 *buffer;
> +	int ret, i;
> +
> +	ret = wmidev_invoke_method(priv->wdev, 0,
> +				   LENOVO_FAN_METHOD_GET_CURVE, &in, &out,
> +				   LENOVO_FAN_CURVE_REPLY_SIZE);
> +	if (ret)
> +		return ret;
> +
> +	buffer = out.data;

Best practices with this interface: Declare variable on this lines 
(mid-function) and use __free().

> +	if (get_unaligned_le32(buffer + LENOVO_FAN_REPLY_SPEED_COUNT_OFFSET) !=
> +	    LENOVO_FAN_CURVE_POINTS ||
> +	    get_unaligned_le32(buffer + LENOVO_FAN_REPLY_TEMP_COUNT_OFFSET) !=

Please read what wmidev_invoke_method()'s function comment says about 
alignment.

> +	    LENOVO_FAN_CURVE_POINTS) {
> +		ret = -ERANGE;
> +		goto out_free;
> +	}
> +
> +	for (i = 0; i < LENOVO_FAN_CURVE_POINTS; i++) {
> +		u32 speed = get_unaligned_le32(buffer + LENOVO_FAN_REPLY_SPEED_OFFSET +
> +					       i * sizeof(u32));
> +		u32 temperature =
> +			get_unaligned_le32(buffer + LENOVO_FAN_REPLY_TEMP_OFFSET +
> +					   i * sizeof(u32));
> +
> +		if (speed > U8_MAX || temperature != lwmi_fan_temperatures[i]) {
> +			ret = -ERANGE;
> +			goto out_free;
> +		}
> +
> +		curve->speed[i] = speed;
> +		curve->temperature[i] = temperature;
> +	}
> +
> +out_free:
> +	kfree(out.data);
> +	return ret;
> +}
> +
> +static int lwmi_fan_method_set_curve(struct lwmi_fan_method_priv *priv,
> +				     const struct lwmi_fan_method_curve *curve)
> +{
> +	u8 buffer[LENOVO_FAN_CURVE_WRITE_SIZE] = { 0xff, 0x01 };
> +	struct wmi_buffer in = {
> +		.length = sizeof(buffer),
> +		.data = buffer,
> +	};
> +	int i;
> +
> +	put_unaligned_le32(LENOVO_FAN_CURVE_POINTS,
> +			   buffer + LENOVO_FAN_WRITE_SPEED_COUNT_OFFSET);
> +	for (i = 0; i < LENOVO_FAN_CURVE_POINTS; i++)
> +		put_unaligned_le16(curve->speed[i],
> +				   buffer + LENOVO_FAN_WRITE_SPEED_OFFSET +
> +				   i * sizeof(u16));
> +
> +	buffer[LENOVO_FAN_WRITE_TEMP_TYPE_OFFSET] = LENOVO_FAN_WRITE_TEMP_TYPE;
> +	put_unaligned_le32(LENOVO_FAN_CURVE_POINTS,
> +			   buffer + LENOVO_FAN_WRITE_TEMP_COUNT_OFFSET);
> +	for (i = 0; i < LENOVO_FAN_CURVE_POINTS; i++)
> +		put_unaligned_le16(curve->temperature[i],
> +				   buffer + LENOVO_FAN_WRITE_TEMP_OFFSET +
> +				   i * sizeof(u16));
> +
> +	buffer[LENOVO_FAN_WRITE_TRAILER_OFFSET] = LENOVO_FAN_WRITE_TRAILER;
> +	put_unaligned_le16(LENOVO_FAN_WRITE_TRAILER_VALUE,
> +			   buffer + LENOVO_FAN_WRITE_TRAILER_VALUE_OFFSET);
> +
> +	return wmidev_invoke_procedure(priv->wdev, 0,
> +				       LENOVO_FAN_METHOD_SET_CURVE, &in);
> +}
> +
> +static ssize_t lwmi_fan_method_pwm_show(struct device *dev,
> +					struct device_attribute *attr,
> +					char *buf)
> +{
> +	struct lwmi_fan_method_attr *point_attr =
> +		container_of(attr, struct lwmi_fan_method_attr, dev_attr);
> +	struct lwmi_fan_method_priv *priv = point_attr->priv;
> +	struct lwmi_fan_method_curve curve;
> +	int ret;
> +
> +	guard(mutex)(&priv->lock);
> +
> +	ret = lwmi_fan_method_get_curve(priv, &curve);
> +	if (ret)
> +		return ret;
> +
> +	return sysfs_emit(buf, "%u\n", curve.speed[point_attr->index]);
> +}
> +
> +static ssize_t lwmi_fan_method_pwm_store(struct device *dev,
> +					 struct device_attribute *attr,
> +					 const char *buf, size_t count)
> +{
> +	struct lwmi_fan_method_attr *point_attr =
> +		container_of(attr, struct lwmi_fan_method_attr, dev_attr);
> +	struct lwmi_fan_method_priv *priv = point_attr->priv;
> +	struct lwmi_fan_method_curve curve;
> +	unsigned long pwm;
> +	int ret;
> +
> +	ret = kstrtoul(buf, 10, &pwm);
> +	if (ret)
> +		return ret;
> +	if (pwm > U8_MAX)

Use kstrtou8() ?

> +		return -EINVAL;
> +
> +	guard(mutex)(&priv->lock);
> +
> +	ret = lwmi_fan_method_get_curve(priv, &curve);
> +	if (ret)
> +		return ret;
> +
> +	curve.speed[point_attr->index] = pwm;
> +	ret = lwmi_fan_method_set_curve(priv, &curve);
> +
> +	return ret ? ret : count;
> +}
> +
> +static ssize_t lwmi_fan_method_temp_show(struct device *dev,
> +					 struct device_attribute *attr,
> +					 char *buf)
> +{
> +	struct lwmi_fan_method_attr *point_attr =
> +		container_of(attr, struct lwmi_fan_method_attr, dev_attr);
> +
> +	return sysfs_emit(buf, "%u\n",
> +			  lwmi_fan_temperatures[point_attr->index] * 1000);

Check if that 1000 can be replaced with something from units.h.

> +}
> +
> +#define LWMI_FAN_METHOD_POINT_NAMES(_point) \
> +	"pwm1_auto_point" #_point "_pwm", \
> +	"pwm1_auto_point" #_point "_temp"
> +
> +static const char * const lwmi_fan_method_attr_names[] = {
> +	LWMI_FAN_METHOD_POINT_NAMES(1),
> +	LWMI_FAN_METHOD_POINT_NAMES(2),
> +	LWMI_FAN_METHOD_POINT_NAMES(3),
> +	LWMI_FAN_METHOD_POINT_NAMES(4),
> +	LWMI_FAN_METHOD_POINT_NAMES(5),
> +	LWMI_FAN_METHOD_POINT_NAMES(6),
> +	LWMI_FAN_METHOD_POINT_NAMES(7),
> +	LWMI_FAN_METHOD_POINT_NAMES(8),
> +	LWMI_FAN_METHOD_POINT_NAMES(9),
> +	LWMI_FAN_METHOD_POINT_NAMES(10),
> +};
> +
> +#undef LWMI_FAN_METHOD_POINT_NAMES
> +
> +static void lwmi_fan_method_attrs_init(struct lwmi_fan_method_priv *priv)
> +{
> +	int i;
> +
> +	for (i = 0; i < LENOVO_FAN_CURVE_POINTS; i++) {
> +		struct lwmi_fan_method_attr *pwm = &priv->point_attrs[i * 2];
> +		struct lwmi_fan_method_attr *temp = &priv->point_attrs[i * 2 + 1];
> +
> +		sysfs_attr_init(&pwm->dev_attr.attr);
> +		pwm->dev_attr.attr.name = lwmi_fan_method_attr_names[i * 2];
> +		pwm->dev_attr.attr.mode = 0644;
> +		pwm->dev_attr.show = lwmi_fan_method_pwm_show;
> +		pwm->dev_attr.store = lwmi_fan_method_pwm_store;
> +		pwm->priv = priv;
> +		pwm->index = i;
> +		priv->attrs[i * 2] = &pwm->dev_attr.attr;
> +
> +		sysfs_attr_init(&temp->dev_attr.attr);
> +		temp->dev_attr.attr.name = lwmi_fan_method_attr_names[i * 2 + 1];
> +		temp->dev_attr.attr.mode = 0444;
> +		temp->dev_attr.show = lwmi_fan_method_temp_show;
> +		temp->index = i;
> +		priv->attrs[i * 2 + 1] = &temp->dev_attr.attr;
> +	}
> +
> +	priv->group.attrs = priv->attrs;
> +}
> +
> +static int lwmi_fan_method_master_bind(struct device *dev)
> +{
> +	struct lwmi_fan_method_priv *priv = dev_get_drvdata(dev);
> +
> +	return component_bind_all(dev, &priv->group);
> +}
> +
> +static void lwmi_fan_method_master_unbind(struct device *dev)
> +{
> +	component_unbind_all(dev, NULL);
> +}
> +
> +static const struct component_master_ops lwmi_fan_method_master_ops = {
> +	.bind = lwmi_fan_method_master_bind,
> +	.unbind = lwmi_fan_method_master_unbind,
> +};
> +
> +static int lwmi_fan_method_component_compare(struct device *dev, void *data)
> +{
> +	struct device *master = data;
> +
> +	return dev->driver &&
> +		!strcmp(dev->driver->name, "lenovo_wmi_other") &&
> +		dev->parent == master->parent;
> +}
> +
> +static int lwmi_fan_method_probe(struct wmi_device *wdev, const void *context)
> +{
> +	struct component_match *master_match = NULL;
> +	struct lwmi_fan_method_priv *priv;
> +
> +	if (!dmi_check_system(lwmi_fan_dmi_table))
> +		return -ENODEV;
> +
> +	priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
> +	if (!priv)
> +		return -ENOMEM;
> +
> +	priv->wdev = wdev;
> +	mutex_init(&priv->lock);

This would need to be paired with mutex_destroy.

BUT instead, use devm_mutex_init() + don't forget error handling, it can 
fail.

> +	lwmi_fan_method_attrs_init(priv);
> +	dev_set_drvdata(&wdev->dev, priv);
> +
> +	component_match_add(&wdev->dev, &master_match,
> +			    lwmi_fan_method_component_compare, &wdev->dev);
> +	if (IS_ERR(master_match))
> +		return PTR_ERR(master_match);
> +
> +	return component_master_add_with_match(&wdev->dev,
> +					       &lwmi_fan_method_master_ops,
> +					       master_match);
> +}
> +
> +static void lwmi_fan_method_remove(struct wmi_device *wdev)
> +{
> +	component_master_del(&wdev->dev, &lwmi_fan_method_master_ops);
> +}
> +
> +static const struct wmi_device_id lwmi_fan_method_id_table[] = {
> +	{ LENOVO_FAN_METHOD_GUID, NULL },
> +	{}
> +};
> +
> +static struct wmi_driver lwmi_fan_method_driver = {
> +	.driver = {
> +		.name = "lenovo_wmi_fan_method",
> +		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
> +	},
> +	.id_table = lwmi_fan_method_id_table,
> +	.probe = lwmi_fan_method_probe,
> +	.remove = lwmi_fan_method_remove,
> +	.no_singleton = true,
> +};
> +
> +MODULE_DEVICE_TABLE(wmi, lwmi_fan_method_id_table);
> +module_wmi_driver(lwmi_fan_method_driver);
> +
> +MODULE_AUTHOR("Aditya Dash <mradityadash@gmail.com>");
> +MODULE_DESCRIPTION("Lenovo Fan Method WMI Driver");
> +MODULE_LICENSE("GPL");
> diff --git a/drivers/platform/x86/lenovo/wmi-other.c b/drivers/platform/x86/lenovo/wmi-other.c
> index b4be7739b243..72d3510b17b2 100644
> --- a/drivers/platform/x86/lenovo/wmi-other.c
> +++ b/drivers/platform/x86/lenovo/wmi-other.c
> @@ -192,11 +192,13 @@ struct lwmi_om_priv {
>  	struct device *fw_attr_dev;
>  	struct kset *fw_attr_kset;
>  	struct wmi_device *wdev;
> +	const struct attribute_group *fan_method_group;
>  	int ida_id;
>  
>  	struct lwmi_fan_info fan_info[LWMI_FAN_NR];
>  	bool fullspeed_supported;
>  	bool fan0_input_fallback;
> +	bool fan_method_group_added;
>  
>  	struct {
>  		bool capdata00_collected : 1;
> @@ -542,6 +544,31 @@ static const struct hwmon_chip_info lwmi_om_hwmon_chip_info = {
>  	.info = lwmi_om_hwmon_info,
>  };
>  
> +static int lwmi_om_fan_method_group_add(struct lwmi_om_priv *priv)
> +{
> +	int ret;
> +
> +	if (!priv->hwmon_dev || !priv->fan_method_group ||
> +	    priv->fan_method_group_added)
> +		return 0;
> +
> +	ret = sysfs_create_group(&priv->hwmon_dev->kobj,
> +				 priv->fan_method_group);
> +	if (!ret)
> +		priv->fan_method_group_added = true;
> +
> +	return ret;
> +}
> +
> +static void lwmi_om_fan_method_group_remove(struct lwmi_om_priv *priv)
> +{
> +	if (!priv->hwmon_dev || !priv->fan_method_group_added)
> +		return;
> +
> +	sysfs_remove_group(&priv->hwmon_dev->kobj, priv->fan_method_group);
> +	priv->fan_method_group_added = false;
> +}
> +
>  /**
>   * lwmi_om_hwmon_add() - Register HWMON device if all info is collected
>   * @priv: Driver private data
> @@ -550,9 +577,9 @@ static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv)
>  {
>  	u32 rpm;
>  	long enable;
> -	int i, valid;
> +	int i, ret, valid;
>  
> -	if (WARN_ON(priv->hwmon_dev))
> +	if (priv->hwmon_dev)

If you need changes to hwmon_dev behavior, could you please separate 
things related to that into own patch with a proper justification (you can 
refer to the following change with "an upcoming change" if needed to 
explain why you have to do it).

>  		return;
>  
>  	if (!priv->fan_flags.capdata00_collected || !priv->fan_flags.capdata_fan_collected) {
> @@ -592,7 +619,8 @@ static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv)
>  		}
>  	}
>  
> -	if (valid == 0 && !priv->fullspeed_supported && !priv->fan0_input_fallback) {
> +	if (valid == 0 && !priv->fullspeed_supported &&
> +	    !priv->fan0_input_fallback && !priv->fan_method_group) {
>  		dev_warn(&priv->wdev->dev,
>  			 "fan reporting/tuning is unsupported on this device\n");
>  		return;
> @@ -609,6 +637,11 @@ static void lwmi_om_hwmon_add(struct lwmi_om_priv *priv)
>  		return;
>  	}
>  
> +	ret = lwmi_om_fan_method_group_add(priv);
> +	if (ret)
> +		dev_warn(&priv->wdev->dev,
> +			 "failed to register Fan Method attributes: %d\n", ret);
> +
>  	dev_dbg(&priv->wdev->dev, "registered HWMON device\n");
>  }
>  
> @@ -623,6 +656,7 @@ static void lwmi_om_hwmon_remove(struct lwmi_om_priv *priv)
>  	if (!priv->hwmon_dev)
>  		return;
>  
> +	lwmi_om_fan_method_group_remove(priv);
>  	hwmon_device_unregister(priv->hwmon_dev);
>  	priv->hwmon_dev = NULL;
>  }
> @@ -1801,10 +1835,44 @@ static const struct component_master_ops lwmi_om_master_ops = {
>  	.unbind = lwmi_om_master_unbind,
>  };
>  
> +static int lwmi_om_fan_method_component_bind(struct device *component,
> +					     struct device *master, void *data)
> +{
> +	const struct attribute_group *group = data;
> +	struct lwmi_om_priv *priv = dev_get_drvdata(component);
> +	int ret;
> +
> +	if (!group)
> +		return -EINVAL;
> +
> +	priv->fan_method_group = group;
> +	lwmi_om_hwmon_add(priv);
> +	ret = lwmi_om_fan_method_group_add(priv);
> +	if (ret)
> +		priv->fan_method_group = NULL;
> +
> +	return ret;
> +}
> +
> +static void lwmi_om_fan_method_component_unbind(struct device *component,
> +						struct device *master, void *data)
> +{
> +	struct lwmi_om_priv *priv = dev_get_drvdata(component);
> +
> +	lwmi_om_fan_method_group_remove(priv);
> +	priv->fan_method_group = NULL;
> +}
> +
> +static const struct component_ops lwmi_om_fan_method_component_ops = {
> +	.bind = lwmi_om_fan_method_component_bind,
> +	.unbind = lwmi_om_fan_method_component_unbind,
> +};
> +
>  static int lwmi_other_probe(struct wmi_device *wdev, const void *context)
>  {
>  	struct component_match *master_match = NULL;
>  	struct lwmi_om_priv *priv;
> +	int ret;
>  
>  	priv = devm_kzalloc(&wdev->dev, sizeof(*priv), GFP_KERNEL);
>  	if (!priv)
> @@ -1816,16 +1884,31 @@ static int lwmi_other_probe(struct wmi_device *wdev, const void *context)
>  	priv->wdev = wdev;
>  	dev_set_drvdata(&wdev->dev, priv);
>  
> +	ret = component_add(&wdev->dev, &lwmi_om_fan_method_component_ops);
> +	if (ret)
> +		return ret;
> +
>  	lwmi_cd_match_add_all(&wdev->dev, &master_match);
> -	if (IS_ERR(master_match))
> -		return PTR_ERR(master_match);
> +	if (IS_ERR(master_match)) {
> +		ret = PTR_ERR(master_match);
> +		goto err_component;
> +	}
> +
> +	ret = component_master_add_with_match(&wdev->dev, &lwmi_om_master_ops,
> +					      master_match);
> +	if (ret)
> +		goto err_component;
> +
> +	return 0;
>  
> -	return component_master_add_with_match(&wdev->dev, &lwmi_om_master_ops,
> -					       master_match);
> +err_component:
> +	component_del(&wdev->dev, &lwmi_om_fan_method_component_ops);
> +	return ret;
>  }
>  
>  static void lwmi_other_remove(struct wmi_device *wdev)
>  {
> +	component_del(&wdev->dev, &lwmi_om_fan_method_component_ops);
>  	component_master_del(&wdev->dev, &lwmi_om_master_ops);
>  }
>  
> 

-- 
 i.


  parent reply	other threads:[~2026-08-24  8:11 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-21 21:47 [RFC PATCH 0/3] platform/x86: lenovo: Add Legion Go fan controls Aditya Dash
2026-08-21 21:47 ` [RFC PATCH 1/3] platform/x86: lenovo-wmi-other: Add Legion Go Full Speed control Aditya Dash
2026-08-22 18:55   ` Antheas Kapenekakis
2026-08-23 19:55   ` Rong Zhang
2026-08-24  7:46   ` Ilpo Järvinen
2026-08-21 21:47 ` [RFC PATCH 2/3] platform/x86: lenovo-wmi-other: Add Legion Go fan RPM fallback Aditya Dash
2026-08-22 18:50   ` Antheas Kapenekakis
2026-08-23 20:23   ` Rong Zhang
2026-08-24  7:49   ` Ilpo Järvinen
2026-08-21 21:47 ` [RFC PATCH 3/3] platform/x86: lenovo: Add Legion Go Fan Method curve driver Aditya Dash
2026-08-22 18:47   ` Antheas Kapenekakis
2026-08-23 21:09   ` Rong Zhang
2026-08-24  8:10   ` Ilpo Järvinen [this message]
2026-08-22 18:33 ` [RFC PATCH 0/3] platform/x86: lenovo: Add Legion Go fan controls Antheas Kapenekakis
2026-08-23 19:32 ` Rong Zhang
2026-08-23 20:08   ` Derek J. Clark
2026-08-23 20:24     ` Rong Zhang

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=b8ca6b4e-a3d1-60bd-fc12-5057bc69856e@linux.intel.com \
    --to=ilpo.jarvinen@linux.intel.com \
    --cc=W_Armin@gmx.de \
    --cc=corbet@lwn.net \
    --cc=derekjohn.clark@gmail.com \
    --cc=hansg@kernel.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-hwmon@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux@roeck-us.net \
    --cc=mpearson-lenovo@squebb.ca \
    --cc=mradityadash@gmail.com \
    --cc=platform-driver-x86@vger.kernel.org \
    --cc=skhan@linuxfoundation.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox