All of lore.kernel.org
 help / color / mirror / Atom feed
From: Torstein Eide <torsteine+linux@gmail.com>
To: Jiri Kosina <jikos@kernel.org>, Benjamin Tissoires <bentiss@kernel.org>
Cc: Hans de Goede <hansg@kernel.org>,
	Maoyi Xie <maoyixie.tju@gmail.com>,
	linux-input@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [PATCH v2 2/2] HID: lg-g15: report Super-key-lock state on G15 v2's keyboard interface
Date: Mon,  9 Nov 2026 18:17:27 +0100	[thread overview]
Message-ID: <20261109171727.2812614-3-torsteine+linux@gmail.com> (raw)
In-Reply-To: <20261109171727.2812614-1-torsteine+linux@gmail.com>

The G15 v2 exposes a second USB HID interface (046D:C226) previously
left to hid-generic, which reports the Super-key-lock ("Game Mode")
state on report 3. Bind it as LG_G15_V2_KBD: keep default hid-input
behavior for normal typing, and additionally decode report 3 to expose
the lock state as a read-only g15::game_mode LED.

Tested on Logitech G15v2.

Signed-off-by: Torstein Eide <torsteine+linux@gmail.com>
Assisted-by: Claude:claude-sonnet-5
---
 drivers/hid/hid-ids.h    |   1 +
 drivers/hid/hid-lg-g15.c | 230 ++++++++++++++++++++++++++++++++++++++-
 2 files changed, 227 insertions(+), 4 deletions(-)

diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 91319aa6516d..44ebf2a0accc 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -950,6 +950,7 @@
 #define USB_DEVICE_ID_LOGITECH_G13		0xc21c
 #define USB_DEVICE_ID_LOGITECH_G15_LCD		0xc222
 #define USB_DEVICE_ID_LOGITECH_G11		0xc225
+#define USB_DEVICE_ID_LOGITECH_G15_V2_KBD	0xc226
 #define USB_DEVICE_ID_LOGITECH_G15_V2_LCD	0xc227
 #define USB_DEVICE_ID_LOGITECH_G510		0xc22d
 #define USB_DEVICE_ID_LOGITECH_G510_USB_AUDIO	0xc22e
diff --git a/drivers/hid/hid-lg-g15.c b/drivers/hid/hid-lg-g15.c
index 3827c66ca3a9..998023ad272d 100644
--- a/drivers/hid/hid-lg-g15.c
+++ b/drivers/hid/hid-lg-g15.c
@@ -12,6 +12,7 @@
 #include <linux/module.h>
 #include <linux/random.h>
 #include <linux/sched.h>
+#include <linux/spinlock.h>
 #include <linux/usb.h>
 #include <linux/wait.h>
 #include <dt-bindings/leds/common.h>
@@ -45,7 +46,8 @@
 enum lg_g15_model {
 	LG_G13,
 	LG_G15,
-	LG_G15_V2,
+	LG_G15_V2,	/* 046D:C227, GamePanel LCD iface */
+	LG_G15_V2_KBD,	/* 046D:C226, keyboard/consumer-ctrl iface, not the LCD */
 	LG_G510,
 	LG_G510_USB_AUDIO,
 	LG_Z10,
@@ -104,6 +106,19 @@ struct lg_g15_data {
 	bool lcd_contrast_set;		/* LCD contrast written at least once */
 };
 
+/*
+ * State for the G15 v2's separate keyboard + consumer-control USB device
+ * (046D:C226). Unlike lg_g15_data above, this is not the GamePanel LCD
+ * interface: it carries normal typing (left to generic hid-input) plus one
+ * extra report we care about, so it only needs a small amount of state.
+ */
+struct lg_g15_kbd_data {
+	struct led_classdev led;	/* g15::game_mode, read-only */
+	spinlock_t lock;		/* Protects super_lock and events */
+	bool super_lock;		/* Cached HW state of the Super/Windows-key lock */
+	unsigned int events;		/* Report 3 input reports seen so far */
+};
+
 /********* G13 LED functions ***********/
 /*
  * G13 retains no state across power cycles, and always powers up with the backlight on,
@@ -699,6 +714,8 @@ static int lg_g15_get_initial_led_brightness(struct lg_g15_data *g15)
 		 * the LCD and touch keys part of the speakers.
 		 */
 		return 0;
+	case LG_G15_V2_KBD:
+		break; /* Never reached, this model has no lg_g15_data */
 	}
 	return -EINVAL; /* Never reached */
 }
@@ -963,11 +980,173 @@ static int lg_g510_leds_event(struct lg_g15_data *g15, u8 *data)
 	return 0;
 }
 
+/********* G15 v2 keyboard interface functions *********/
+
+/* Report 3, byte 1 bit 0: Super/Windows key lock state ("Game Mode") */
+#define LG_G15_V2_KBD_GAME_MODE_REPORT	0x03
+
+/*
+ * The G15 v2's keyboard device uses struct lg_g15_kbd_data as drvdata, not
+ * struct lg_g15_data. This is the one check used by every entry point to
+ * tell the two apart.
+ */
+static bool lg_g15_is_v2_kbd(struct hid_device *hdev)
+{
+	return hdev->vendor == USB_VENDOR_ID_LOGITECH &&
+	       hdev->product == USB_DEVICE_ID_LOGITECH_G15_V2_KBD;
+}
+
+static enum led_brightness lg_g15_v2_kbd_super_lock_get(struct led_classdev *led_cdev)
+{
+	struct lg_g15_kbd_data *kbd = container_of(led_cdev, struct lg_g15_kbd_data, led);
+
+	return kbd->super_lock;
+}
+
+/* Must be called with kbd->lock held */
+static void lg_g15_v2_kbd_set_super_lock(struct lg_g15_kbd_data *kbd, bool super_lock)
+{
+	if (super_lock == kbd->super_lock)
+		return;
+
+	kbd->super_lock = super_lock;
+	led_classdev_notify_brightness_hw_changed(&kbd->led, super_lock);
+}
+
+/* Fetches the current lock state, which may already be on at probe/resume. */
+static int lg_g15_v2_kbd_get_super_lock(struct hid_device *hdev, bool *super_lock)
+{
+	u8 *buf;
+	int ret;
+
+	buf = kzalloc(2, GFP_KERNEL); /* DMA-able, not on the stack */
+	if (!buf)
+		return -ENOMEM;
+
+	ret = hid_hw_raw_request(hdev, LG_G15_V2_KBD_GAME_MODE_REPORT, buf, 2,
+				 HID_INPUT_REPORT, HID_REQ_GET_REPORT);
+	if (ret == 2 && buf[0] == LG_G15_V2_KBD_GAME_MODE_REPORT) {
+		*super_lock = buf[1] & 0x01;
+		ret = 0;
+	} else {
+		hid_dbg(hdev, "Error reading Super-key-lock state: %d\n", ret);
+		ret = (ret < 0) ? ret : -EIO;
+	}
+
+	kfree(buf);
+	return ret;
+}
+
+/*
+ * Re-reads the lock state on resume. Input reports keep arriving while the
+ * GET_REPORT is in flight; if one did, it is newer than what we read, so
+ * keep the state it set.
+ */
+static void lg_g15_v2_kbd_refresh_super_lock(struct hid_device *hdev,
+					     struct lg_g15_kbd_data *kbd)
+{
+	unsigned long flags;
+	unsigned int events;
+	bool super_lock;
+
+	spin_lock_irqsave(&kbd->lock, flags);
+	events = kbd->events;
+	spin_unlock_irqrestore(&kbd->lock, flags);
+
+	if (lg_g15_v2_kbd_get_super_lock(hdev, &super_lock))
+		return;
+
+	spin_lock_irqsave(&kbd->lock, flags);
+	if (kbd->events == events)
+		lg_g15_v2_kbd_set_super_lock(kbd, super_lock);
+	spin_unlock_irqrestore(&kbd->lock, flags);
+}
+
+/* Decodes report 3 and mirrors the Super-key-lock bit into the game_mode LED. */
+static int lg_g15_v2_kbd_raw_event(struct hid_device *hdev, u8 *data, int size)
+{
+	struct lg_g15_kbd_data *kbd = hid_get_drvdata(hdev);
+	unsigned long flags;
+
+	if (!kbd || data[0] != LG_G15_V2_KBD_GAME_MODE_REPORT || size != 2)
+		return 0; /* Not our report, or the non-consumer-control interface */
+
+	spin_lock_irqsave(&kbd->lock, flags);
+	kbd->events++;
+	lg_g15_v2_kbd_set_super_lock(kbd, data[1] & 0x01);
+	spin_unlock_irqrestore(&kbd->lock, flags);
+
+	return 0;
+}
+
+/*
+ * The G15 v2's keyboard USB device exposes two HID interfaces of its own
+ * (boot keyboard + consumer control); both match this same USB product ID,
+ * so this probe runs once per interface. Only the consumer-control one
+ * carries report 3, so the LED is only registered there; the other gets
+ * plain generic hid-input and nothing else, same as if hid-generic had
+ * bound it.
+ */
+static int lg_g15_v2_kbd_probe(struct hid_device *hdev)
+{
+	struct hid_report_enum *rep_enum;
+	struct hid_report *rep;
+	bool has_game_mode_report = false;
+	struct lg_g15_kbd_data *kbd;
+	int ret;
+
+	hdev->quirks |= HID_QUIRK_INPUT_PER_APP; /* As hid-generic does */
+
+	ret = hid_parse(hdev);
+	if (ret)
+		return ret;
+
+	rep_enum = &hdev->report_enum[HID_INPUT_REPORT];
+	list_for_each_entry(rep, &rep_enum->report_list, list) {
+		if (rep->id == LG_G15_V2_KBD_GAME_MODE_REPORT)
+			has_game_mode_report = true; /* This is the consumer-control interface */
+	}
+
+	ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); /* Plain generic keyboard input */
+	if (ret)
+		return ret;
+
+	if (!has_game_mode_report)
+		return 0; /* Boot-keyboard interface, nothing more to do */
+
+	kbd = devm_kzalloc(&hdev->dev, sizeof(*kbd), GFP_KERNEL);
+	if (!kbd)
+		return 0; /* Non-fatal, keyboard input still works without the LED */
+
+	spin_lock_init(&kbd->lock);
+
+	/* Register the LED with the current state, not a default of off */
+	lg_g15_v2_kbd_get_super_lock(hdev, &kbd->super_lock);
+
+	kbd->led.name = "g15::game_mode";
+	kbd->led.max_brightness = 1;
+	kbd->led.brightness_get = lg_g15_v2_kbd_super_lock_get;
+	kbd->led.flags = LED_BRIGHT_HW_CHANGED; /* Enables the hw_changed notify below */
+
+	ret = devm_led_classdev_register(&hdev->dev, &kbd->led);
+	if (ret)
+		return 0; /* Non-fatal, keyboard input still works without the LED */
+
+	/* hid-core does not deliver input reports until probe() returns */
+	hid_set_drvdata(hdev, kbd);
+
+	return 0;
+}
+
 static int lg_g15_raw_event(struct hid_device *hdev, struct hid_report *report,
 			    u8 *data, int size)
 {
-	struct lg_g15_data *g15 = hid_get_drvdata(hdev);
+	struct lg_g15_data *g15;
+
+	if (lg_g15_is_v2_kbd(hdev)) /* Separate kbd/consumer-ctrl dev */
+		return lg_g15_v2_kbd_raw_event(hdev, data, size);
 
+	g15 = hid_get_drvdata(hdev);
 	if (!g15)
 		return 0;
 
@@ -997,6 +1176,8 @@ static int lg_g15_raw_event(struct hid_device *hdev, struct hid_report *report,
 		if (data[0] == LG_G510_INPUT_KBD_BACKLIGHT && size == 2)
 			return lg_g510_leds_event(g15, data);
 		break;
+	case LG_G15_V2_KBD:
+		break; /* Never reached, handled via lg_g15_is_v2_kbd() above */
 	}
 
 	return 0;
@@ -1135,6 +1316,9 @@ static int lg_g15_register_led(struct lg_g15_data *g15, int i, const char *name)
 			ret = devm_led_classdev_register(&g15->hdev->dev, &g15->leds[i].cdev);
 		}
 		break;
+	case LG_G15_V2_KBD:
+		ret = -EINVAL; /* Never reached, this model has no lg_g15_data LEDs */
+		break;
 	}
 
 	return ret;
@@ -1210,6 +1394,12 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
 	struct hid_report *rep;
 	int ret, i, gkeys = 0;
 
+	if (lg_g15_is_v2_kbd(hdev)) /* Not the GamePanel LCD interface */
+		return lg_g15_v2_kbd_probe(hdev);
+
+	if (id->driver_data == LG_G15_V2_KBD) /* e.g. a mismatching new_id */
+		return -ENODEV;
+
 	hdev->quirks |= HID_QUIRK_INPUT_PER_APP;
 
 	ret = hid_parse(hdev);
@@ -1292,6 +1482,8 @@ static int lg_g15_probe(struct hid_device *hdev, const struct hid_device_id *id)
 	case LG_Z10:
 		connect_mask = HID_CONNECT_HIDRAW;
 		break;
+	case LG_G15_V2_KBD:
+		break; /* Never reached, rejected above */
 	}
 
 	ret = hid_hw_start(hdev, connect_mask);
@@ -1446,6 +1638,16 @@ static const struct hid_device_id lg_g15_devices[] = {
 	{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
 			 USB_DEVICE_ID_LOGITECH_G15_V2_LCD),
 		.driver_data = LG_G15_V2 },
+	/*
+	 * The G15 v2's keyboard + consumer-control USB device (a separate
+	 * device from the GamePanel LCD one above, on the keyboard's builtin
+	 * hub). Normal typing is handled by generic hid-input; we only bind
+	 * this to also expose the Super/Windows-key-lock ("Game Mode") state,
+	 * see lg_g15_v2_kbd_probe().
+	 */
+	{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
+			 USB_DEVICE_ID_LOGITECH_G15_V2_KBD),
+		.driver_data = LG_G15_V2_KBD },
 	/* G510 without a headset plugged in */
 	{ HID_USB_DEVICE(USB_VENDOR_ID_LOGITECH,
 			 USB_DEVICE_ID_LOGITECH_G510),
@@ -1464,7 +1666,18 @@ MODULE_DEVICE_TABLE(hid, lg_g15_devices);
 
 static void lg_g15_remove(struct hid_device *hdev)
 {
-	struct lg_g15_data *g15 = hid_get_drvdata(hdev);
+	struct lg_g15_data *g15;
+
+	/*
+	 * The G15 v2's keyboard device uses struct lg_g15_kbd_data, not
+	 * lg_g15_data below; it has nothing to clean up beyond hid_hw_stop().
+	 */
+	if (lg_g15_is_v2_kbd(hdev)) {
+		hid_hw_stop(hdev);
+		return;
+	}
+
+	g15 = hid_get_drvdata(hdev);
 
 	/*
 	 * g15->work is only initialized for the models that schedule it
@@ -1483,9 +1696,18 @@ static void lg_g15_remove(struct hid_device *hdev)
  */
 static int lg_g15_resume(struct hid_device *hdev)
 {
-	struct lg_g15_data *g15 = hid_get_drvdata(hdev);
+	struct lg_g15_data *g15;
 	struct lg_g15_led *contrast;
 
+	if (lg_g15_is_v2_kbd(hdev)) {
+		struct lg_g15_kbd_data *kbd = hid_get_drvdata(hdev);
+
+		if (kbd)
+			lg_g15_v2_kbd_refresh_super_lock(hdev, kbd);
+		return 0;
+	}
+
+	g15 = hid_get_drvdata(hdev);
 	if (!g15)
 		return 0;
 
-- 
2.53.0


      parent reply	other threads:[~2026-11-09 17:17 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-09 17:17 [PATCH v2 0/2] HID: lg-g15: G15 v2 contrast and game-mode LEDs Torstein Eide
2026-11-09 17:17 ` [PATCH v2 1/2] HID: lg-g15: add LCD contrast LED Torstein Eide
2026-11-09 17:17 ` Torstein Eide [this message]

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=20261109171727.2812614-3-torsteine+linux@gmail.com \
    --to=torsteine+linux@gmail.com \
    --cc=bentiss@kernel.org \
    --cc=hansg@kernel.org \
    --cc=jikos@kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=maoyixie.tju@gmail.com \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.