[PATCH v2] platform/x86: ideapad-laptop: Report camera switch as SW_CAMERA_LENS_COVER
From: Marco Giunta
Date: Sun Sep 20 2026 - 16:28:39 EST
On certain 2025 Lenovo laptops, such as the Yoga Pro 7 14ASP10 and Legion
Pro 7 16AFR10H, a switch on the side disables the camera. This is handled
by the firmware, which also sends event 0x0d (camera disabled) or 0x0c
(camera enabled) through the WMI event GUID handled with
ideapad_wmi_context_fn_keys. These events are currently reported as
KEY_UNKNOWN.
Other Lenovo laptops expose a similar camera switch through a separate WMI
GUID, which lenovo-wmi-camera reports as SW_CAMERA_LENS_COVER since
commit d98bf6a6ed61 ("platform/x86: lenovo-wmi-camera: Use
SW_CAMERA_LENS_COVER instead of KEY_CAMERA_ACESS"). That driver does not
bind on these laptops, so do the same here: as the switch state cannot be
read from the firmware, register a separate input device with the switch
on the first event, and report the state carried by each event.
Signed-off-by: Marco Giunta <marco_giunta@xxxxxxxxxx>
---
Changes in v2:
* Add lockdep_assert_held(&ideapad_shared_mutex) and a comment at the top
of ideapad_camera_switch_report() to document and check that the WMI
notify path is serialized through ideapad_shared_mutex (suggested by
Huang Wei)
* Replace dev_warn() with dev_warn_once() in
ideapad_camera_switch_report() so that a persistent registration failure
does not log a warning on every toggle (suggested by Huang Wei)
Link to v1: https://lore.kernel.org/platform-driver-x86/SN6PR19MB23039910DD1918BEE3825BAFFC862@xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/
Notes (copied verbatim from v1, to keep the context easily accessible):
* I considered a couple of alternatives to the approach in this patch:
1. Simply map 0x0c and 0x0d to KEY_CAMERA_ACCESS_ENABLE/DISABLE:
{ KE_KEY, 0x0c | IDEAPAD_WMI_KEY, { KEY_CAMERA_ACCESS_ENABLE } },
{ KE_KEY, 0x0d | IDEAPAD_WMI_KEY, { KEY_CAMERA_ACCESS_DISABLE } },
similar to the touchpad off/on hkey events (66/67) this driver emits
after the firmware has toggled the touchpad state. However, HUTRR72
describes these hkey events as asserting a camera access state for
the host to apply, rather than reporting one the firmware has
already applied. Furthermore, lenovo-wmi-camera moved away from
these hkey events in favor of SW_CAMERA_LENS_COVER in the quoted
commit, which is arguably the closest relevant precedent.
2. Ignore these events like the firmware-handled FnLock events:
{ KE_IGNORE, 0x0c | IDEAPAD_WMI_KEY },
{ KE_IGNORE, 0x0d | IDEAPAD_WMI_KEY },
This fixes the KEY_UNKNOWN issue, but userspace gets no information.
Overall, in the end I copied the approach of the lenovo-wmi-camera
driver. Kindly let me know if there are other/better solutions.
* Unlike the touchpad, whose state this driver reads with VPCCMD_R_TOUCHPAD
at probe, on resume and on each event, the camera switch state is only
available in EC-private fields, and the two tested laptops even use
different ones. Hence the lenovo-wmi-camera approach: the switch appears
on the first event, and a change made while suspended is not reported,
leaving the state wrong until the switch is toggled again.
* For reference, on KDE Plasma 6.7.5, using the Legion's touchpad toggle
button shows an OSD, but nothing on a SW_CAMERA_LENS_COVER change or if
sending KEY_CAMERA_ACCESS_ENABLE/DISABLE events. I am not sure whether
this is simply a case of "not implemented yet" or if I misunderstood how
these events are supposed to be used to communicate with userspace.
Please feel free to correct me here.
Best regards,
Marco
---
drivers/platform/x86/lenovo/ideapad-laptop.c | 59 ++++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/drivers/platform/x86/lenovo/ideapad-laptop.c b/drivers/platform/x86/lenovo/ideapad-laptop.c
index 207368f5d489..0066638ee332 100644
--- a/drivers/platform/x86/lenovo/ideapad-laptop.c
+++ b/drivers/platform/x86/lenovo/ideapad-laptop.c
@@ -178,6 +178,7 @@ struct ideapad_private {
struct ideapad_rfk_priv rfk_priv[IDEAPAD_RFKILL_DEV_NUM];
struct platform_device *platform_device;
struct input_dev *inputdev;
+ struct input_dev *camera_switch_idev;
struct backlight_device *blightdev;
struct ideapad_dytc_priv *dytc;
struct dentry *debug;
@@ -1410,10 +1411,43 @@ static int ideapad_input_init(struct ideapad_private *priv)
return err;
}
+static int ideapad_camera_switch_init(struct ideapad_private *priv, bool covered)
+{
+ struct input_dev *idev;
+ int err;
+
+ idev = input_allocate_device();
+ if (!idev)
+ return -ENOMEM;
+
+ idev->name = "Ideapad Camera Switch";
+ idev->phys = "ideapad/input1";
+ idev->id.bustype = BUS_HOST;
+ idev->dev.parent = &priv->platform_device->dev;
+
+ input_set_capability(idev, EV_SW, SW_CAMERA_LENS_COVER);
+ input_report_switch(idev, SW_CAMERA_LENS_COVER, covered);
+ input_sync(idev);
+
+ err = input_register_device(idev);
+ if (err) {
+ input_free_device(idev);
+ return err;
+ }
+
+ priv->camera_switch_idev = idev;
+ return 0;
+}
+
static void ideapad_input_exit(struct ideapad_private *priv)
{
input_unregister_device(priv->inputdev);
priv->inputdev = NULL;
+
+ if (priv->camera_switch_idev) {
+ input_unregister_device(priv->camera_switch_idev);
+ priv->camera_switch_idev = NULL;
+ }
}
static void ideapad_input_report(struct ideapad_private *priv,
@@ -1422,6 +1456,22 @@ static void ideapad_input_report(struct ideapad_private *priv,
sparse_keymap_report_event(priv->inputdev, scancode, 1, true);
}
+static void ideapad_camera_switch_report(struct ideapad_private *priv, bool covered)
+{
+ /* The WMI notify path is serialized through ideapad_shared_mutex */
+ lockdep_assert_held(&ideapad_shared_mutex);
+
+ if (!priv->camera_switch_idev) {
+ if (ideapad_camera_switch_init(priv, covered))
+ dev_warn_once(&priv->platform_device->dev,
+ "Failed to register camera switch input device\n");
+ return;
+ }
+
+ input_report_switch(priv->camera_switch_idev, SW_CAMERA_LENS_COVER, covered);
+ input_sync(priv->camera_switch_idev);
+}
+
static void ideapad_input_novokey(struct ideapad_private *priv)
{
unsigned long long_pressed;
@@ -2312,6 +2362,15 @@ static void ideapad_wmi_notify(struct wmi_device *wdev, union acpi_object *data)
break;
}
+ /*
+ * Camera switch, handled by the firmware:
+ * 0x0c camera enabled, 0x0d camera disabled
+ */
+ if (data->integer.value == 0x0c || data->integer.value == 0x0d) {
+ ideapad_camera_switch_report(priv, data->integer.value == 0x0d);
+ break;
+ }
+
/* 0x02 FnLock, 0x03 Esc */
if (data->integer.value == 0x02 || data->integer.value == 0x03)
ideapad_fn_lock_led_notify(priv, data->integer.value == 0x02);
base-commit: f475845eaf3d749114a63270bf2efea459e14dd2
--
2.55.0