* [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver
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 ` Yibo Wang
2026-10-09 12:24 ` sashiko-bot
0 siblings, 1 reply; 8+ messages in thread
From: Yibo Wang @ 2026-10-09 12:11 UTC (permalink / raw)
To: Dmitry Torokhov, linux-input
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
linux-kernel, Yibo Wang
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
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards
@ 2026-10-09 12:20 wyibo6109
2026-10-09 12:20 ` [PATCH 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard binding wyibo6109
2026-10-09 12:20 ` [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver wyibo6109
0 siblings, 2 replies; 8+ messages in thread
From: wyibo6109 @ 2026-10-09 12:20 UTC (permalink / raw)
To: Dmitry Torokhov, linux-input
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
linux-kernel, Yibo Wang
From: Yibo Wang <wyibo6109@gmail.com>
This series adds support for the magnetic pogo-pin smart keyboards of
the OnePlus Pad 3 Pro / Pad 4 and the OPPO Pad 5 Pro. The keyboard is
built around a Tinno MCU attached to a tablet UART. Both sides share a
single data wire, so the host gates its transmission onto the bus with a
PMIC GPIO for the duration of a frame, and every host frame is echoed
back on RX.
The MCU speaks a framed "one wire bus" protocol over the half-duplex
921600 8N1 UART: 8x 0x55 preamble, 0xF1, src/dst addresses, command,
length, payload, CRC16-IBM (poly 0x8005, init 0xC596), 0xFE and 4x 0xAA.
The protocol was reverse engineered from the vendor kernel driver; this
is a clean-room serdev reimplementation that replaces the downstream
platform driver, which depends on a private msm_geni_serial callback and
a user-space held tty fd.
Patch 1 adds the DT binding for the "oneplus,pogo-keyboard" device,
describing the supply, TX-gate and wake GPIOs and the touchpad geometry,
with a MAINTAINERS entry.
Patch 2 adds the driver: keyboard matrix, touchpad, function and URL
keys, caps-lock LED, attach detection through the wake GPIO plus a
100 ms heartbeat timeout, and the keyboard backlight as a
"pogo-keyboard::kbd_backlight" LED class device so UPower and the
desktop keyboard-backlight controls drive it through the standard
interface (the level is cached while detached and restored on
re-attach). A sysfs battery level is exported for the keyboards that
carry a battery.
Tested on the OnePlus Pad 4 (iceland) running Linux 7.2.0-sm8850
(v7.2-based iceland tree).
---
.../bindings/input/oneplus,pogo-keyboard.yaml | 81 ++
MAINTAINERS | 7 +
drivers/input/keyboard/Kconfig | 11 +
drivers/input/keyboard/Makefile | 1 +
drivers/input/keyboard/oneplus-pogo-kbd.c | 1109 +++++++++++++++++
5 files changed, 1209 insertions(+)
--
2.53.0
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard binding
2026-10-09 12:20 [PATCH 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards wyibo6109
@ 2026-10-09 12:20 ` wyibo6109
2026-10-09 12:28 ` sashiko-bot
2026-10-09 13:22 ` Krzysztof Kozlowski
2026-10-09 12:20 ` [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver wyibo6109
1 sibling, 2 replies; 8+ messages in thread
From: wyibo6109 @ 2026-10-09 12:20 UTC (permalink / raw)
To: Dmitry Torokhov, linux-input
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
linux-kernel, Yibo Wang
From: Yibo Wang <wyibo6109@gmail.com>
Binding for the Tinno keyboard MCU used by the OnePlus Pad 3 Pro /
Pad 4 and OPPO Pad 5 Pro smart keyboards, attached to a tablet UART.
Describes the supply, TX-gate and wake GPIOs and the touchpad
geometry properties, with a MAINTAINERS entry.
Signed-off-by: Yibo Wang <wyibo6109@gmail.com>
---
.../bindings/input/oneplus,pogo-keyboard.yaml | 81 +++++++++++++++++++
MAINTAINERS | 7 ++
2 files changed, 88 insertions(+)
create mode 100644 Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml
diff --git a/Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml b/Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml
new file mode 100644
index 000000000..849558458
--- /dev/null
+++ b/Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml
@@ -0,0 +1,81 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+# Copyright (c) 2026 Yibo Wang <wyibo6109@gmail.com>
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/input/oneplus,pogo-keyboard.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: OnePlus / OPPO Pad pogo-pin smart keyboard
+
+maintainers:
+ - Yibo Wang <wyibo6109@gmail.com>
+
+description: |
+ The Tinno keyboard MCU used by the OnePlus Pad 3 Pro / Pad 4 and OPPO
+ Pad 5 Pro smart keyboards. It is attached to a tablet UART and speaks
+ a framed half-duplex protocol over a shared single wire; host
+ transmission is gated onto the wire by a PMIC GPIO for the duration of
+ a frame. The device provides a keyboard matrix, a touchpad and a
+ keyboard backlight.
+
+allOf:
+ - $ref: /schemas/serial/serial-peripheral-props.yaml#
+
+properties:
+ compatible:
+ const: oneplus,pogo-keyboard
+
+ power-gpios:
+ description: GPIO enabling the keyboard supply, held high while attached.
+ maxItems: 1
+
+ tx-enable-gpios:
+ description: GPIO gating host TX onto the shared one-wire bus.
+ maxItems: 1
+
+ wake-gpios:
+ description: GPIO signalling attach/detach, also used as the wake line.
+ maxItems: 1
+
+ touchpad-xy-max:
+ description: Touchpad maximum X and Y coordinates in units.
+ items:
+ - description: maximum X
+ - description: maximum Y
+ minItems: 2
+ maxItems: 2
+
+ touchpad-xy-resolution:
+ description: Touchpad X and Y resolution in units per millimetre.
+ items:
+ - description: X resolution
+ - description: Y resolution
+ minItems: 2
+ maxItems: 2
+
+required:
+ - compatible
+ - power-gpios
+ - tx-enable-gpios
+ - wake-gpios
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+
+ serial {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ keyboard {
+ compatible = "oneplus,pogo-keyboard";
+ power-gpios = <&pmic_gpios 12 GPIO_ACTIVE_HIGH>;
+ tx-enable-gpios = <&pmic2_gpios 6 GPIO_ACTIVE_HIGH>;
+ wake-gpios = <&tlmm 151 GPIO_ACTIVE_LOW>;
+ touchpad-xy-max = <2560 1440>;
+ touchpad-xy-resolution = <24 25>;
+ };
+ };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index 8014b9f82..e978a26c1 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -20149,6 +20149,13 @@ S: Maintained
T: git git://linuxtv.org/media.git
F: drivers/media/i2c/ov9734.c
+ONEPLUS POGO-PIN KEYBOARD DRIVER
+M: Yibo Wang <wyibo6109@gmail.com>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml
+F: drivers/input/keyboard/oneplus-pogo-kbd.c
+
ONBOARD USB HUB DRIVER
M: Matthias Kaehlcke <mka@chromium.org>
L: linux-usb@vger.kernel.org
--
2.53.0
^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver
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:20 ` wyibo6109
2026-10-09 12:38 ` sashiko-bot
1 sibling, 1 reply; 8+ messages in thread
From: wyibo6109 @ 2026-10-09 12:20 UTC (permalink / raw)
To: Dmitry Torokhov, linux-input
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
linux-kernel, Yibo Wang
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
^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver
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
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-10-09 12:24 UTC (permalink / raw)
To: Yibo Wang; +Cc: robh, dmitry.torokhov, conor+dt, devicetree, linux-input
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] input: Use-After-Free in workqueues due to incorrect teardown sequence
- [High] input: Resource leak and UAF due to race between plug_work and power_off
- [Medium] input: Data race and corruption on kb->resp buffer in UART RX path
- [Medium] input: Data race on wake_irq_enabled state causing permanent IRQ disablement
- [Medium] input: Missing Kconfig dependencies for INPUT_MT and LEDS_CLASS
- [Low] input: Success logging in probe path violates subsystem guidelines
--
commit 3f827ef8e9c7e47ecd0b1c17c9418abab787c03c
Author: Yibo Wang <wyibo6109@gmail.com>
Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver
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.
> diff --git a/drivers/input/keyboard/Kconfig b/drivers/input/keyboard/Kconfig
> --- a/drivers/input/keyboard/Kconfig
> +++ b/drivers/input/keyboard/Kconfig
> @@ -768,4 +768,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
[Severity: Medium]
Should this Kconfig entry select CONFIG_INPUT_MT and depend on
CONFIG_LEDS_CLASS?
The driver explicitly calls input_mt_init_slots() and
devm_led_classdev_register_ext(). If CONFIG_INPUT_MT is disabled, the kernel
will fail to link. If CONFIG_LEDS_CLASS is configured as a module and this
driver is built-in, it will cause a linker error or silently drop
functionality. Standard kernel practices require this Kconfig to select
CONFIG_INPUT_MT and depend on CONFIG_LEDS_CLASS.
> diff --git a/drivers/input/keyboard/oneplus-pogo-kbd.c b/drivers/input/keyboard/oneplus-pogo-kbd.c
> --- /dev/null
> +++ b/drivers/input/keyboard/oneplus-pogo-kbd.c
[ ... ]
> +static int pogo_xfer(struct pogo_kbd *kb, u8 cmd, const u8 *payload, u8 len,
> + u8 *resp, unsigned int resp_size)
> +{
[ ... ]
> + 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;
> +}
[Severity: Medium]
Is there a data race on kb->resp when timeouts occur?
If wait_for_completion_timeout() times out, it proceeds to the next loop
iteration and calls reinit_completion(&kb->resp_done) without setting
kb->tx_pending = false.
If a delayed reply from the previous attempt arrives concurrently, the
serdev RX callback pogo_handle_frame() sees kb->tx_pending == true and
writes the payload into kb->resp locklessly:
if (READ_ONCE(kb->tx_pending) && cmd == kb->tx_cmd + 1) {
kb->resp_len = min_t(unsigned int, len + 2, sizeof(kb->resp));
memcpy(kb->resp, &f[3], kb->resp_len);
This can overwrite kb->resp while pogo_xfer() is concurrently reading from
it, or it can spuriously complete the new iteration before the packet is
even sent.
[ ... ]
> +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;
> +}
[Severity: Medium]
Does updating the state after enabling the IRQ cause a race condition?
If the wake line is already asserted, the IRQ can fire immediately on another
CPU as soon as enable_irq() executes. The ISR pogo_wake_isr() would then run,
call disable_irq_nosync(), and assign kb->wake_irq_enabled = false.
When the original CPU resumes, it unconditionally assigns
kb->wake_irq_enabled = true, overwriting the ISR's state update. The
software state now claims the IRQ is enabled, while the hardware IRQ is
actually disabled.
Subsequent calls to pogo_wake_irq_enable() will return early without enabling
the IRQ, leaving the keyboard permanently asleep.
[ ... ]
> +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");
> + }
> +}
[Severity: High]
Can a race between pogo_plug_work() and pogo_power_off() leak input devices?
In pogo_plug_work(), device registration proceeds outside the lock:
if (!kb->kbd) {
ret = pogo_register_inputs(kb);
Concurrently, pogo_hb_work() can call pogo_power_off(), which sets
kb->connected = false and calls pogo_unregister_inputs(). Since plug_work
has not yet assigned kb->kbd under the lock, pogo_unregister_inputs() does
nothing.
After plug_work completes registration and assigns kb->kbd, if the driver
is unbound, pogo_remove() calls pogo_power_off(). Because kb->connected is
already false, it skips unregistering the devices.
This leaks the input devices, and their private data pointers retain the
freed kb struct, causing a use-after-free when accessed by userspace.
[ ... ]
> +static int pogo_probe(struct serdev_device *serdev)
> +{
[ ... ]
> + /* 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;
> +}
[Severity: Low]
Should this success message be removed to match subsystem guidelines?
The driver prints an explicit success log message upon normal completion of
the probe sequence. The LED and Input maintainer guidelines request that
drivers only log errors or warnings.
[ ... ]
> +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);
> +}
[Severity: High]
Could this teardown sequence leave a window for use-after-free bugs?
First, cancel_work_sync(&kb->led_work) is called before pogo_power_off(kb)
unregisters the input devices. Userspace can trigger pogo_kbd_event() via
sysfs/evdev and queue led_work after the cancellation.
Second, the serdev port is managed via devm_serdev_device_open(), so it
remains open until devres cleanup runs after pogo_remove() returns. An
incoming UART frame could trigger pogo_receive_buf() and queue plug_work
on the freed memory.
Third, detect_work and hb_work can schedule each other. Sequentially
cancelling them without a state flag (e.g., kb->quitting = true) allows
hb_work to queue detect_work immediately after detect_work is cancelled.
Also, if pogo_probe() fails after opening the serdev, plug_work is never
cancelled before devres cleanup.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261009121143.550498-1-2024312277@stu.hit.edu.cn?part=2
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard binding
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
1 sibling, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-10-09 12:28 UTC (permalink / raw)
To: wyibo6109; +Cc: devicetree, conor+dt, robh, dmitry.torokhov, linux-input
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Low] dt-bindings: missing vendor prefix and reuse of standard touchscreen properties
- [Low] dt-bindings: custom array properties missing type $ref
- [Low] dt-bindings: additionalProperties: false incorrectly used with allOf $ref
- [Low] dt-bindings: unnecessary #address-cells and #size-cells in example
- [Low] MAINTAINERS: alphabetical ordering violation
--
commit ef349c1a2a4dec7c790c73c2c1a20cb2d95556c8
Author: Yibo Wang <wyibo6109@gmail.com>
dt-bindings: input: add OnePlus pogo-pin keyboard binding
Binding for the Tinno keyboard MCU used by the OnePlus Pad 3 Pro /
Pad 4 and OPPO Pad 5 Pro smart keyboards, attached to a tablet UART.
> diff --git a/Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml b/Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml
> new file mode 100644
> index 0000000000000..3086a2c4e2895
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml
> @@ -0,0 +1,81 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
[ ... ]
> +allOf:
> + - $ref: /schemas/serial/serial-peripheral-props.yaml#
> +
> +properties:
> + compatible:
> + const: oneplus,pogo-keyboard
[ ... ]
> + touchpad-xy-max:
> + description: Touchpad maximum X and Y coordinates in units.
> + items:
> + - description: maximum X
> + - description: maximum Y
> + minItems: 2
> + maxItems: 2
> +
> + touchpad-xy-resolution:
> + description: Touchpad X and Y resolution in units per millimetre.
> + items:
> + - description: X resolution
> + - description: Y resolution
> + minItems: 2
> + maxItems: 2
[Severity: Low]
Does this bypass the standard touchscreen properties? The binding could reuse
properties from touchscreen.yaml, such as touchscreen-size-x,
touchscreen-size-y, touchscreen-x-mm, and touchscreen-y-mm, rather than
introducing custom variations.
If custom properties are strictly necessary here, should they include a
vendor prefix like oneplus,touchpad-xy-max?
Additionally, since these custom properties define an items array, do they
also need an explicit type reference (such as
$ref: /schemas/types.yaml#/definitions/uint32-array)? Without a standard
suffix, device tree schema tools might fail meta-schema validation because
they cannot automatically infer the type.
> +
> +required:
> + - compatible
> + - power-gpios
> + - tx-enable-gpios
> + - wake-gpios
> +
> +additionalProperties: false
[Severity: Low]
Does this strict constraint incorrectly override the referenced schema?
Because the schema imports serial-peripheral-props.yaml via the allOf block,
JSON schema evaluation will not account for those imported properties under
additionalProperties. Does this cause valid serial properties (like
max-speed) to be incorrectly rejected?
Could this be resolved by using unevaluatedProperties: false instead?
> +
> +examples:
> + - |
> + #include <dt-bindings/gpio/gpio.h>
> +
> + serial {
> + #address-cells = <1>;
> + #size-cells = <0>;
> +
> + keyboard {
[Severity: Low]
Since the child keyboard node omits a reg property because it is a simple
point-to-point UART peripheral, are #address-cells and #size-cells needed on
the parent node?
Could this trigger the avoid_unnecessary_addr_size dtc warning during make
dt_binding_check?
> + compatible = "oneplus,pogo-keyboard";
> + power-gpios = <&pmic_gpios 12 GPIO_ACTIVE_HIGH>;
> + tx-enable-gpios = <&pmic2_gpios 6 GPIO_ACTIVE_HIGH>;
> + wake-gpios = <&tlmm 151 GPIO_ACTIVE_LOW>;
> + touchpad-xy-max = <2560 1440>;
> + touchpad-xy-resolution = <24 25>;
> + };
> + };
> +...
> diff --git a/MAINTAINERS b/MAINTAINERS
> index b8c4e1080af94..d0adba45c8f0b 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -20147,6 +20147,13 @@ S: Maintained
> T: git git://linuxtv.org/media.git
> F: drivers/media/i2c/ov9734.c
>
> +ONEPLUS POGO-PIN KEYBOARD DRIVER
> +M: Yibo Wang <wyibo6109@gmail.com>
> +L: linux-input@vger.kernel.org
> +S: Maintained
> +F: Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml
> +F: drivers/input/keyboard/oneplus-pogo-kbd.c
> +
> ONBOARD USB HUB DRIVER
[Severity: Low]
Should ONEPLUS POGO-PIN KEYBOARD DRIVER be placed after ONBOARD USB HUB
DRIVER?
Alphabetically, ONEPLUS comes after ONBOARD, so inserting it here appears to
break the alphabetical ordering requested by the MAINTAINERS file.
> M: Matthias Kaehlcke <mka@chromium.org>
> L: linux-usb@vger.kernel.org
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261009122046.551915-1-wyibo6109@gmail.com?part=1
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver
2026-10-09 12:20 ` [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver wyibo6109
@ 2026-10-09 12:38 ` sashiko-bot
0 siblings, 0 replies; 8+ messages in thread
From: sashiko-bot @ 2026-10-09 12:38 UTC (permalink / raw)
To: wyibo6109; +Cc: conor+dt, robh, linux-input, devicetree, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] input: UAF in led_work due to teardown ordering
- [High] serdev: UAF in plug_work due to active port during devres teardown
- [High] workqueue: UAF due to detect_work and hb_work cyclic dependency
- [High] workqueue: UAF due to pogo_wake_irq_enable missing synchronize_irq
- [High] input: Race between plug_work and power_off leaks input devices
- [Low] leds: Success logging in probe violates guidelines
--
commit 0ce1872d7cb39e14970c678cd47393649379aea8
Author: Yibo Wang <wyibo6109@gmail.com>
Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver
Adds a serdev driver for the Tinno keyboard MCU used by OnePlus and OPPO
Pad smart keyboards. Communication happens over a half-duplex UART using
a custom framed protocol. The driver exposes touchpad, keys, and backlight
controls to userspace.
> diff --git a/drivers/input/keyboard/oneplus-pogo-kbd.c b/drivers/input/keyboard/oneplus-pogo-kbd.c
> index 0000000000000..589f31ecef848
> --- /dev/null
> +++ b/drivers/input/keyboard/oneplus-pogo-kbd.c
> @@ -778,7 +778,7 @@
> static void pogo_wake_irq_enable(struct pogo_kbd *kb, bool enable)
> {
> if (enable == kb->wake_irq_enabled)
> return;
[Severity: High]
If pogo_wake_isr() fires concurrently, sets kb->wake_irq_enabled = false,
and then pogo_remove() calls pogo_wake_irq_enable(kb, false), will this
return early and bypass disable_irq()?
Since disable_irq() also acts as a synchronize_irq(), missing it might
allow the preempted ISR to resume and queue detect_work after it has
already been cancelled in pogo_remove().
[ ... ]
> @@ -849,10 +849,10 @@
> 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);
[Severity: High]
Can a race between pogo_plug_work() and pogo_power_off() leak input
devices?
If pogo_plug_work() evaluates kb->connected as true, and concurrently
pogo_power_off() sets kb->connected = false (unregistering nothing since
kb->kbd is still NULL):
- pogo_plug_work() would then allocate and register the input devices.
- The state becomes desynchronized: kb->connected is false, but devices
are registered.
When the driver unbinds, pogo_remove() calls pogo_power_off(), which will
skip unregistering the devices because kb->connected is false. Will this
leave the input devices leaked and pointing to freed memory?
[ ... ]
> @@ -1074,6 +1074,6 @@
> /* 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);
[Severity: Low]
This isn't a bug, but does this success log violate the LED subsystem
guidelines? The guidelines suggest only logging errors or warnings during
probe, to avoid cluttering the kernel log with success messages.
> return 0;
> }
[ ... ]
> @@ -1084,7 +1084,7 @@
> 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);
[Severity: High]
Because detect_work and hb_work can queue each other, and neither checks a
shared teardown flag (like kb->going_away), could hb_work re-arm
detect_work after detect_work was cancelled?
If hb_work is running concurrently during pogo_remove():
1. cancel_delayed_work_sync() for detect_work returns.
2. hb_work calls schedule_delayed_work() for detect_work.
3. cancel_delayed_work_sync() for hb_work waits for hb_work to finish.
This leaves detect_work pending after pogo_remove() returns, which would
lead to a use-after-free when devres frees the kb structure.
> cancel_work_sync(&kb->plug_work);
[Severity: High]
Since the serdev port is managed by devm_serdev_device_open(), the UART
receiver remains active until devres cleanup, which executes after
pogo_remove() returns.
Could an incoming UART frame trigger pogo_handle_sync() and re-queue
plug_work after this cancellation? If the work remains queued after devres
frees the kb memory, it would cause a use-after-free.
Similarly, if pogo_probe() fails after devm_serdev_device_open(), could an
incoming frame schedule the work on the error path where it is never
cancelled? Does the UART receiver need to be closed or the callback
disabled prior to work cancellation?
> cancel_work_sync(&kb->led_work);
> pogo_power_off(kb);
[Severity: High]
Because pogo_power_off(kb) is called after cancelling led_work, the input
devices are still fully registered and active during cancellation.
Can a concurrent EV_LED event routed by the input core trigger
pogo_kbd_event(), which unconditionally calls schedule_work() for
led_work? If led_work is re-armed after it was cancelled, it will execute
on freed memory once devres cleans up. Should the input devices be
unregistered before cancelling led_work?
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261009122046.551915-1-wyibo6109@gmail.com?part=2
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard binding
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
1 sibling, 0 replies; 8+ messages in thread
From: Krzysztof Kozlowski @ 2026-10-09 13:22 UTC (permalink / raw)
To: wyibo6109, Dmitry Torokhov, linux-input
Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, devicetree,
linux-kernel
On 09/10/2026 14:20, wyibo6109@gmail.com wrote:
A nit, subject: drop second/last, redundant "binding". The "dt-bindings"
prefix is already stating that these are bindings.
See also:
https://elixir.bootlin.com/linux/v7.1-rc7/source/Documentation/devicetree/bindings/submitting-patches.rst#L23
> + touchpad-xy-max:
> + description: Touchpad maximum X and Y coordinates in units.
> + items:
> + - description: maximum X
> + - description: maximum Y
> + minItems: 2
> + maxItems: 2
None of this was tested. Anyway, use existing properties from
touchscreen schema for example or actually better explain why this is
not deducible from the compatible.
> +
> + touchpad-xy-resolution:
> + description: Touchpad X and Y resolution in units per millimetre.
> + items:
> + - description: X resolution
> + - description: Y resolution
> + minItems: 2
> + maxItems: 2
> +
> +required:
> + - compatible
> + - power-gpios
> + - tx-enable-gpios
> + - wake-gpios
> +
> +additionalProperties: false
> +
> +examples:
> + - |
> + #include <dt-bindings/gpio/gpio.h>
> +
> + serial {
> + #address-cells = <1>;
> + #size-cells = <0>;
Drop node
> +
> + keyboard {
> + compatible = "oneplus,pogo-keyboard";
> + power-gpios = <&pmic_gpios 12 GPIO_ACTIVE_HIGH>;
> + tx-enable-gpios = <&pmic2_gpios 6 GPIO_ACTIVE_HIGH>;
> + wake-gpios = <&tlmm 151 GPIO_ACTIVE_LOW>;
> + touchpad-xy-max = <2560 1440>;
> + touchpad-xy-resolution = <24 25>;
> + };
> + };
> +...
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 8014b9f82..e978a26c1 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -20149,6 +20149,13 @@ S: Maintained
> T: git git://linuxtv.org/media.git
> F: drivers/media/i2c/ov9734.c
>
> +ONEPLUS POGO-PIN KEYBOARD DRIVER
> +M: Yibo Wang <wyibo6109@gmail.com>
> +L: linux-input@vger.kernel.org
> +S: Maintained
> +F: Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml
> +F: drivers/input/keyboard/oneplus-pogo-kbd.c
There is no such file.
> +
> ONBOARD USB HUB DRIVER
> M: Matthias Kaehlcke <mka@chromium.org>
> L: linux-usb@vger.kernel.org
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-10-09 13:22 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver wyibo6109
2026-10-09 12:38 ` 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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox