Devicetree
 help / color / mirror / Atom feed
From: wyibo6109@gmail.com
To: Dmitry Torokhov <dmitry.torokhov@gmail.com>, linux-input@vger.kernel.org
Cc: Rob Herring <robh@kernel.org>,
	Krzysztof Kozlowski <krzk+dt@kernel.org>,
	Conor Dooley <conor+dt@kernel.org>,
	devicetree@vger.kernel.org, linux-kernel@vger.kernel.org,
	Yibo Wang <wyibo6109@gmail.com>
Subject: [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver
Date: Fri,  9 Oct 2026 07:20:46 -0500	[thread overview]
Message-ID: <20261009122046.551915-3-wyibo6109@gmail.com> (raw)
In-Reply-To: <20261009122046.551915-1-wyibo6109@gmail.com>

From: Yibo Wang <wyibo6109@gmail.com>

Clean-room serdev driver for the Tinno keyboard MCU used by the OnePlus
Pad 3 Pro / Pad 4 and OPPO Pad 5 Pro smart keyboards, replacing the
vendor platform driver that depends on a downstream msm_geni_serial
callback and a user-space held tty fd.

The MCU speaks a framed "one wire bus" protocol over a half-duplex
921600 8N1 UART: 8x 0x55 preamble, 0xF1, src/dst address, cmd, len,
payload, CRC16-IBM (poly 0x8005, init 0xC596), 0xFE, 4x 0xAA.  Host TX is
gated onto the shared wire by a PMIC GPIO for the duration of a frame
(450us before, 300us after) and every host frame is echoed back on RX.

Supported: attach detection via the wake GPIO + 100ms heartbeat timeout,
touchpad, function/URL keys and the keyboard backlight exposed as a
"pogo-keyboard::kbd_backlight" LED class device (max 100, the range the
MCU accepts) so UPower and the desktop keyboard-backlight controls drive
it through the standard interface.  The backlight level is cached while
detached and restored on re-attach.

Two robustness fixes are folded in from bring-up:

1. The MCU keeps sending the plug-in sync frame (0x2F/0x01) every 100 ms
   until the host reports its wake state (0x3A/0x02); only then does it
   switch to regular heartbeats (0x2F/0x05).  Treat a repeated plug-in
   frame as a re-plug only once heartbeats have been seen, and send the
   wake notification on attach, otherwise the LED re-send loop at 10 Hz
   collides with the MCU and drops the link a few minutes after boot.

2. After a heartbeat timeout the detect path sampled the wake line once;
   it now re-arms detection properly so a working keyboard is picked up
   again without a re-plug.

DT binding for the "oneplus,pogo-keyboard" device is added along with a
MAINTAINERS entry.

Signed-off-by: Yibo Wang <wyibo6109@gmail.com>
---
 drivers/input/keyboard/Kconfig            |   11 +
 drivers/input/keyboard/Makefile           |    1 +
 drivers/input/keyboard/oneplus-pogo-kbd.c | 1109 +++++++++++++++++++++
 3 files changed, 1121 insertions(+)
 create mode 100644 drivers/input/keyboard/oneplus-pogo-kbd.c

diff --git a/drivers/input/keyboard/Kconfig b/drivers/input/keyboard/Kconfig
index 9d1019ba0..ced2a2174 100644
--- a/drivers/input/keyboard/Kconfig
+++ b/drivers/input/keyboard/Kconfig
@@ -806,4 +806,15 @@ config KEYBOARD_CYPRESS_SF
 	  To compile this driver as a module, choose M here: the
 	  module will be called cypress-sf.
 
+config KEYBOARD_ONEPLUS_POGO
+	tristate "OnePlus/OPPO Pad pogo-pin keyboard"
+	depends on SERIAL_DEV_BUS && GPIOLIB
+	help
+	  Say Y here to enable the magnetic pogo-pin keyboard with touchpad
+	  used by OnePlus Pad 3 Pro / Pad 4 and OPPO Pad 5 Pro.  The keyboard
+	  MCU talks a framed protocol over a half-duplex UART.
+
+	  To compile this driver as a module, choose M here: the
+	  module will be called oneplus-pogo-kbd.
+
 endif
diff --git a/drivers/input/keyboard/Makefile b/drivers/input/keyboard/Makefile
index 60bb7baf8..3554c5011 100644
--- a/drivers/input/keyboard/Makefile
+++ b/drivers/input/keyboard/Makefile
@@ -50,6 +50,7 @@ obj-$(CONFIG_KEYBOARD_NEWTON)		+= newtonkbd.o
 obj-$(CONFIG_KEYBOARD_NSPIRE)		+= nspire-keypad.o
 obj-$(CONFIG_KEYBOARD_OMAP)		+= omap-keypad.o
 obj-$(CONFIG_KEYBOARD_OMAP4)		+= omap4-keypad.o
+obj-$(CONFIG_KEYBOARD_ONEPLUS_POGO)	+= oneplus-pogo-kbd.o
 obj-$(CONFIG_KEYBOARD_OPENCORES)	+= opencores-kbd.o
 obj-$(CONFIG_KEYBOARD_PINEPHONE)	+= pinephone-keyboard.o
 obj-$(CONFIG_KEYBOARD_PMIC8XXX)		+= pmic8xxx-keypad.o
diff --git a/drivers/input/keyboard/oneplus-pogo-kbd.c b/drivers/input/keyboard/oneplus-pogo-kbd.c
new file mode 100644
index 000000000..589f31ece
--- /dev/null
+++ b/drivers/input/keyboard/oneplus-pogo-kbd.c
@@ -0,0 +1,1109 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * OnePlus / OPPO Pad pogo-pin keyboard driver (Tinno keyboard MCU)
+ *
+ * The keyboard hangs off a half-duplex UART (921600 8N1) carried over the
+ * pogo pins: a single data wire is shared by the host TX and the keyboard
+ * TX.  The host TX is gated onto the wire by a PMIC GPIO which is only
+ * asserted while a frame is being sent; every host frame is therefore
+ * echoed back on RX.  The same wire is also routed to a plain GPIO that is
+ * used for attach detection while the keyboard is unpowered.
+ *
+ * Frame format ("one wire bus"):
+ *
+ *   55 x8 | F1 | src | dst | cmd | len | payload[len] | crc_hi crc_lo | FE | AA x4
+ *
+ * src/dst are 0xA1 (keyboard) and 0xA2 (pad).  CRC16 uses the IBM polynomial
+ * 0x8005 (MSB first, init 0xC596) over F1 .. payload.  Replies carry cmd + 1.
+ *
+ * Unsolicited keyboard frames: 0x01 keys (HID boot report), 0x02 consumer
+ * keys (two LE16 HID usages), 0x03 touchpad, 0x2F sync/heartbeat (~100 ms).
+ */
+
+#include <linux/bitops.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/input.h>
+#include <linux/input/mt.h>
+#include <linux/interrupt.h>
+#include <linux/jiffies.h>
+#include <linux/kernel.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/property.h>
+#include <linux/serdev.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/sysfs.h>
+#include <linux/unaligned.h>
+#include <linux/workqueue.h>
+
+#define POGO_BAUD		921600
+
+#define OWB_HEAD_SYNC		0x55
+#define OWB_TAIL_SYNC		0xAA
+#define OWB_START		0xF1
+#define OWB_START_REPEAT	0xF2
+#define OWB_END			0xFE
+#define OWB_ADDR_KBD		0xA1
+#define OWB_ADDR_PAD		0xA2
+
+#define OWB_HEAD_SYNC_LEN	8
+#define OWB_TAIL_SYNC_LEN	4
+#define OWB_HDR_LEN		5	/* start, src, dst, cmd, len */
+#define OWB_TRAILER_LEN		5	/* crc(2), end, tail sync(2) seen on rx */
+#define OWB_MAX_PAYLOAD		128
+#define OWB_RX_BUF_LEN		(OWB_HDR_LEN + OWB_MAX_PAYLOAD + OWB_TRAILER_LEN)
+#define OWB_TX_BUF_LEN		(OWB_HEAD_SYNC_LEN + OWB_HDR_LEN + OWB_MAX_PAYLOAD + \
+				 2 + 1 + OWB_TAIL_SYNC_LEN)
+
+#define OWB_CRC_POLY		0x8005
+#define OWB_CRC_INIT		0xC596
+
+/* main commands */
+#define OWB_CMD_KEYS		0x01
+#define OWB_CMD_MEDIA_KEYS	0x02
+#define OWB_CMD_TOUCHPAD	0x03
+#define OWB_CMD_PARAM_SET	0x20
+#define OWB_CMD_SYNC_UPLOAD	0x2F
+#define OWB_CMD_GENERAL		0x3A
+#define OWB_CMD_GENERAL_ACK	0x3B
+
+/* 0x3A sub commands */
+#define OWB_GEN_SLEEP		0x02
+#define OWB_GEN_BATTERY_STATUS	0x0E
+#define OWB_GEN_BRIGHTNESS	0x1C
+
+/* sync upload sub commands */
+#define OWB_SYNC_PLUG_IN	0x01
+#define OWB_SYNC_HEARTBEAT	0x05
+
+/* timing, all derived from the vendor driver */
+#define POGO_TX_GATE_ON_US	450
+#define POGO_TX_GATE_OFF_US	300
+#define POGO_ECHO_TIMEOUT_MS	50
+#define POGO_RESP_TIMEOUT_MS	100
+#define POGO_XFER_RETRIES	3
+#define POGO_DETECT_DEBOUNCE_MS	120
+#define POGO_DETECT_SAMPLES	6	/* 6 x 20 ms, as the vendor driver */
+#define POGO_DETECT_SAMPLE_MS	20
+#define POGO_HB_POLL_MS		250
+#define POGO_HB_TIMEOUT_MS	500	/* after the first sync frame */
+#define POGO_BOOT_TIMEOUT_MS	2000	/* power on -> first sync frame */
+#define POGO_RETRY_MIN_MS	1000
+#define POGO_RETRY_MAX_MS	30000
+
+#define POGO_TOUCH_FINGERS	5
+#define POGO_TOUCH_REC_LEN	5
+#define POGO_KEY_REPORT_LEN	8
+#define POGO_MEDIA_REPORT_LEN	4
+#define POGO_MAC_LEN		6
+#define POGO_BACKLIGHT_MAX	100	/* MCU accepts 0..100 */
+
+struct pogo_kbd {
+	struct serdev_device *serdev;
+	struct device *dev;
+
+	struct gpio_desc *power_gpio;
+	struct gpio_desc *tx_en_gpio;
+	struct gpio_desc *wake_gpio;
+	int wake_irq;
+	bool wake_irq_enabled;
+
+	/* rx frame assembly, serdev receive context only */
+	u8 rx_buf[OWB_RX_BUF_LEN];
+	unsigned int rx_len;
+	unsigned int rx_expected;
+	unsigned int sync_cnt;
+	bool in_frame;
+
+	/* host -> keyboard transfers */
+	struct mutex io_lock;
+	struct completion echo_done;
+	struct completion resp_done;
+	u8 tx_cmd;
+	bool tx_pending;
+	u8 resp[OWB_MAX_PAYLOAD];
+	unsigned int resp_len;
+
+	/* link state */
+	bool powered;
+	bool connected;
+	bool hb_seen;		/* MCU left its plug-in phase */
+	unsigned long last_rx;
+	unsigned int retry_ms;
+	u8 brand;
+	u8 mac[POGO_MAC_LEN];
+	int battery_level;
+
+	struct delayed_work detect_work;
+	struct delayed_work hb_work;
+	struct work_struct plug_work;
+	struct work_struct led_work;
+	bool caps_led;
+	struct led_classdev backlight;
+	u8 brightness;
+
+	/* input */
+	struct mutex input_lock;
+	struct input_dev *kbd;
+	struct input_dev *tp;
+	u8 old_keys[POGO_KEY_REPORT_LEN];
+	u8 old_media[POGO_MEDIA_REPORT_LEN];
+	u32 tp_max_x, tp_max_y, tp_res_x, tp_res_y;
+	char kbd_name[64];
+	char tp_name[64];
+};
+
+/* HID boot keyboard usage -> linux keycode, identical to usbkbd + vendor extras */
+static const unsigned char pogo_keycode[256] = {
+	  0,  0,  0,  0, 30, 48, 46, 32, 18, 33, 34, 35, 23, 36, 37, 38,
+	 50, 49, 24, 25, 16, 19, 31, 20, 22, 47, 17, 45, 21, 44,  2,  3,
+	  4,  5,  6,  7,  8,  9, 10, 11, 28,  1, 14, 15, 57, 12, 13, 26,
+	 27, 43, 43, 39, 40, 41, 51, 52, 53, 58, 59, 60, 61, 62, 63, 64,
+	 65, 66, 67, 68, 87, 88, 99, 70, 119, 110, 102, 104, 111, 107, 109, 106,
+	105, 108, 103, 69, 98, 55, 74, 78, 96, 79, 80, 81, 75, 76, 77, 71,
+	 72, 73, 82, 83, 86, 127, 116, 117, 183, 184, 185, 186, 187, 188, 189, 190,
+	191, 192, 193, 194, 134, 138, 130, 132, 128, 129, 131, 137, 133, 135, 136, 113,
+	115, 114,  0,  0,  0, 121,  0, 89, 93, 124, 92, 94, 95,  0,  0,  0,
+	122, 123, 90, 91, 85,  0,  0,  0,  0,  0,  0,  0, 111,  0,  0,  0,
+	  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
+	  0,  0,  0,  0,  0,  0, 179, 180,  0,  0,  0,  0,  0,  0,  0,  0,
+	  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
+	  0,  0,  0,  0,  0,  0,  0,  0, 111,  0,  0,  0,  0,  0,  0,  0,
+	 29, 42, 56, 125, 97, 54, 100, 126, 164, 166, 165, 163, 161, 115, 114, 113,
+	150, 158, 159, 128, 136, 177, 178, 176, 142, 152, 173, 140,  0,  0,  0,  0
+};
+
+/* HID consumer usage -> linux keycode for the function row */
+static const struct {
+	u16 usage;
+	u16 keycode;
+} pogo_media_keys[] = {
+	{ 0x070, KEY_BRIGHTNESSDOWN },
+	{ 0x06f, KEY_BRIGHTNESSUP },
+	{ 0x0e2, KEY_MUTE },
+	{ 0x0ea, KEY_VOLUMEDOWN },
+	{ 0x0e9, KEY_VOLUMEUP },
+	{ 0x224, KEY_BACK },
+	{ 0x38e, KEY_SCREENLOCK },
+	{ 0x390, KEY_KBD_LAYOUT_NEXT },
+	{ 0x391, KEY_MICMUTE },
+	{ 0x392, KEY_TOUCHPAD_TOGGLE },
+	{ 0x393, KEY_SEARCH },
+	{ 0x394, KEY_FULL_SCREEN },
+	{ 0x395, KEY_SELECTIVE_SCREENSHOT },
+	{ 0x397, KEY_PROG1 },
+	{ 0x398, KEY_PROG2 },
+	{ 0x399, KEY_PROG3 },
+	{ 0x0cd, KEY_PLAYPAUSE },
+	{ 0x0b5, KEY_NEXTSONG },
+	{ 0x0b6, KEY_PREVIOUSSONG },
+	{ 0x244, KEY_APPSELECT },
+	{ 0x2fa, KEY_ASSISTANT },
+	{ 0x1d0, KEY_FN },
+};
+
+static const char * const pogo_brand_names[] = {
+	"OPPO Pad 5 Pro Smart Keyboard",
+	"OnePlus Pad 3 Pro Smart Keyboard",
+	"OnePlus Pad 4 Smart Keyboard",
+};
+
+/* ------------------------------------------------------------------------ */
+/* framing                                                                  */
+
+static u16 owb_crc16(const u8 *buf, unsigned int len)
+{
+	u16 crc = OWB_CRC_INIT;
+	unsigned int i, bit;
+
+	for (i = 0; i < len; i++) {
+		u8 data = buf[i];
+
+		for (bit = 0; bit < 8; bit++) {
+			bool feedback = (crc ^ (data << 8)) & 0x8000;
+
+			crc <<= 1;
+			if (feedback)
+				crc ^= OWB_CRC_POLY;
+			data <<= 1;
+		}
+	}
+	return crc;
+}
+
+static unsigned int owb_build_frame(u8 *out, u8 cmd, const u8 *payload, u8 len)
+{
+	unsigned int n = 0;
+	u16 crc;
+
+	memset(out, OWB_HEAD_SYNC, OWB_HEAD_SYNC_LEN);
+	n += OWB_HEAD_SYNC_LEN;
+	out[n++] = OWB_START;
+	out[n++] = OWB_ADDR_PAD;
+	out[n++] = OWB_ADDR_KBD;
+	out[n++] = cmd;
+	out[n++] = len;
+	memcpy(&out[n], payload, len);
+	n += len;
+	crc = owb_crc16(&out[OWB_HEAD_SYNC_LEN], OWB_HDR_LEN + len);
+	out[n++] = crc >> 8;
+	out[n++] = crc & 0xff;
+	out[n++] = OWB_END;
+	memset(&out[n], OWB_TAIL_SYNC, OWB_TAIL_SYNC_LEN);
+	n += OWB_TAIL_SYNC_LEN;
+
+	return n;
+}
+
+/* ------------------------------------------------------------------------ */
+/* host -> keyboard                                                         */
+
+/*
+ * Send cmd/payload and optionally wait for the cmd + 1 reply.  The reply
+ * (cmd, len, data...) is copied to @resp when non-NULL and its length is
+ * returned.  Process context only; serialised by io_lock.
+ */
+static int pogo_xfer(struct pogo_kbd *kb, u8 cmd, const u8 *payload, u8 len,
+		     u8 *resp, unsigned int resp_size)
+{
+	u8 frame[OWB_TX_BUF_LEN];
+	unsigned int frame_len;
+	int attempt, ret = -ETIMEDOUT;
+
+	if (len > OWB_MAX_PAYLOAD)
+		return -EINVAL;
+
+	frame_len = owb_build_frame(frame, cmd, payload, len);
+	print_hex_dump_debug("pogo tx: ", DUMP_PREFIX_NONE, 32, 1, frame, frame_len, false);
+
+	mutex_lock(&kb->io_lock);
+	for (attempt = 0; attempt < POGO_XFER_RETRIES; attempt++) {
+		bool echoed;
+
+		reinit_completion(&kb->echo_done);
+		reinit_completion(&kb->resp_done);
+		kb->tx_cmd = cmd;
+		WRITE_ONCE(kb->tx_pending, true);
+
+		gpiod_set_value_cansleep(kb->tx_en_gpio, 1);
+		usleep_range(POGO_TX_GATE_ON_US, POGO_TX_GATE_ON_US + 50);
+
+		ret = serdev_device_write(kb->serdev, frame, frame_len,
+					  msecs_to_jiffies(POGO_RESP_TIMEOUT_MS));
+		if (ret >= 0)
+			serdev_device_wait_until_sent(kb->serdev,
+						      msecs_to_jiffies(20));
+
+		/*
+		 * Release the wire as soon as the last bit is out: the MCU
+		 * answers within a few hundred microseconds and would collide
+		 * with our idle-high TX driver otherwise.  The echo is only
+		 * checked afterwards, it is already queued in the tty buffer.
+		 */
+		usleep_range(POGO_TX_GATE_OFF_US, POGO_TX_GATE_OFF_US + 50);
+		gpiod_set_value_cansleep(kb->tx_en_gpio, 0);
+
+		echoed = wait_for_completion_timeout(&kb->echo_done,
+				msecs_to_jiffies(POGO_ECHO_TIMEOUT_MS)) != 0;
+
+		if (ret < 0) {
+			dev_warn(kb->dev, "cmd 0x%02x write failed: %d\n", cmd, ret);
+			continue;
+		}
+		if (!echoed) {
+			dev_dbg(kb->dev, "cmd 0x%02x: no echo\n", cmd);
+			ret = -EIO;
+			continue;
+		}
+		if (!resp) {
+			ret = 0;
+			break;
+		}
+		if (wait_for_completion_timeout(&kb->resp_done,
+				msecs_to_jiffies(POGO_RESP_TIMEOUT_MS))) {
+			unsigned int n = min(kb->resp_len, resp_size);
+
+			memcpy(resp, kb->resp, n);
+			ret = n;
+			break;
+		}
+		dev_dbg(kb->dev, "cmd 0x%02x: no reply\n", cmd);
+		ret = -ETIMEDOUT;
+	}
+	WRITE_ONCE(kb->tx_pending, false);
+	mutex_unlock(&kb->io_lock);
+
+	return ret;
+}
+
+static int pogo_set_general(struct pogo_kbd *kb, u8 sub, u8 value)
+{
+	const u8 payload[] = { sub, 0x01, value };
+	u8 resp[8];
+	int ret;
+
+	ret = pogo_xfer(kb, OWB_CMD_GENERAL, payload, sizeof(payload),
+			resp, sizeof(resp));
+	if (ret < 0)
+		return ret;
+	if (ret < 3 || resp[0] != OWB_CMD_GENERAL_ACK || resp[2] != sub)
+		return -EPROTO;
+	return 0;
+}
+
+static int pogo_set_leds(struct pogo_kbd *kb)
+{
+	/* parameter 0x0D "led state": caps, reserved, mute, mic */
+	const u8 payload[] = { 0x0D, 0x04, kb->caps_led, 0x00, 0x00, 0x00 };
+
+	return pogo_xfer(kb, OWB_CMD_PARAM_SET, payload, sizeof(payload),
+			 NULL, 0);
+}
+
+/* ------------------------------------------------------------------------ */
+/* input reporting (input_lock held)                                        */
+
+static void pogo_report_keys(struct pogo_kbd *kb, const u8 *rep)
+{
+	struct input_dev *input = kb->kbd;
+	int i;
+
+	for (i = 0; i < 8; i++)
+		input_report_key(input, pogo_keycode[i + 224], (rep[0] >> i) & 1);
+
+	for (i = 2; i < POGO_KEY_REPORT_LEN; i++) {
+		u8 old = kb->old_keys[i], new = rep[i];
+
+		if (old > 3 && !memchr(rep + 2, old, 6)) {
+			if (pogo_keycode[old])
+				input_report_key(input, pogo_keycode[old], 0);
+			else
+				dev_dbg(kb->dev, "unknown scancode %#x released\n", old);
+		}
+		if (new > 3 && !memchr(kb->old_keys + 2, new, 6)) {
+			if (pogo_keycode[new])
+				input_report_key(input, pogo_keycode[new], 1);
+			else
+				dev_dbg(kb->dev, "unknown scancode %#x pressed\n", new);
+		}
+	}
+	input_sync(input);
+	memcpy(kb->old_keys, rep, POGO_KEY_REPORT_LEN);
+}
+
+static int pogo_media_keycode(u16 usage)
+{
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(pogo_media_keys); i++)
+		if (pogo_media_keys[i].usage == usage)
+			return pogo_media_keys[i].keycode;
+	return 0;
+}
+
+static bool pogo_media_has(const u8 *rep, u16 usage)
+{
+	return get_unaligned_le16(rep) == usage ||
+	       get_unaligned_le16(rep + 2) == usage;
+}
+
+static void pogo_report_media(struct pogo_kbd *kb, const u8 *rep)
+{
+	struct input_dev *input = kb->kbd;
+	int i;
+
+	for (i = 0; i < POGO_MEDIA_REPORT_LEN; i += 2) {
+		u16 old = get_unaligned_le16(kb->old_media + i);
+		u16 new = get_unaligned_le16(rep + i);
+		int code;
+
+		if (old && !pogo_media_has(rep, old)) {
+			code = pogo_media_keycode(old);
+			if (code)
+				input_report_key(input, code, 0);
+		}
+		if (new && !pogo_media_has(kb->old_media, new)) {
+			code = pogo_media_keycode(new);
+			if (code)
+				input_report_key(input, code, 1);
+			else
+				dev_dbg(kb->dev, "unknown consumer usage %#x\n", new);
+		}
+	}
+	input_sync(input);
+	memcpy(kb->old_media, rep, POGO_MEDIA_REPORT_LEN);
+}
+
+/*
+ * Touchpad payload: fingers x {flags, x_lo, x_hi, y_lo, y_hi} followed by
+ * an optional 16 bit pressure, a finger count byte and a button byte.
+ * flags: [7:4] slot id, [1] down, [1:0] palm (2 = palm).
+ */
+static void pogo_report_touchpad(struct pogo_kbd *kb, const u8 *data,
+				 unsigned int len)
+{
+	struct input_dev *input = kb->tp;
+	unsigned int fingers, i;
+	u8 buttons;
+
+	if (len < 2)
+		return;
+	buttons = data[len - 1];
+	fingers = data[len - 2];
+	if (fingers > POGO_TOUCH_FINGERS ||
+	    fingers * POGO_TOUCH_REC_LEN + 2 > len)
+		return;
+
+	for (i = 0; i < fingers; i++) {
+		const u8 *rec = data + i * POGO_TOUCH_REC_LEN;
+		unsigned int slot = rec[0] >> 4;
+		bool down = rec[0] & BIT(1);
+		unsigned int tool = (rec[0] & 0x03) == 0x02 ?
+				    MT_TOOL_PALM : MT_TOOL_FINGER;
+
+		if (slot >= POGO_TOUCH_FINGERS)
+			continue;
+		input_mt_slot(input, slot);
+		input_mt_report_slot_state(input, tool, down);
+		if (down) {
+			input_report_abs(input, ABS_MT_POSITION_X,
+					 get_unaligned_le16(rec + 1));
+			input_report_abs(input, ABS_MT_POSITION_Y,
+					 get_unaligned_le16(rec + 3));
+		}
+	}
+	input_mt_sync_frame(input);
+	/* clickpad: libinput derives left/right from the finger position */
+	input_report_key(input, BTN_LEFT, buttons & 0x03);
+	input_sync(input);
+}
+
+static void pogo_release_all(struct pogo_kbd *kb)
+{
+	static const u8 zeros[POGO_KEY_REPORT_LEN];
+
+	if (kb->kbd) {
+		pogo_report_keys(kb, zeros);
+		pogo_report_media(kb, zeros);
+	}
+	if (kb->tp) {
+		input_mt_sync_frame(kb->tp);
+		input_report_key(kb->tp, BTN_LEFT, 0);
+		input_sync(kb->tp);
+	}
+}
+
+static int pogo_kbd_event(struct input_dev *input, unsigned int type,
+			  unsigned int code, int value)
+{
+	struct pogo_kbd *kb = input_get_drvdata(input);
+
+	if (type != EV_LED || code != LED_CAPSL)
+		return -EINVAL;
+
+	kb->caps_led = !!value;
+	schedule_work(&kb->led_work);
+	return 0;
+}
+
+static int pogo_register_inputs(struct pogo_kbd *kb)
+{
+	struct input_dev *kbd, *tp;
+	const char *name;
+	int i, ret;
+
+	name = (kb->brand >= 1 && kb->brand <= ARRAY_SIZE(pogo_brand_names)) ?
+		pogo_brand_names[kb->brand - 1] : "Pogo Keyboard";
+	snprintf(kb->kbd_name, sizeof(kb->kbd_name), "%s", name);
+	snprintf(kb->tp_name, sizeof(kb->tp_name), "%s Touchpad", name);
+
+	kbd = input_allocate_device();
+	tp = input_allocate_device();
+	if (!kbd || !tp) {
+		ret = -ENOMEM;
+		goto err_free;
+	}
+
+	kbd->name = kb->kbd_name;
+	kbd->phys = "pogo/input0";
+	kbd->dev.parent = kb->dev;
+	kbd->id.bustype = BUS_HOST;
+	kbd->id.vendor = 0x22d9;
+	kbd->id.product = 0x3869;
+	kbd->id.version = 0x0010;
+	kbd->event = pogo_kbd_event;
+	input_set_drvdata(kbd, kb);
+	__set_bit(EV_KEY, kbd->evbit);
+	__set_bit(EV_REP, kbd->evbit);
+	__set_bit(EV_LED, kbd->evbit);
+	__set_bit(LED_CAPSL, kbd->ledbit);
+	for (i = 0; i < ARRAY_SIZE(pogo_keycode); i++)
+		if (pogo_keycode[i])
+			__set_bit(pogo_keycode[i], kbd->keybit);
+	for (i = 0; i < ARRAY_SIZE(pogo_media_keys); i++)
+		__set_bit(pogo_media_keys[i].keycode, kbd->keybit);
+
+	tp->name = kb->tp_name;
+	tp->phys = "pogo/input1";
+	tp->dev.parent = kb->dev;
+	tp->id.bustype = BUS_HOST;
+	tp->id.vendor = 0x22d9;
+	tp->id.product = 0x3869;
+	tp->id.version = 0x0010;
+	input_set_drvdata(tp, kb);
+	__set_bit(EV_KEY, tp->evbit);
+	__set_bit(BTN_LEFT, tp->keybit);
+	__set_bit(INPUT_PROP_POINTER, tp->propbit);
+	__set_bit(INPUT_PROP_BUTTONPAD, tp->propbit);
+	input_set_abs_params(tp, ABS_MT_POSITION_X, 0, kb->tp_max_x, 0, 0);
+	input_set_abs_params(tp, ABS_MT_POSITION_Y, 0, kb->tp_max_y, 0, 0);
+	input_abs_set_res(tp, ABS_MT_POSITION_X, kb->tp_res_x);
+	input_abs_set_res(tp, ABS_MT_POSITION_Y, kb->tp_res_y);
+	ret = input_mt_init_slots(tp, POGO_TOUCH_FINGERS,
+				  INPUT_MT_POINTER | INPUT_MT_DROP_UNUSED);
+	if (ret)
+		goto err_free;
+
+	ret = input_register_device(kbd);
+	if (ret)
+		goto err_free;
+	ret = input_register_device(tp);
+	if (ret) {
+		input_unregister_device(kbd);
+		kbd = NULL;
+		goto err_free;
+	}
+
+	mutex_lock(&kb->input_lock);
+	kb->kbd = kbd;
+	kb->tp = tp;
+	mutex_unlock(&kb->input_lock);
+	return 0;
+
+err_free:
+	input_free_device(tp);
+	input_free_device(kbd);
+	return ret;
+}
+
+static void pogo_unregister_inputs(struct pogo_kbd *kb)
+{
+	struct input_dev *kbd, *tp;
+
+	mutex_lock(&kb->input_lock);
+	pogo_release_all(kb);
+	kbd = kb->kbd;
+	tp = kb->tp;
+	kb->kbd = NULL;
+	kb->tp = NULL;
+	memset(kb->old_keys, 0, sizeof(kb->old_keys));
+	memset(kb->old_media, 0, sizeof(kb->old_media));
+	mutex_unlock(&kb->input_lock);
+
+	if (tp)
+		input_unregister_device(tp);
+	if (kbd)
+		input_unregister_device(kbd);
+}
+
+/* ------------------------------------------------------------------------ */
+/* rx path (serdev receive context)                                         */
+
+static void pogo_handle_sync(struct pogo_kbd *kb, const u8 *p, unsigned int len)
+{
+	bool replug = false;
+
+	if (len < 2)
+		return;
+
+	if (p[0] == OWB_SYNC_PLUG_IN && p[1] == 0x02 && len >= 9) {
+		kb->brand = p[2];
+		memcpy(kb->mac, &p[3], POGO_MAC_LEN);
+		/*
+		 * The MCU repeats this frame until the host has talked to it;
+		 * only a plug-in frame after regular heartbeats is a re-plug.
+		 */
+		replug = kb->connected && kb->hb_seen;
+	} else if (p[0] == OWB_SYNC_HEARTBEAT && p[1] == 0x02 && len >= 10) {
+		kb->brand = p[3];
+		memcpy(kb->mac, &p[4], POGO_MAC_LEN);
+		kb->hb_seen = true;
+	}
+
+	if (!kb->connected || replug) {
+		kb->connected = true;
+		kb->hb_seen = false;
+		kb->retry_ms = POGO_RETRY_MIN_MS;
+		schedule_work(&kb->plug_work);
+	}
+}
+
+static void pogo_handle_frame(struct pogo_kbd *kb, const u8 *f, unsigned int n)
+{
+	u8 src = f[1], cmd = f[3], len = f[4];
+	const u8 *payload = &f[OWB_HDR_LEN];
+
+	if (src == OWB_ADDR_PAD) {
+		/* our own frame echoed back through the shared wire */
+		if (READ_ONCE(kb->tx_pending) && cmd == kb->tx_cmd)
+			complete(&kb->echo_done);
+		return;
+	}
+	if (src != OWB_ADDR_KBD)
+		return;
+
+	kb->last_rx = jiffies;
+
+	switch (cmd) {
+	case OWB_CMD_KEYS:
+		if (len < POGO_KEY_REPORT_LEN)
+			break;
+		mutex_lock(&kb->input_lock);
+		if (kb->kbd)
+			pogo_report_keys(kb, payload);
+		mutex_unlock(&kb->input_lock);
+		break;
+	case OWB_CMD_MEDIA_KEYS:
+		if (len < POGO_MEDIA_REPORT_LEN)
+			break;
+		mutex_lock(&kb->input_lock);
+		if (kb->kbd)
+			pogo_report_media(kb, payload);
+		mutex_unlock(&kb->input_lock);
+		break;
+	case OWB_CMD_TOUCHPAD:
+		mutex_lock(&kb->input_lock);
+		if (kb->tp)
+			pogo_report_touchpad(kb, payload, len);
+		mutex_unlock(&kb->input_lock);
+		break;
+	case OWB_CMD_SYNC_UPLOAD:
+		pogo_handle_sync(kb, payload, len);
+		break;
+	default:
+		if (cmd == OWB_CMD_GENERAL_ACK && len >= 4 &&
+		    payload[0] == OWB_GEN_BATTERY_STATUS)
+			kb->battery_level = payload[2];
+
+		if (READ_ONCE(kb->tx_pending) && cmd == kb->tx_cmd + 1) {
+			/* reply layout for callers: cmd, len, payload */
+			kb->resp_len = min_t(unsigned int, len + 2, sizeof(kb->resp));
+			memcpy(kb->resp, &f[3], kb->resp_len);
+			complete(&kb->resp_done);
+		} else {
+			dev_dbg(kb->dev, "unhandled frame cmd 0x%02x len %u\n",
+				cmd, len);
+		}
+		break;
+	}
+}
+
+static void pogo_rx_frame_done(struct pogo_kbd *kb)
+{
+	const u8 *f = kb->rx_buf;
+	unsigned int n = kb->rx_len;
+	u16 crc = (f[n - 5] << 8) | f[n - 4];
+
+	if (f[n - 3] != OWB_END || f[n - 2] != OWB_TAIL_SYNC ||
+	    f[n - 1] != OWB_TAIL_SYNC) {
+		dev_dbg(kb->dev, "bad frame trailer\n");
+		return;
+	}
+	if (crc != owb_crc16(f, n - 5)) {
+		dev_dbg(kb->dev, "bad crc %04x\n", crc);
+		return;
+	}
+	pogo_handle_frame(kb, f, n);
+}
+
+static void pogo_rx_byte(struct pogo_kbd *kb, u8 c)
+{
+	if (!kb->in_frame) {
+		if (c == OWB_HEAD_SYNC) {
+			kb->sync_cnt++;
+			return;
+		}
+		if ((c == OWB_START || c == OWB_START_REPEAT) &&
+		    kb->sync_cnt >= 4) {
+			kb->in_frame = true;
+			kb->rx_len = 0;
+			kb->rx_expected = 0;
+			kb->rx_buf[kb->rx_len++] = c;
+		}
+		kb->sync_cnt = 0;
+		return;
+	}
+
+	kb->rx_buf[kb->rx_len++] = c;
+
+	if (kb->rx_len == OWB_HDR_LEN) {
+		kb->rx_expected = OWB_HDR_LEN + c + OWB_TRAILER_LEN;
+		if (kb->rx_expected > sizeof(kb->rx_buf)) {
+			kb->in_frame = false;
+			return;
+		}
+	}
+	if (!kb->rx_expected || kb->rx_len < kb->rx_expected)
+		return;
+
+	kb->in_frame = false;
+	pogo_rx_frame_done(kb);
+}
+
+static size_t pogo_receive_buf(struct serdev_device *serdev, const u8 *data,
+			       size_t count)
+{
+	struct pogo_kbd *kb = serdev_device_get_drvdata(serdev);
+	size_t i;
+
+	print_hex_dump_debug("pogo rx: ", DUMP_PREFIX_NONE, 32, 1, data, count, false);
+	for (i = 0; i < count; i++)
+		pogo_rx_byte(kb, data[i]);
+	return count;
+}
+
+static const struct serdev_device_ops pogo_serdev_ops = {
+	.receive_buf = pogo_receive_buf,
+	.write_wakeup = serdev_device_write_wakeup,
+};
+
+/* ------------------------------------------------------------------------ */
+/* power / attach state machine (process context)                           */
+
+static void pogo_wake_irq_enable(struct pogo_kbd *kb, bool enable)
+{
+	if (enable == kb->wake_irq_enabled)
+		return;
+	if (enable)
+		enable_irq(kb->wake_irq);
+	else
+		disable_irq(kb->wake_irq);
+	kb->wake_irq_enabled = enable;
+}
+
+static void pogo_power_on(struct pogo_kbd *kb)
+{
+	dev_dbg(kb->dev, "power on\n");
+	kb->connected = false;
+	kb->hb_seen = false;
+	kb->battery_level = -1;
+	kb->last_rx = jiffies;
+	kb->powered = true;
+	gpiod_set_value_cansleep(kb->power_gpio, 1);
+	mod_delayed_work(system_dfl_wq, &kb->hb_work,
+			 msecs_to_jiffies(POGO_HB_POLL_MS));
+}
+
+static void pogo_power_off(struct pogo_kbd *kb)
+{
+	dev_dbg(kb->dev, "power off\n");
+	kb->powered = false;
+	gpiod_set_value_cansleep(kb->power_gpio, 0);
+	if (kb->connected) {
+		kb->connected = false;
+		pogo_unregister_inputs(kb);
+		dev_info(kb->dev, "keyboard detached\n");
+	}
+}
+
+static void pogo_detect_work(struct work_struct *work)
+{
+	struct pogo_kbd *kb = container_of(work, struct pogo_kbd,
+					   detect_work.work);
+
+	int i;
+
+	if (kb->powered)
+		return;
+
+	/* the unpowered keyboard holds the wire low; require a stable level */
+	for (i = 0; i < POGO_DETECT_SAMPLES; i++) {
+		if (!gpiod_get_value_cansleep(kb->wake_gpio))
+			break;
+		msleep(POGO_DETECT_SAMPLE_MS);
+	}
+	if (i == POGO_DETECT_SAMPLES) {
+		pogo_power_on(kb);
+		return;
+	}
+
+	kb->retry_ms = POGO_RETRY_MIN_MS;
+	pogo_wake_irq_enable(kb, true);
+	/* the edge may have happened before the irq was armed */
+	if (gpiod_get_value_cansleep(kb->wake_gpio))
+		mod_delayed_work(system_dfl_wq, &kb->detect_work,
+				 msecs_to_jiffies(POGO_DETECT_DEBOUNCE_MS));
+}
+
+static void pogo_hb_work(struct work_struct *work)
+{
+	struct pogo_kbd *kb = container_of(work, struct pogo_kbd, hb_work.work);
+	unsigned int timeout = kb->connected ? POGO_HB_TIMEOUT_MS :
+					       POGO_BOOT_TIMEOUT_MS;
+
+	if (!kb->powered)
+		return;
+
+	if (time_before(jiffies, kb->last_rx + msecs_to_jiffies(timeout))) {
+		schedule_delayed_work(&kb->hb_work,
+				      msecs_to_jiffies(POGO_HB_POLL_MS));
+		return;
+	}
+
+	dev_dbg(kb->dev, "heartbeat lost (%s)\n",
+		kb->connected ? "connected" : "booting");
+	pogo_power_off(kb);
+
+	/* re-arm: retry while the wake line still reports a keyboard */
+	schedule_delayed_work(&kb->detect_work, msecs_to_jiffies(kb->retry_ms));
+	kb->retry_ms = min(kb->retry_ms * 2, POGO_RETRY_MAX_MS);
+}
+
+static void pogo_plug_work(struct work_struct *work)
+{
+	struct pogo_kbd *kb = container_of(work, struct pogo_kbd, plug_work);
+	int ret;
+
+	if (!kb->powered || !kb->connected)
+		return;
+
+	if (!kb->kbd) {
+		ret = pogo_register_inputs(kb);
+		if (ret) {
+			dev_err(kb->dev, "failed to register input devices: %d\n",
+				ret);
+			return;
+		}
+		dev_info(kb->dev, "keyboard attached: %s (brand %u, mac %pM)\n",
+			 kb->kbd_name, kb->brand, kb->mac);
+	}
+
+	/*
+	 * Report the host as awake: this is what moves the MCU from the
+	 * repeated plug-in sync frames to regular heartbeats.  Then restore
+	 * the host side state on the (possibly re-plugged) MCU.
+	 */
+	ret = pogo_set_general(kb, OWB_GEN_SLEEP, 0);
+	if (ret)
+		dev_warn(kb->dev, "wake notify failed: %d\n", ret);
+	pogo_set_leds(kb);
+	if (kb->brightness)
+		pogo_set_general(kb, OWB_GEN_BRIGHTNESS, kb->brightness);
+}
+
+/* ------------------------------------------------------------------------ */
+/* keyboard backlight as a LED class device (picked up by UPower/GNOME)    */
+
+static int pogo_backlight_set(struct led_classdev *cdev, enum led_brightness value)
+{
+	struct pogo_kbd *kb = container_of(cdev, struct pogo_kbd, backlight);
+
+	kb->brightness = value;
+	/* when detached just remember the level; plug_work restores it */
+	if (!kb->connected)
+		return 0;
+	return pogo_set_general(kb, OWB_GEN_BRIGHTNESS, value);
+}
+
+static int pogo_register_backlight(struct pogo_kbd *kb)
+{
+	struct led_init_data init_data = {
+		/* no colour: yields "pogo-keyboard::kbd_backlight" */
+		.default_label = ":" LED_FUNCTION_KBD_BACKLIGHT,
+		.devicename = "pogo-keyboard",
+	};
+
+	kb->backlight.max_brightness = POGO_BACKLIGHT_MAX;
+	kb->backlight.brightness_set_blocking = pogo_backlight_set;
+	kb->backlight.flags = LED_CORE_SUSPENDRESUME;
+
+	return devm_led_classdev_register_ext(kb->dev, &kb->backlight, &init_data);
+}
+
+static void pogo_led_work(struct work_struct *work)
+{
+	struct pogo_kbd *kb = container_of(work, struct pogo_kbd, led_work);
+
+	if (kb->connected)
+		pogo_set_leds(kb);
+}
+
+static irqreturn_t pogo_wake_isr(int irq, void *data)
+{
+	struct pogo_kbd *kb = data;
+
+	/* the wire carries UART traffic once powered; stay quiet until then */
+	disable_irq_nosync(irq);
+	kb->wake_irq_enabled = false;
+	mod_delayed_work(system_dfl_wq, &kb->detect_work,
+			 msecs_to_jiffies(POGO_DETECT_DEBOUNCE_MS));
+	return IRQ_HANDLED;
+}
+
+/* ------------------------------------------------------------------------ */
+/* sysfs: keyboard battery and link state                                   */
+
+static ssize_t battery_level_show(struct device *dev,
+				  struct device_attribute *attr, char *buf)
+{
+	struct pogo_kbd *kb = dev_get_drvdata(dev);
+
+	if (!kb->connected)
+		return -ENODEV;
+	/* only keyboards with a battery push 0x0E status frames */
+	if (kb->battery_level < 0)
+		return -ENODATA;
+	return sysfs_emit(buf, "%d\n", kb->battery_level);
+}
+static DEVICE_ATTR_RO(battery_level);
+
+static ssize_t connected_show(struct device *dev,
+			      struct device_attribute *attr, char *buf)
+{
+	struct pogo_kbd *kb = dev_get_drvdata(dev);
+
+	return sysfs_emit(buf, "%d\n", kb->connected);
+}
+static DEVICE_ATTR_RO(connected);
+
+static struct attribute *pogo_attrs[] = {
+	&dev_attr_battery_level.attr,
+	&dev_attr_connected.attr,
+	NULL
+};
+ATTRIBUTE_GROUPS(pogo);
+
+/* ------------------------------------------------------------------------ */
+/* probe / remove                                                           */
+
+static int pogo_parse_dt(struct pogo_kbd *kb)
+{
+	struct device *dev = kb->dev;
+	u32 val[2];
+
+	kb->power_gpio = devm_gpiod_get(dev, "power", GPIOD_OUT_LOW);
+	if (IS_ERR(kb->power_gpio))
+		return dev_err_probe(dev, PTR_ERR(kb->power_gpio), "power gpio\n");
+
+	kb->tx_en_gpio = devm_gpiod_get(dev, "tx-enable", GPIOD_OUT_LOW);
+	if (IS_ERR(kb->tx_en_gpio))
+		return dev_err_probe(dev, PTR_ERR(kb->tx_en_gpio),
+				     "tx-enable gpio\n");
+
+	kb->wake_gpio = devm_gpiod_get(dev, "wake", GPIOD_IN);
+	if (IS_ERR(kb->wake_gpio))
+		return dev_err_probe(dev, PTR_ERR(kb->wake_gpio), "wake gpio\n");
+
+	kb->wake_irq = gpiod_to_irq(kb->wake_gpio);
+	if (kb->wake_irq < 0)
+		return dev_err_probe(dev, kb->wake_irq, "wake irq\n");
+
+	kb->tp_max_x = 2560;
+	kb->tp_max_y = 1440;
+	if (!device_property_read_u32_array(dev, "touchpad-xy-max", val, 2)) {
+		kb->tp_max_x = val[0];
+		kb->tp_max_y = val[1];
+	}
+	if (!device_property_read_u32_array(dev, "touchpad-xy-resolution",
+					    val, 2)) {
+		kb->tp_res_x = val[0];
+		kb->tp_res_y = val[1];
+	}
+	return 0;
+}
+
+static int pogo_probe(struct serdev_device *serdev)
+{
+	struct device *dev = &serdev->dev;
+	struct pogo_kbd *kb;
+	int ret;
+
+	kb = devm_kzalloc(dev, sizeof(*kb), GFP_KERNEL);
+	if (!kb)
+		return -ENOMEM;
+
+	kb->serdev = serdev;
+	kb->dev = dev;
+	kb->battery_level = -1;
+	kb->retry_ms = POGO_RETRY_MIN_MS;
+	mutex_init(&kb->io_lock);
+	mutex_init(&kb->input_lock);
+	init_completion(&kb->echo_done);
+	init_completion(&kb->resp_done);
+	INIT_DELAYED_WORK(&kb->detect_work, pogo_detect_work);
+	INIT_DELAYED_WORK(&kb->hb_work, pogo_hb_work);
+	INIT_WORK(&kb->plug_work, pogo_plug_work);
+	INIT_WORK(&kb->led_work, pogo_led_work);
+	serdev_device_set_drvdata(serdev, kb);
+
+	ret = pogo_parse_dt(kb);
+	if (ret)
+		return ret;
+
+	serdev_device_set_client_ops(serdev, &pogo_serdev_ops);
+	ret = devm_serdev_device_open(dev, serdev);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to open serdev\n");
+
+	ret = serdev_device_set_baudrate(serdev, POGO_BAUD);
+	if (ret != POGO_BAUD)
+		dev_warn(dev, "baudrate set to %d instead of %d\n", ret, POGO_BAUD);
+	serdev_device_set_flow_control(serdev, false);
+	ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to set parity\n");
+
+	ret = pogo_register_backlight(kb);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to register backlight\n");
+
+	ret = devm_request_irq(dev, kb->wake_irq, pogo_wake_isr,
+			       IRQF_TRIGGER_FALLING | IRQF_NO_AUTOEN,
+			       dev_name(dev), kb);
+	if (ret)
+		return dev_err_probe(dev, ret, "failed to request wake irq\n");
+
+	/* evaluate the current attach state; enables the irq when idle */
+	schedule_delayed_work(&kb->detect_work,
+			      msecs_to_jiffies(POGO_DETECT_DEBOUNCE_MS));
+
+	dev_info(dev, "pogo keyboard link ready (wake irq %d)\n", kb->wake_irq);
+	return 0;
+}
+
+static void pogo_remove(struct serdev_device *serdev)
+{
+	struct pogo_kbd *kb = serdev_device_get_drvdata(serdev);
+
+	pogo_wake_irq_enable(kb, false);
+	cancel_delayed_work_sync(&kb->detect_work);
+	cancel_delayed_work_sync(&kb->hb_work);
+	cancel_work_sync(&kb->plug_work);
+	cancel_work_sync(&kb->led_work);
+	pogo_power_off(kb);
+}
+
+static const struct of_device_id pogo_of_match[] = {
+	{ .compatible = "oneplus,pogo-keyboard" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, pogo_of_match);
+
+static struct serdev_device_driver pogo_driver = {
+	.probe = pogo_probe,
+	.remove = pogo_remove,
+	.driver = {
+		.name = "oneplus-pogo-kbd",
+		.of_match_table = pogo_of_match,
+		.dev_groups = pogo_groups,
+	},
+};
+module_serdev_device_driver(pogo_driver);
+
+MODULE_DESCRIPTION("OnePlus/OPPO Pad pogo-pin keyboard driver");
+MODULE_LICENSE("GPL");
-- 
2.53.0


  parent reply	other threads:[~2026-10-09 12:21 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-09 12:20 [PATCH 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards wyibo6109
2026-10-09 12:20 ` [PATCH 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard binding wyibo6109
2026-10-09 12:28   ` sashiko-bot
2026-10-09 13:22   ` Krzysztof Kozlowski
2026-10-09 12:20 ` wyibo6109 [this message]
2026-10-09 12:38   ` [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver sashiko-bot
  -- strict thread matches above, loose matches on Subject: below --
2026-10-09 12:11 [PATCH 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards Yibo Wang
2026-10-09 12:11 ` [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver Yibo Wang
2026-10-09 12:24   ` sashiko-bot

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=20261009122046.551915-3-wyibo6109@gmail.com \
    --to=wyibo6109@gmail.com \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=dmitry.torokhov@gmail.com \
    --cc=krzk+dt@kernel.org \
    --cc=linux-input@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=robh@kernel.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