[PATCH v4 6/6] platform/x86: bitland-mifs-wmi: Add Redmi laptop support

Mingyou Chen <[email protected]>
Newsgroups org.kernel.vger.platform-driver-x86,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
Redmi laptops (Redmi Book and Redmi G series) use the same MIFS WMI
interface but encode the performance mode values of
WMI_FN_SYSTEM_PER_MODE differently, as reverse engineered from the
Windows GCommandCenter application:

  0 = performance, 1 = balanced, 2 = quiet, 4 = full speed

Add "Redmi Book" and "Redmi G" ops entries selected via DMI:
- Redmi G (board TM2135/TM2137): the full-speed mode is available
- Redmi Book/Xiaomi Book: no full-speed mode
- The performance modes only require AC power (any source); the
  barrel-jack requirement is Bitland-specific

The GCommandCenter application further showed that:
- the CPU power sensor (WMI_FN_CPU_POWER) is only available on Redmi
  machines, gate the hwmon power channel on the ops table
- the refresh-rate-toggle quirk in the CPU fan speed event is
  Redmi-specific, gate it on the ops table instead of applying it
  unconditionally

Also add Redmi support for Fn lock and touchpad lock control
(WMI_FN_FN_LOCK / WMI_FN_TP_LOCK sysfs attributes), keyboard type
detection (hide the RGB kb_mode attribute on white-only backlit
keyboards) and hotkeys (airplane mode -> KEY_RFKILL, ignore the
lock-state and keyboard mode indicator events).

The sysfs attribute group is now registered in probe (instead of via
dev_groups) so that the is_visible callback can access the probe
populated driver data.

Signed-off-by: Mingyou Chen <[email protected]>
---
 drivers/platform/x86/bitland-mifs-wmi.c | 336 ++++++++++++++++++++++--
 1 file changed, 318 insertions(+), 18 deletions(-)

diff --git a/drivers/platform/x86/bitland-mifs-wmi.c b/drivers/platform/x86/bitland-mifs-wmi.c
index 17792d8b7c29..41ffc5ca56a1 100644
--- a/drivers/platform/x86/bitland-mifs-wmi.c
+++ b/drivers/platform/x86/bitland-mifs-wmi.c
@@ -14,6 +14,7 @@
 #include <linux/dev_printk.h>
 #include <linux/device.h>
 #include <linux/device/devres.h>
+#include <linux/dmi.h>
 #include <linux/err.h>
 #include <linux/hwmon.h>
 #include <linux/init.h>
@@ -71,6 +72,11 @@ enum bitland_system_ac_mode {
 	WMI_SYSTEM_AC_CIRCULARHOLE	= 2,
 };
 
+enum bitland_kbd_type {
+	WMI_KBD_TYPE_WHITE		= 0,
+	WMI_KBD_TYPE_RGB		= 1,
+};
+
 enum bitland_mifs_power_profile {
 	WMI_PP_BALANCED		= 0,
 	WMI_PP_PERFORMANCE	= 1,
@@ -97,7 +103,15 @@ static const struct bitland_profile_mode_map bitland_mode_map = {
 	.full_speed	= WMI_PP_FULL_SPEED,
 };
 
+static const struct bitland_profile_mode_map redmi_mode_map = {
+	.balanced	= 1,
+	.performance	= 0,
+	.quiet		= 2,
+	.full_speed	= 4,
+};
+
 static int bitland_pp_check_capability(struct bitland_mifs_wmi_data *data);
+static int redmi_pp_check_capability(struct bitland_mifs_wmi_data *data);
 
 /*
  * Machine-specific operations. The MIFS WMI interface is shared by
@@ -109,6 +123,8 @@ struct bitland_mifs_ops {
 	const struct bitland_profile_mode_map *mode_map;
 	int (*check_performance_capability)(struct bitland_mifs_wmi_data *data);
 	bool has_full_speed;
+	bool has_cpu_power;
+	bool quirk_refresh_rate_toggle;
 };
 
 static const struct bitland_mifs_ops bitland_ops = {
@@ -118,6 +134,54 @@ static const struct bitland_mifs_ops bitland_ops = {
 	.has_full_speed = true,
 };
 
+static const struct bitland_mifs_ops redmi_book_ops = {
+	.name = "Redmi Book",
+	.mode_map = &redmi_mode_map,
+	.check_performance_capability = redmi_pp_check_capability,
+	.has_cpu_power = true,
+	.quirk_refresh_rate_toggle = true,
+};
+
+static const struct bitland_mifs_ops redmi_g_ops = {
+	.name = "Redmi G",
+	.mode_map = &redmi_mode_map,
+	.check_performance_capability = redmi_pp_check_capability,
+	.has_full_speed = true,
+	.has_cpu_power = true,
+	.quirk_refresh_rate_toggle = true,
+};
+
+static const struct dmi_system_id bitland_mifs_dmi_table[] = {
+	{
+		/* Redmi G */
+		.matches = {
+			DMI_MATCH(DMI_BOARD_NAME, "TM2135"),
+		},
+		.driver_data = (void *)&redmi_g_ops,
+	},
+	{
+		.matches = {
+			DMI_MATCH(DMI_BOARD_NAME, "TM2137"),
+		},
+		.driver_data = (void *)&redmi_g_ops,
+	},
+	{
+		/* Redmi Book series */
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "Redmi"),
+		},
+		.driver_data = (void *)&redmi_book_ops,
+	},
+	{
+		/* Xiaomi Book series */
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "Xiaomi"),
+		},
+		.driver_data = (void *)&redmi_book_ops,
+	},
+	{}
+};
+
 enum bitland_mifs_event_id {
 	WMI_EVENT_RESERVED_1		= 1,
 	WMI_EVENT_RESERVED_2		= 2,
@@ -203,6 +267,8 @@ struct bitland_mifs_wmi_data {
 	struct input_dev *input_dev;
 	struct device *hwmon_dev;
 	struct device *pp_dev;
+	enum bitland_wmi_device_type dev_type;
+	u8 kbd_type;
 	const struct bitland_mifs_ops *ops;
 	enum platform_profile_option saved_profile;
 };
@@ -255,8 +321,10 @@ static int laptop_profile_get(struct device *dev,
 	else if (mode == map->balanced)
 		*profile = PLATFORM_PROFILE_BALANCED;
 	else if (mode == map->performance)
-		*profile = PLATFORM_PROFILE_BALANCED_PERFORMANCE;
-	else if (mode == map->full_speed)
+		*profile = data->ops->has_full_speed ?
+			   PLATFORM_PROFILE_BALANCED_PERFORMANCE :
+			   PLATFORM_PROFILE_PERFORMANCE;
+	else if (data->ops->has_full_speed && mode == map->full_speed)
 		*profile = PLATFORM_PROFILE_PERFORMANCE;
 	else
 		return -EINVAL;
@@ -273,10 +341,11 @@ static int bitland_pp_check_capability(struct bitland_mifs_wmi_data *data)
 	struct bitland_mifs_output output;
 	int ret;
 
-	/* Full-speed/performance mode requires DC power (not USB-C) */
+	/* The performance modes require AC power */
 	if (!power_supply_is_system_supplied())
 		return -EOPNOTSUPP;
 
+	/* Additionally require the barrel-jack power supply (not USB-C) */
 	ret = bitland_mifs_wmi_call(data, &input, &output);
 	if (ret)
 		return ret;
@@ -287,6 +356,15 @@ static int bitland_pp_check_capability(struct bitland_mifs_wmi_data *data)
 	return 0;
 }
 
+static int redmi_pp_check_capability(struct bitland_mifs_wmi_data *data)
+{
+	/* The performance modes require AC power (any source) */
+	if (!power_supply_is_system_supplied())
+		return -EOPNOTSUPP;
+
+	return 0;
+}
+
 static int laptop_profile_set(struct device *dev,
 			      enum platform_profile_option profile)
 {
@@ -309,6 +387,8 @@ static int laptop_profile_set(struct device *dev,
 		mode = map->balanced;
 		break;
 	case PLATFORM_PROFILE_BALANCED_PERFORMANCE:
+		if (!data->ops->has_full_speed)
+			return -EOPNOTSUPP;
 		ret = data->ops->check_performance_capability(data);
 		if (ret)
 			return ret;
@@ -318,7 +398,7 @@ static int laptop_profile_set(struct device *dev,
 		ret = data->ops->check_performance_capability(data);
 		if (ret)
 			return ret;
-		mode = map->full_speed;
+		mode = data->ops->has_full_speed ? map->full_speed : map->performance;
 		break;
 	default:
 		return -EOPNOTSUPP;
@@ -335,10 +415,10 @@ static int platform_profile_probe(void *drvdata, unsigned long *choices)
 
 	set_bit(PLATFORM_PROFILE_LOW_POWER, choices);
 	set_bit(PLATFORM_PROFILE_BALANCED, choices);
-	set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, choices);
+	set_bit(PLATFORM_PROFILE_PERFORMANCE, choices);
 
 	if (data->ops->has_full_speed)
-		set_bit(PLATFORM_PROFILE_PERFORMANCE, choices);
+		set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, choices);
 
 	return 0;
 }
@@ -418,6 +498,13 @@ static int laptop_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
 		default:
 			return -EINVAL;
 		}
+	case hwmon_power:
+		/* WMI_FN_CPU_POWER returns the CPU power draw in watts */
+		input.function = WMI_FN_CPU_POWER;
+		ret = bitland_mifs_wmi_call(data, &input, &res);
+		if (!ret)
+			*val = res.data[0] * MICROWATT_PER_WATT;
+		return ret;
 	default:
 		return -EINVAL;
 	}
@@ -444,6 +531,15 @@ static const struct hwmon_channel_info *laptop_hwmon_info[] = {
 	NULL
 };
 
+static const struct hwmon_channel_info *laptop_hwmon_info_power[] = {
+	HWMON_CHANNEL_INFO(temp, HWMON_T_INPUT),
+	HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT | HWMON_F_LABEL,
+			   HWMON_F_INPUT | HWMON_F_LABEL,
+			   HWMON_F_INPUT | HWMON_F_LABEL),
+	HWMON_CHANNEL_INFO(power, HWMON_P_INPUT),
+	NULL
+};
+
 static const struct hwmon_ops laptop_hwmon_ops = {
 	.visible = 0444,
 	.read = laptop_hwmon_read,
@@ -455,6 +551,11 @@ static const struct hwmon_chip_info laptop_chip_info = {
 	.info = laptop_hwmon_info,
 };
 
+static const struct hwmon_chip_info laptop_chip_info_power = {
+	.ops = &laptop_hwmon_ops,
+	.info = laptop_hwmon_info_power,
+};
+
 static int laptop_kbd_led_set(struct led_classdev *led_cdev,
 			      enum led_brightness value)
 {
@@ -498,6 +599,96 @@ static const char *const gpu_mode_strings[] = {
 	"uma",
 };
 
+static ssize_t fn_lock_show(struct device *dev, struct device_attribute *attr,
+			    char *buf)
+{
+	struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev);
+	struct bitland_mifs_input input = {
+		.reserved1 = 0,
+		.operation = WMI_METHOD_GET,
+		.reserved2 = 0,
+		.function = WMI_FN_FN_LOCK,
+	};
+	struct bitland_mifs_output res;
+	int ret;
+
+	ret = bitland_mifs_wmi_call(data, &input, &res);
+	if (ret)
+		return ret;
+
+	return sysfs_emit(buf, "%u\n", res.data[0]);
+}
+
+static ssize_t fn_lock_store(struct device *dev, struct device_attribute *attr,
+			     const char *buf, size_t count)
+{
+	struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev);
+	struct bitland_mifs_input input = {
+		.reserved1 = 0,
+		.operation = WMI_METHOD_SET,
+		.reserved2 = 0,
+		.function = WMI_FN_FN_LOCK,
+	};
+	bool val;
+	int ret;
+
+	if (kstrtobool(buf, &val))
+		return -EINVAL;
+
+	input.payload[0] = val;
+
+	ret = bitland_mifs_wmi_call(data, &input, NULL);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
+static ssize_t touchpad_lock_show(struct device *dev, struct device_attribute *attr,
+				  char *buf)
+{
+	struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev);
+	struct bitland_mifs_input input = {
+		.reserved1 = 0,
+		.operation = WMI_METHOD_GET,
+		.reserved2 = 0,
+		.function = WMI_FN_TP_LOCK,
+	};
+	struct bitland_mifs_output res;
+	int ret;
+
+	ret = bitland_mifs_wmi_call(data, &input, &res);
+	if (ret)
+		return ret;
+
+	return sysfs_emit(buf, "%u\n", res.data[0]);
+}
+
+static ssize_t touchpad_lock_store(struct device *dev, struct device_attribute *attr,
+				   const char *buf, size_t count)
+{
+	struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev);
+	struct bitland_mifs_input input = {
+		.reserved1 = 0,
+		.operation = WMI_METHOD_SET,
+		.reserved2 = 0,
+		.function = WMI_FN_TP_LOCK,
+	};
+	bool val;
+	int ret;
+
+	if (kstrtobool(buf, &val))
+		return -EINVAL;
+
+	input.payload[0] = val;
+
+	ret = bitland_mifs_wmi_call(data, &input, NULL);
+	if (ret)
+		return ret;
+
+	return count;
+}
+
 /* GPU Mode: 0:Hybrid, 1:Discrete, 2:UMA */
 static ssize_t gpu_mode_show(struct device *dev, struct device_attribute *attr,
 			     char *buf)
@@ -637,17 +828,46 @@ static ssize_t fan_boost_store(struct device *dev,
 	return count;
 }
 
-static const DEVICE_ATTR_RW(gpu_mode);
-static const DEVICE_ATTR_RW(kb_mode);
-static const DEVICE_ATTR_WO(fan_boost);
+static DEVICE_ATTR_RW(gpu_mode);
+static DEVICE_ATTR_RW(kb_mode);
+static DEVICE_ATTR_WO(fan_boost);
+static DEVICE_ATTR_RW(fn_lock);
+static DEVICE_ATTR_RW(touchpad_lock);
 
-static const struct attribute *const laptop_attrs[] = {
+static struct attribute *laptop_attrs[] = {
 	&dev_attr_gpu_mode.attr,
 	&dev_attr_kb_mode.attr,
 	&dev_attr_fan_boost.attr,
+	&dev_attr_fn_lock.attr,
+	&dev_attr_touchpad_lock.attr,
 	NULL,
 };
-ATTRIBUTE_GROUPS(laptop);
+
+static umode_t laptop_attr_is_visible(struct kobject *kobj,
+				      struct attribute *attr, int idx)
+{
+	struct device *dev = kobj_to_dev(kobj);
+	struct bitland_mifs_wmi_data *data = dev_get_drvdata(dev);
+
+	/* The sysfs attributes are only supported on the control device */
+	if (data->dev_type != BITLAND_WMI_CONTROL)
+		return 0;
+
+	/*
+	 * White-only backlit keyboards (e.g. Redmi Book series) do not
+	 * support the RGB backlight modes.
+	 */
+	if (attr == &dev_attr_kb_mode.attr &&
+	    data->kbd_type == WMI_KBD_TYPE_WHITE)
+		return 0;
+
+	return attr->mode;
+}
+
+static const struct attribute_group laptop_group = {
+	.attrs = laptop_attrs,
+	.is_visible = laptop_attr_is_visible,
+};
 
 static const struct key_entry bitland_mifs_wmi_keymap[] = {
 	{ KE_KEY, BI_HOTKEY_CODE(WMI_EVENT_OPEN_APP, 1, 0), { KEY_PROG1 } },
@@ -699,6 +919,34 @@ static const struct key_entry bitland_mifs_wmi_keymap[] = {
 	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_FNLOCK_STATE, 0, 0), {} },
 	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_FNLOCK_STATE, 1, 0), {} },
 
+	/* Airplane mode toggle (Redmi) */
+	{ KE_KEY, BI_HOTKEY_CODE(WMI_EVENT_AIRPLANE_MODE, 0, 0), { KEY_RFKILL } },
+	{ KE_KEY, BI_HOTKEY_CODE(WMI_EVENT_AIRPLANE_MODE, 1, 0), { KEY_RFKILL } },
+
+	/*
+	 * Touchpad state change (Redmi Fn+F7). The EC performs the actual
+	 * toggle itself; userspace can control the state via the
+	 * touchpad_lock sysfs attribute.
+	 */
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_TOUCHPAD_STATE, 0, 0), {} },
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_TOUCHPAD_STATE, 1, 0), {} },
+
+	/* Keyboard backlight mode change */
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_KBD_MODE, 0, 0), {} },
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_KBD_MODE, 1, 0), {} },
+
+	/* Lock-state indicator events */
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_CAPSLOCK_STATE, 0, 0), {} },
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_CAPSLOCK_STATE, 1, 0), {} },
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_NUMLOCK_STATE, 0, 0), {} },
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_NUMLOCK_STATE, 1, 0), {} },
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_SCROLLLOCK_STATE, 0, 0), {} },
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_SCROLLLOCK_STATE, 1, 0), {} },
+
+	/* Win key lock (Redmi gaming mode) */
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_WIN_KEY_LOCK, 0, 0), {} },
+	{ KE_IGNORE, BI_HOTKEY_CODE(WMI_EVENT_WIN_KEY_LOCK, 1, 0), {} },
+
 	/* Fn+`/1/2/3/4 */
 	{ KE_KEY, BI_HOTKEY_CODE(WMI_EVENT_FN_F, 1, 0), { KEY_F13 } },
 	{ KE_KEY, BI_HOTKEY_CODE(WMI_EVENT_FN_0, 1, 0), { KEY_F14 } },
@@ -744,6 +992,36 @@ static int bitland_notifier_callback(struct notifier_block *nb,
 	return NOTIFY_OK;
 }
 
+static int bitland_detect_kbd_type(struct bitland_mifs_wmi_data *data)
+{
+	struct bitland_mifs_input input = {
+		.reserved1 = 0,
+		.operation = WMI_METHOD_GET,
+		.reserved2 = 0,
+		.function = WMI_FN_KBD_TYPE,
+	};
+	struct bitland_mifs_output res;
+	int ret;
+
+	ret = bitland_mifs_wmi_call(data, &input, &res);
+	if (ret)
+		return ret;
+
+	data->kbd_type = res.data[0];
+	return 0;
+}
+
+static void bitland_detect_ops(struct bitland_mifs_wmi_data *data)
+{
+	const struct dmi_system_id *id;
+
+	id = dmi_first_match(bitland_mifs_dmi_table);
+	if (id)
+		data->ops = id->driver_data;
+
+	dev_dbg(&data->wdev->dev, "Detected machine ops: %s\n", data->ops->name);
+}
+
 static int bitland_mifs_wmi_probe(struct wmi_device *wdev, const void *context)
 {
 	struct bitland_mifs_wmi_data *drv_data;
@@ -761,6 +1039,9 @@ static int bitland_mifs_wmi_probe(struct wmi_device *wdev, const void *context)
 		return -ENOMEM;
 
 	drv_data->wdev = wdev;
+	drv_data->dev_type = dev_type;
+	/* Default to RGB so the RGB attributes stay visible if detection fails */
+	drv_data->kbd_type = WMI_KBD_TYPE_RGB;
 	drv_data->ops = &bitland_ops;
 
 	ret = devm_mutex_init(&wdev->dev, &drv_data->lock);
@@ -769,6 +1050,8 @@ static int bitland_mifs_wmi_probe(struct wmi_device *wdev, const void *context)
 
 	dev_set_drvdata(&wdev->dev, drv_data);
 
+	bitland_detect_ops(drv_data);
+
 	if (dev_type == BITLAND_WMI_EVENT) {
 		/* Register input device for hotkeys */
 		drv_data->input_dev = devm_input_allocate_device(&wdev->dev);
@@ -794,12 +1077,28 @@ static int bitland_mifs_wmi_probe(struct wmi_device *wdev, const void *context)
 	if (IS_ERR(drv_data->pp_dev))
 		return PTR_ERR(drv_data->pp_dev);
 
+	/* Detect the keyboard type */
+	ret = bitland_detect_kbd_type(drv_data);
+	if (ret)
+		dev_dbg(&wdev->dev, "Failed to detect keyboard type, assuming RGB: %d\n", ret);
+
+	ret = devm_device_add_group(&wdev->dev, &laptop_group);
+	if (ret)
+		return ret;
+
 	/* Register hwmon */
-	drv_data->hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev,
-								   "bitland_mifs",
-								   drv_data,
-								   &laptop_chip_info,
-								   NULL);
+	if (drv_data->ops->has_cpu_power)
+		drv_data->hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev,
+									   "bitland_mifs",
+									   drv_data,
+									   &laptop_chip_info_power,
+									   NULL);
+	else
+		drv_data->hwmon_dev = devm_hwmon_device_register_with_info(&wdev->dev,
+									   "bitland_mifs",
+									   drv_data,
+									   &laptop_chip_info,
+									   NULL);
 	if (IS_ERR(drv_data->hwmon_dev))
 		return PTR_ERR(drv_data->hwmon_dev);
 
@@ -851,6 +1150,7 @@ static void bitland_mifs_wmi_report_key(struct wmi_device *wdev, u32 payload)
 static void bitland_mifs_wmi_notify(struct wmi_device *wdev,
 				    const struct wmi_buffer *buffer)
 {
+	struct bitland_mifs_wmi_data *data = dev_get_drvdata(&wdev->dev);
 	const struct bitland_mifs_event *event = buffer->data;
 	struct bitland_fan_notify_data fan_data;
 	u32 payload;
@@ -885,7 +1185,8 @@ static void bitland_mifs_wmi_notify(struct wmi_device *wdev,
 	case WMI_EVENT_CPU_FAN_SPEED:
 	case WMI_EVENT_GPU_FAN_SPEED:
 		/* Redmi refresh rate toggle quirk */
-		if (event->event_id == WMI_EVENT_CPU_FAN_SPEED &&
+		if (data->ops->quirk_refresh_rate_toggle &&
+		    event->event_id == WMI_EVENT_CPU_FAN_SPEED &&
 		    event->value_low == 0 && event->value_high == 0) {
 			payload = BI_HOTKEY_CODE(WMI_EVENT_REFRESH_RATE, 0, 0);
 			bitland_mifs_wmi_report_key(wdev, payload);
@@ -923,7 +1224,6 @@ static struct wmi_driver bitland_mifs_wmi_driver = {
 	.no_singleton = true,
 	.driver = {
 		.name = DRV_NAME,
-		.dev_groups = laptop_groups,
 		.pm = pm_sleep_ptr(&bitland_mifs_wmi_pm_ops),
 	},
 	.id_table = bitland_mifs_wmi_id_table,
-- 
2.55.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.