* [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
` (2 more replies)
0 siblings, 3 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 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-10 14:38 ` [PATCH v2 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards wyibo6109 2 siblings, 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> 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 [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 13:22 ` Krzysztof Kozlowski 0 siblings, 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
* [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-10 14:38 ` [PATCH v2 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards wyibo6109 2 siblings, 0 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> 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 [flat|nested] 8+ messages in thread
* [PATCH v2 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards 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 ` [PATCH 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver wyibo6109 @ 2026-10-10 14:38 ` wyibo6109 2026-10-10 14:38 ` [PATCH v2 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard wyibo6109 2026-10-10 14:38 ` [PATCH v2 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver wyibo6109 2 siblings, 2 replies; 8+ messages in thread From: wyibo6109 @ 2026-10-10 14:38 UTC (permalink / raw) To: Dmitry Torokhov, linux-input Cc: Krzysztof Kozlowski, Rob Herring, 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, 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. Changes in v2, addressing Krzysztof Kozlowski's review of the binding: - dropped the redundant "binding" from the dt-bindings subject; - replaced the custom oneplus,touchpad-xy-max/-resolution properties with the common touchscreen properties touchscreen-size-x/y and touchscreen-x-mm/y-mm, parsed with touchscreen_parse_properties() in the driver: the MCU protocol has no way to query the touchpad geometry and the same MCU is shipped in keyboards with different touchpad matrices, so the geometry stays in DT but uses the common schema instead of vendor properties; - dropped the serial wrapper node from the binding example; - the MAINTAINERS entry added with the binding references only the binding file; the driver F: line is added together with the driver patch. Tested on the OnePlus Pad 4 (iceland) running Linux 7.2.0-sm8850 (v7.2-based iceland tree). Yibo Wang (2): dt-bindings: input: add OnePlus pogo-pin keyboard Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver .../bindings/input/oneplus,pogo-keyboard.yaml | 66 + MAINTAINERS | 7 + drivers/input/keyboard/Kconfig | 13 + drivers/input/keyboard/Makefile | 1 + drivers/input/keyboard/oneplus-pogo-kbd.c | 1196 +++++++++++++++++ 5 files changed, 1283 insertions(+) create mode 100644 Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml create mode 100644 drivers/input/keyboard/oneplus-pogo-kbd.c -- 2.53.0 ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v2 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard 2026-10-10 14:38 ` [PATCH v2 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards wyibo6109 @ 2026-10-10 14:38 ` wyibo6109 2026-10-10 14:38 ` [PATCH v2 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver wyibo6109 1 sibling, 0 replies; 8+ messages in thread From: wyibo6109 @ 2026-10-10 14:38 UTC (permalink / raw) To: Dmitry Torokhov, linux-input Cc: Krzysztof Kozlowski, Rob Herring, 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, with a MAINTAINERS entry. The touchpad is described through the common touchscreen properties (touchscreen-size-x/y, touchscreen-x-mm/y-mm): the MCU protocol has no way to query the touchpad geometry and the same MCU is shipped in keyboards with different touchpad matrices. Signed-off-by: Yibo Wang <wyibo6109@gmail.com> --- .../bindings/input/oneplus,pogo-keyboard.yaml | 66 +++++++++++++++++++ MAINTAINERS | 6 ++ 2 files changed, 72 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..9055ececc --- /dev/null +++ b/Documentation/devicetree/bindings/input/oneplus,pogo-keyboard.yaml @@ -0,0 +1,66 @@ +# 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# + - $ref: /schemas/input/touchscreen/touchscreen.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 + + touchscreen-size-x: true + touchscreen-size-y: true + touchscreen-x-mm: true + touchscreen-y-mm: true + +required: + - compatible + - power-gpios + - tx-enable-gpios + - wake-gpios + +unevaluatedProperties: false + +examples: + - | + #include <dt-bindings/gpio/gpio.h> + + 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>; + touchscreen-size-x = <2561>; + touchscreen-size-y = <1441>; + }; +... diff --git a/MAINTAINERS b/MAINTAINERS index 8014b9f82..9c29eb961 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20156,6 +20156,12 @@ S: Maintained F: Documentation/ABI/testing/sysfs-bus-platform-onboard-usb-dev F: drivers/usb/misc/onboard_usb_dev.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 + ONENAND FLASH DRIVER M: Kyungmin Park <kyungmin.park@samsung.com> L: linux-mtd@lists.infradead.org -- 2.53.0 ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v2 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver 2026-10-10 14:38 ` [PATCH v2 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards wyibo6109 2026-10-10 14:38 ` [PATCH v2 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard wyibo6109 @ 2026-10-10 14:38 ` wyibo6109 1 sibling, 0 replies; 8+ messages in thread From: wyibo6109 @ 2026-10-10 14:38 UTC (permalink / raw) To: Dmitry Torokhov, linux-input Cc: Krzysztof Kozlowski, Rob Herring, 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. The touchpad geometry defaults to the tested OnePlus Pad 4 keyboard and can be overridden with the standard touchscreen DT properties parsed by touchscreen_parse_properties(). The MAINTAINERS entry from the binding patch gains the driver file. Signed-off-by: Yibo Wang <wyibo6109@gmail.com> --- MAINTAINERS | 1 + drivers/input/keyboard/Kconfig | 13 + drivers/input/keyboard/Makefile | 1 + drivers/input/keyboard/oneplus-pogo-kbd.c | 1196 +++++++++++++++++++++ 4 files changed, 1211 insertions(+) create mode 100644 drivers/input/keyboard/oneplus-pogo-kbd.c diff --git a/MAINTAINERS b/MAINTAINERS index 9c29eb961..d2b83a637 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -20161,6 +20161,7 @@ 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 ONENAND FLASH DRIVER M: Kyungmin Park <kyungmin.park@samsung.com> diff --git a/drivers/input/keyboard/Kconfig b/drivers/input/keyboard/Kconfig index 9d1019ba0..011b9248b 100644 --- a/drivers/input/keyboard/Kconfig +++ b/drivers/input/keyboard/Kconfig @@ -806,4 +806,17 @@ 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 + depends on LEDS_CLASS + select INPUT_MT + 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..298f0eff5 --- /dev/null +++ b/drivers/input/keyboard/oneplus-pogo-kbd.c @@ -0,0 +1,1196 @@ +// 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/input/touchscreen.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_TP_MAX_X 2560 /* fallback geometry, DT can override */ +#define POGO_TP_MAX_Y 1440 +#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 mutex state_lock; + struct completion echo_done; + struct completion resp_done; + u8 tx_cmd; + bool tx_pending; + bool quitting; + 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]; + 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; + + /* + * Drop any late echo/reply of the previous attempt: with + * tx_pending cleared the RX path ignores frames until the + * transfer below re-arms, so a delayed reply cannot complete + * the fresh iteration or race the resp buffer. + */ + WRITE_ONCE(kb->tx_pending, false); + 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); + + /* stop the RX path before copying the reply out */ + WRITE_ONCE(kb->tx_pending, false); + 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; + if (!READ_ONCE(kb->quitting)) + schedule_work(&kb->led_work); + return 0; +} + +static int pogo_register_inputs(struct pogo_kbd *kb) +{ + struct input_dev *kbd, *tp; + const char *name; + u32 x_mm, y_mm, res; + 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, POGO_TP_MAX_X, 0, 0); + input_set_abs_params(tp, ABS_MT_POSITION_Y, 0, POGO_TP_MAX_Y, 0, 0); + /* standard touchscreen DT properties override the defaults above */ + touchscreen_parse_properties(tp, true, NULL); + if (!device_property_read_u32(kb->dev, "touchscreen-x-mm", &x_mm) && + !device_property_read_u32(kb->dev, "touchscreen-y-mm", &y_mm) && + x_mm && y_mm) { + res = (input_abs_get_max(tp, ABS_MT_POSITION_X) + 1) / x_mm; + input_abs_set_res(tp, ABS_MT_POSITION_X, res); + res = (input_abs_get_max(tp, ABS_MT_POSITION_Y) + 1) / y_mm; + input_abs_set_res(tp, ABS_MT_POSITION_Y, res); + } + 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; + if (!READ_ONCE(kb->quitting)) + 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; + + /* teardown: no new work off the RX path once remove started */ + if (READ_ONCE(kb->quitting)) + return 0; + + 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) { + /* + * Update the state before enabling: the ISR may fire on + * another CPU the moment enable_irq() runs and flip the + * state to false; assigning after enable_irq() would + * clobber that and desynchronise software and hardware. + */ + kb->wake_irq_enabled = true; + enable_irq(kb->wake_irq); + } else { + /* disable_irq() waits for a running ISR before returning */ + disable_irq(kb->wake_irq); + kb->wake_irq_enabled = false; + } +} + +static void pogo_power_on(struct pogo_kbd *kb) +{ + dev_dbg(kb->dev, "power on\n"); + mutex_lock(&kb->state_lock); + kb->connected = false; + kb->hb_seen = false; + kb->battery_level = -1; + kb->last_rx = jiffies; + kb->powered = true; + mutex_unlock(&kb->state_lock); + 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) +{ + bool was_connected; + + mutex_lock(&kb->state_lock); + was_connected = kb->connected; + kb->connected = false; + kb->powered = false; + mutex_unlock(&kb->state_lock); + + dev_dbg(kb->dev, "power off\n"); + gpiod_set_value_cansleep(kb->power_gpio, 0); + if (was_connected) + dev_info(kb->dev, "keyboard detached\n"); + + /* + * Unregister whenever the devices exist, not only when connected: + * pogo_plug_work() can race a disconnect and register them after + * this function already ran once, and pogo_remove() relies on this + * to release them. No-op when already unregistered. + */ + pogo_unregister_inputs(kb); +} + +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 (READ_ONCE(kb->quitting) || 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 (!READ_ONCE(kb->quitting) && 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))) { + if (!READ_ONCE(kb->quitting)) + 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 */ + if (!READ_ONCE(kb->quitting)) + 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 (READ_ONCE(kb->quitting) || !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; + } + /* + * A disconnect may have raced the registration: power_off() + * could have run before kb->kbd was assigned and therefore + * could not unregister. Re-check under state_lock and undo. + */ + mutex_lock(&kb->state_lock); + if (!kb->powered) { + mutex_unlock(&kb->state_lock); + pogo_unregister_inputs(kb); + return; + } + mutex_unlock(&kb->state_lock); + 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; + if (!READ_ONCE(kb->quitting)) + 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; + + 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"); + + 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); + mutex_init(&kb->state_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) { + ret = dev_err_probe(dev, ret, "failed to set parity\n"); + goto err_shutdown; + } + + ret = pogo_register_backlight(kb); + if (ret) { + ret = dev_err_probe(dev, ret, "failed to register backlight\n"); + goto err_shutdown; + } + + ret = devm_request_irq(dev, kb->wake_irq, pogo_wake_isr, + IRQF_TRIGGER_FALLING | IRQF_NO_AUTOEN, + dev_name(dev), kb); + if (ret) { + ret = dev_err_probe(dev, ret, "failed to request wake irq\n"); + goto err_shutdown; + } + + /* evaluate the current attach state; enables the irq when idle */ + schedule_delayed_work(&kb->detect_work, + msecs_to_jiffies(POGO_DETECT_DEBOUNCE_MS)); + + dev_dbg(dev, "pogo keyboard link ready (wake irq %d)\n", kb->wake_irq); + return 0; + +err_shutdown: + /* + * The serdev stays open until devres cleanup runs after probe + * fails, and the RX path can already have queued work: tear it + * down here instead of relying on remove(), which is not called + * for a failed probe. + */ + WRITE_ONCE(kb->quitting, true); + 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); + return ret; +} + +static void pogo_remove(struct serdev_device *serdev) +{ + struct pogo_kbd *kb = serdev_device_get_drvdata(serdev); + + /* + * Stop the world first: with quitting set, the RX path and the + * works stop queueing each other, so the cancels below cannot be + * undone again. The wake IRQ is disabled (and any in-flight ISR + * finished) before detect_work is cancelled. + */ + WRITE_ONCE(kb->quitting, true); + 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); + + /* unregisters the input devices, the last possible led_work queuer */ + pogo_power_off(kb); + + /* flush a led_work queued before the inputs went away */ + cancel_work_sync(&kb->led_work); +} + +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 [flat|nested] 8+ messages in thread
* [PATCH 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards @ 2026-10-09 12:11 Yibo Wang 2026-10-09 12:11 ` [PATCH 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard binding Yibo Wang 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> 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:11 [PATCH 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards Yibo Wang @ 2026-10-09 12:11 ` Yibo Wang 0 siblings, 0 replies; 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> 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 [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-10-10 14:39 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 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-10 14:38 ` [PATCH v2 0/2] Input: add support for OnePlus/OPPO Pad pogo-pin keyboards wyibo6109 2026-10-10 14:38 ` [PATCH v2 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard wyibo6109 2026-10-10 14:38 ` [PATCH v2 2/2] Input: add OnePlus/OPPO Pad pogo-pin keyboard serdev driver wyibo6109 -- 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 1/2] dt-bindings: input: add OnePlus pogo-pin keyboard binding Yibo Wang
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®