* [PATCH v7 2/7] Input: synaptics-rmi4 - handle duplicate/unknown PDT entries
From: David Heidelberg via B4 Relay @ 2026-03-20 16:44 UTC (permalink / raw)
To: Kaustabh Chakraborty, Dmitry Torokhov, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Jason A. Donenfeld,
Matthias Schiffer, Vincent Huang
Cc: David Heidelberg, linux-input, devicetree, linux-kernel,
Casey Connolly, phone-devel
In-Reply-To: <20260320-synaptics-rmi4-v7-0-379360de18d0@ixit.cz>
From: Casey Connolly <casey.connolly@linaro.org>
Some third party rmi4-compatible ICs don't expose their PDT entries
very well. Add a few checks to skip duplicate entries as well as entries
for unsupported functions.
This is required to support some phones with third party displays.
Validated on a stock OnePlus 6T (original parts):
manufacturer: Synaptics, product: S3706B, fw id: 2852315
Co-developed-by: Kaustabh Chakraborty <kauschluss@disroot.org>
Signed-off-by: Kaustabh Chakraborty <kauschluss@disroot.org>
Signed-off-by: Casey Connolly <casey.connolly@linaro.org>
Co-developed-by: David Heidelberg <david@ixit.cz>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/input/rmi4/rmi_driver.c | 42 +++++++++++++++++++++++++++++++++++------
drivers/input/rmi4/rmi_driver.h | 8 ++++++++
2 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/drivers/input/rmi4/rmi_driver.c b/drivers/input/rmi4/rmi_driver.c
index ccd9338a44dbe..c7d2f68e65487 100644
--- a/drivers/input/rmi4/rmi_driver.c
+++ b/drivers/input/rmi4/rmi_driver.c
@@ -494,12 +494,39 @@ static void rmi_driver_copy_pdt_to_fd(const struct pdt_entry *pdt,
fd->function_version = pdt->function_version;
}
+static bool rmi_pdt_entry_is_valid(struct rmi_device *rmi_dev,
+ struct pdt_scan_state *state, u8 fn)
+{
+ switch (fn) {
+ case 0x01:
+ case 0x03:
+ case 0x11:
+ case 0x12:
+ case 0x30:
+ case 0x34:
+ case 0x3a:
+ case 0x54:
+ case 0x55:
+ if (state->pdts[fn] == true)
+ return false;
+ break;
+ default:
+ rmi_dbg(RMI_DEBUG_CORE, &rmi_dev->dev,
+ "PDT has unknown function number %#02x\n", fn);
+ return false;
+ }
+
+ state->pdts[fn] = true;
+ state->pdt_count++;
+ return true;
+}
+
#define RMI_SCAN_CONTINUE 0
#define RMI_SCAN_DONE 1
static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
int page,
- int *empty_pages,
+ struct pdt_scan_state *state,
void *ctx,
int (*callback)(struct rmi_device *rmi_dev,
void *ctx,
@@ -522,6 +549,9 @@ static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
if (RMI4_END_OF_PDT(pdt_entry.function_number))
break;
+ if (!rmi_pdt_entry_is_valid(rmi_dev, state, pdt_entry.function_number))
+ continue;
+
retval = callback(rmi_dev, ctx, &pdt_entry);
if (retval != RMI_SCAN_CONTINUE)
return retval;
@@ -532,11 +562,11 @@ static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
* or more is found, stop scanning.
*/
if (addr == pdt_start)
- ++*empty_pages;
+ ++state->empty_pages;
else
- *empty_pages = 0;
+ state->empty_pages = 0;
- return (data->bootloader_mode || *empty_pages >= 2) ?
+ return (data->bootloader_mode || state->empty_pages >= 2) ?
RMI_SCAN_DONE : RMI_SCAN_CONTINUE;
}
@@ -545,11 +575,11 @@ int rmi_scan_pdt(struct rmi_device *rmi_dev, void *ctx,
void *ctx, const struct pdt_entry *entry))
{
int page;
- int empty_pages = 0;
+ struct pdt_scan_state state = {0, 0, {0}};
int retval = RMI_SCAN_DONE;
for (page = 0; page <= RMI4_MAX_PAGE; page++) {
- retval = rmi_scan_pdt_page(rmi_dev, page, &empty_pages,
+ retval = rmi_scan_pdt_page(rmi_dev, page, &state,
ctx, callback);
if (retval != RMI_SCAN_CONTINUE)
break;
diff --git a/drivers/input/rmi4/rmi_driver.h b/drivers/input/rmi4/rmi_driver.h
index e84495caab151..a4ae2af93ce3a 100644
--- a/drivers/input/rmi4/rmi_driver.h
+++ b/drivers/input/rmi4/rmi_driver.h
@@ -46,6 +46,14 @@ struct pdt_entry {
u8 function_number;
};
+#define RMI_PDT_MAX 0x55
+
+struct pdt_scan_state {
+ u8 empty_pages;
+ u8 pdt_count;
+ bool pdts[RMI_PDT_MAX];
+};
+
#define RMI_REG_DESC_PRESENSE_BITS (32 * BITS_PER_BYTE)
#define RMI_REG_DESC_SUBPACKET_BITS (37 * BITS_PER_BYTE)
--
2.53.0
^ permalink raw reply related
* [PATCH v7 3/7] Input: synaptics-rmi4 - f12: use hardcoded values for aftermarket touch ICs
From: David Heidelberg via B4 Relay @ 2026-03-20 16:44 UTC (permalink / raw)
To: Kaustabh Chakraborty, Dmitry Torokhov, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Jason A. Donenfeld,
Matthias Schiffer, Vincent Huang
Cc: David Heidelberg, linux-input, devicetree, linux-kernel,
Casey Connolly, phone-devel
In-Reply-To: <20260320-synaptics-rmi4-v7-0-379360de18d0@ixit.cz>
From: Kaustabh Chakraborty <kauschluss@disroot.org>
Some replacement displays include third-party touch ICs which are
devoid of register descriptors. Create a fake data register descriptor
for such ICs and provide hardcoded default values.
It isn't possible to reliably determine if the touch IC is original or
not, so these fallback values are offered as an alternative to the error
path when register descriptors aren't available.
Signed-off-by: Kaustabh Chakraborty <kauschluss@disroot.org>
[changes for readability / codeflow, checkpatch fixes]
Signed-off-by: Casey Connolly <casey.connolly@linaro.org>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/input/rmi4/rmi_f12.c | 117 +++++++++++++++++++++++++++++++++----------
1 file changed, 91 insertions(+), 26 deletions(-)
diff --git a/drivers/input/rmi4/rmi_f12.c b/drivers/input/rmi4/rmi_f12.c
index 8246fe77114bb..1a103cc5f2235 100644
--- a/drivers/input/rmi4/rmi_f12.c
+++ b/drivers/input/rmi4/rmi_f12.c
@@ -218,6 +218,41 @@ static void rmi_f12_process_objects(struct f12_data *f12, u8 *data1, int size)
rmi_2d_sensor_abs_report(sensor, &sensor->objs[i], i);
}
+static void rmi_f12_set_hardcoded_desc(struct rmi_function *fn, struct f12_data *f12)
+{
+ struct rmi_2d_sensor *sensor = &f12->sensor;
+ struct rmi_register_desc_item *reg_desc;
+
+ /* We have no f12->data_reg_desc, so the pkt_size is 0, override it with
+ * a somewhat sensible default (this corresponds to 10 fingers).
+ */
+ sensor->pkt_size = 88;
+
+ /*
+ * There are no register descriptors to get these values from.
+ * We set them to high values to either be overwritten by the clip
+ * properties from devicetree, or to just not get in the way.
+ */
+ sensor->max_x = 65535;
+ sensor->max_y = 65535;
+
+ /*
+ * Create the Data1 register descriptor so that touch events
+ * can work properly.
+ */
+ reg_desc = devm_kcalloc(&fn->dev, 1,
+ sizeof(struct rmi_register_desc_item), GFP_KERNEL);
+ reg_desc->reg = 1;
+ reg_desc->reg_size = 80;
+ reg_desc->num_subpackets = 10;
+
+ f12->data1 = reg_desc;
+ f12->data1_offset = 0;
+ sensor->nbr_fingers = reg_desc->num_subpackets;
+ sensor->report_abs = 1;
+ sensor->attn_size += reg_desc->reg_size;
+}
+
static irqreturn_t rmi_f12_attention(int irq, void *ctx)
{
int retval;
@@ -338,6 +373,40 @@ static int rmi_f12_config(struct rmi_function *fn)
return 0;
}
+static int rmi_f12_sensor_init(struct rmi_function *fn, struct f12_data *f12)
+{
+ struct rmi_2d_sensor *sensor = &f12->sensor;
+
+ sensor->fn = fn;
+ f12->data_addr = fn->fd.data_base_addr;
+
+ /* On quirky devices that don't have a data_reg_desc we hardcode the packet
+ * in rmi_f12_set_hardcoded_desc(). Make sure not to set it to 0 here.
+ */
+ if (!sensor->pkt_size)
+ sensor->pkt_size = rmi_register_desc_calc_size(&f12->data_reg_desc);
+
+ sensor->axis_align =
+ f12->sensor_pdata.axis_align;
+
+ sensor->x_mm = f12->sensor_pdata.x_mm;
+ sensor->y_mm = f12->sensor_pdata.y_mm;
+ sensor->dribble = f12->sensor_pdata.dribble;
+
+ if (sensor->sensor_type == rmi_sensor_default)
+ sensor->sensor_type =
+ f12->sensor_pdata.sensor_type;
+
+ rmi_dbg(RMI_DEBUG_FN, &fn->dev, "%s: data packet size: %d\n", __func__,
+ sensor->pkt_size);
+
+ sensor->data_pkt = devm_kzalloc(&fn->dev, sensor->pkt_size, GFP_KERNEL);
+ if (!sensor->data_pkt)
+ return -ENOMEM;
+
+ return 0;
+}
+
static int rmi_f12_probe(struct rmi_function *fn)
{
struct f12_data *f12;
@@ -351,6 +420,7 @@ static int rmi_f12_probe(struct rmi_function *fn)
struct rmi_driver_data *drvdata = dev_get_drvdata(&rmi_dev->dev);
u16 data_offset = 0;
int mask_size;
+ bool hardcoded_desc_quirk = false;
rmi_dbg(RMI_DEBUG_FN, &fn->dev, "%s\n", __func__);
@@ -365,9 +435,9 @@ static int rmi_f12_probe(struct rmi_function *fn)
++query_addr;
if (!(buf & BIT(0))) {
- dev_err(&fn->dev,
- "Behavior of F12 without register descriptors is undefined.\n");
- return -ENODEV;
+ rmi_dbg(RMI_DEBUG_FN, &fn->dev,
+ "No register descriptors defined for F12, using fallback\n");
+ hardcoded_desc_quirk = true;
}
f12 = devm_kzalloc(&fn->dev, sizeof(struct f12_data) + mask_size * 2,
@@ -375,6 +445,8 @@ static int rmi_f12_probe(struct rmi_function *fn)
if (!f12)
return -ENOMEM;
+ dev_set_drvdata(&fn->dev, f12);
+
f12->abs_mask = (unsigned long *)((char *)f12
+ sizeof(struct f12_data));
f12->rel_mask = (unsigned long *)((char *)f12
@@ -393,6 +465,18 @@ static int rmi_f12_probe(struct rmi_function *fn)
f12->sensor_pdata = pdata->sensor_pdata;
}
+ sensor = &f12->sensor;
+
+ if (hardcoded_desc_quirk) {
+ rmi_f12_set_hardcoded_desc(fn, f12);
+
+ ret = rmi_f12_sensor_init(fn, f12);
+ if (ret)
+ return ret;
+
+ goto skip_register_desc;
+ }
+
ret = rmi_read_register_desc(rmi_dev, query_addr,
&f12->query_reg_desc);
if (ret) {
@@ -423,29 +507,9 @@ static int rmi_f12_probe(struct rmi_function *fn)
}
query_addr += 3;
- sensor = &f12->sensor;
- sensor->fn = fn;
- f12->data_addr = fn->fd.data_base_addr;
- sensor->pkt_size = rmi_register_desc_calc_size(&f12->data_reg_desc);
-
- sensor->axis_align =
- f12->sensor_pdata.axis_align;
-
- sensor->x_mm = f12->sensor_pdata.x_mm;
- sensor->y_mm = f12->sensor_pdata.y_mm;
- sensor->dribble = f12->sensor_pdata.dribble;
-
- if (sensor->sensor_type == rmi_sensor_default)
- sensor->sensor_type =
- f12->sensor_pdata.sensor_type;
-
- rmi_dbg(RMI_DEBUG_FN, &fn->dev, "%s: data packet size: %d\n", __func__,
- sensor->pkt_size);
- sensor->data_pkt = devm_kzalloc(&fn->dev, sensor->pkt_size, GFP_KERNEL);
- if (!sensor->data_pkt)
- return -ENOMEM;
-
- dev_set_drvdata(&fn->dev, f12);
+ ret = rmi_f12_sensor_init(fn, f12);
+ if (ret)
+ return ret;
ret = rmi_f12_read_sensor_tuning(f12);
if (ret)
@@ -543,6 +607,7 @@ static int rmi_f12_probe(struct rmi_function *fn)
data_offset += item->reg_size;
}
+skip_register_desc:
/* allocate the in-kernel tracking buffers */
sensor->tracking_pos = devm_kcalloc(&fn->dev,
sensor->nbr_fingers, sizeof(struct input_mt_pos),
--
2.53.0
^ permalink raw reply related
* [PATCH v7 4/7] Input: synaptics-rmi4 - f55: handle zero electrode count
From: David Heidelberg via B4 Relay @ 2026-03-20 16:44 UTC (permalink / raw)
To: Kaustabh Chakraborty, Dmitry Torokhov, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Jason A. Donenfeld,
Matthias Schiffer, Vincent Huang
Cc: David Heidelberg, linux-input, devicetree, linux-kernel,
Casey Connolly, phone-devel
In-Reply-To: <20260320-synaptics-rmi4-v7-0-379360de18d0@ixit.cz>
From: Kaustabh Chakraborty <kauschluss@disroot.org>
Some third party ICs claim to support f55 but report an electrode count
of 0. Catch this and bail out early so that we don't confuse the i2c bus
with 0 sized reads.
Signed-off-by: Kaustabh Chakraborty <kauschluss@disroot.org>
[simplify code, adjust wording]
Signed-off-by: Casey Connolly <casey.connolly@linaro.org>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/input/rmi4/rmi_f55.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/drivers/input/rmi4/rmi_f55.c b/drivers/input/rmi4/rmi_f55.c
index 488adaca4dd00..776c915b82e72 100644
--- a/drivers/input/rmi4/rmi_f55.c
+++ b/drivers/input/rmi4/rmi_f55.c
@@ -52,6 +52,11 @@ static int rmi_f55_detect(struct rmi_function *fn)
f55->num_rx_electrodes = f55->qry[F55_NUM_RX_OFFSET];
f55->num_tx_electrodes = f55->qry[F55_NUM_TX_OFFSET];
+ if (!f55->num_rx_electrodes || !f55->num_tx_electrodes) {
+ dev_err(&fn->dev, "%s: F55 query returned no electrodes, giving up\n",
+ __func__);
+ return -EINVAL;
+ }
f55->cfg_num_rx_electrodes = f55->num_rx_electrodes;
f55->cfg_num_tx_electrodes = f55->num_rx_electrodes;
--
2.53.0
^ permalink raw reply related
* [PATCH v7 5/7] Input: synaptics-rmi4 - don't do unaligned reads in IRQ context
From: David Heidelberg via B4 Relay @ 2026-03-20 16:44 UTC (permalink / raw)
To: Kaustabh Chakraborty, Dmitry Torokhov, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Jason A. Donenfeld,
Matthias Schiffer, Vincent Huang
Cc: David Heidelberg, linux-input, devicetree, linux-kernel,
Casey Connolly, phone-devel
In-Reply-To: <20260320-synaptics-rmi4-v7-0-379360de18d0@ixit.cz>
From: Kaustabh Chakraborty <kauschluss@disroot.org>
Some replacement displays include third-party touch ICs which incur a
significant penalty (1-2 seconds) when doing certain unaligned reads.
This is enough to break functionality when it happens in the hot path,
so adjust the interrupt handler to not read from an unaligned address.
Signed-off-by: Kaustabh Chakraborty <kauschluss@disroot.org>
Signed-off-by: Casey Connolly <casey.connolly@linaro.org>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/input/rmi4/rmi_driver.c | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/drivers/input/rmi4/rmi_driver.c b/drivers/input/rmi4/rmi_driver.c
index c7d2f68e65487..93a190e333c66 100644
--- a/drivers/input/rmi4/rmi_driver.c
+++ b/drivers/input/rmi4/rmi_driver.c
@@ -137,9 +137,14 @@ static int rmi_process_interrupt_requests(struct rmi_device *rmi_dev)
return 0;
if (!data->attn_data.data) {
+ /*
+ * Read the device status register as well and ignore it.
+ * Some aftermarket ICs have issues with interrupt requests
+ * otherwise.
+ */
error = rmi_read_block(rmi_dev,
- data->f01_container->fd.data_base_addr + 1,
- data->irq_status, data->num_of_irq_regs);
+ data->f01_container->fd.data_base_addr,
+ (u8 *)data->irq_status - 1, data->num_of_irq_regs + 1);
if (error < 0) {
dev_err(dev, "Failed to read irqs, code=%d\n", error);
return error;
@@ -1079,16 +1084,17 @@ int rmi_probe_interrupts(struct rmi_driver_data *data)
data->num_of_irq_regs = (data->irq_count + 7) / 8;
size = BITS_TO_LONGS(data->irq_count) * sizeof(unsigned long);
- data->irq_memory = devm_kcalloc(dev, size, 4, GFP_KERNEL);
+ data->irq_memory = devm_kzalloc(dev, size * 4 + 1, GFP_KERNEL);
if (!data->irq_memory) {
dev_err(dev, "Failed to allocate memory for irq masks.\n");
return -ENOMEM;
}
- data->irq_status = data->irq_memory + size * 0;
- data->fn_irq_bits = data->irq_memory + size * 1;
- data->current_irq_mask = data->irq_memory + size * 2;
- data->new_irq_mask = data->irq_memory + size * 3;
+ /* The first byte is reserved for the device status register */
+ data->irq_status = data->irq_memory + size * 0 + 1;
+ data->fn_irq_bits = data->irq_memory + size * 1 + 1;
+ data->current_irq_mask = data->irq_memory + size * 2 + 1;
+ data->new_irq_mask = data->irq_memory + size * 3 + 1;
return retval;
}
--
2.53.0
^ permalink raw reply related
* [PATCH v7 6/7] Input: synaptics-rmi4 - read product ID on aftermarket touch ICs
From: David Heidelberg via B4 Relay @ 2026-03-20 16:44 UTC (permalink / raw)
To: Kaustabh Chakraborty, Dmitry Torokhov, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Jason A. Donenfeld,
Matthias Schiffer, Vincent Huang
Cc: David Heidelberg, linux-input, devicetree, linux-kernel,
Casey Connolly, phone-devel
In-Reply-To: <20260320-synaptics-rmi4-v7-0-379360de18d0@ixit.cz>
From: Kaustabh Chakraborty <kauschluss@disroot.org>
Some replacement displays include third-party touch ICs which do not
report the product ID correctly unless we read directly from the
product ID register. Add a check and a fallback read to handle this.
Signed-off-by: Kaustabh Chakraborty <kauschluss@disroot.org>
Signed-off-by: Casey Connolly <casey.connolly@linaro.org>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/input/rmi4/rmi_f01.c | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/drivers/input/rmi4/rmi_f01.c b/drivers/input/rmi4/rmi_f01.c
index 47be64284b25e..2278e9b6a9207 100644
--- a/drivers/input/rmi4/rmi_f01.c
+++ b/drivers/input/rmi4/rmi_f01.c
@@ -250,6 +250,20 @@ static int rmi_f01_read_properties(struct rmi_device *rmi_dev,
}
}
+ /*
+ * Some aftermarket ICs put garbage into the product id field unless
+ * we read directly from the product id register.
+ */
+ if (props->product_id[0] < 0x20) {
+ ret = rmi_read_block(rmi_dev, query_base_addr + 11,
+ props->product_id, RMI_PRODUCT_ID_LENGTH);
+ if (ret) {
+ dev_err(&rmi_dev->dev,
+ "Failed to read product id: %d\n", ret);
+ return ret;
+ }
+ }
+
return 0;
}
--
2.53.0
^ permalink raw reply related
* [PATCH v7 7/7] Input: synaptics-rmi4 - support fallback values for PDT descriptor bytes
From: David Heidelberg via B4 Relay @ 2026-03-20 16:44 UTC (permalink / raw)
To: Kaustabh Chakraborty, Dmitry Torokhov, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Jason A. Donenfeld,
Matthias Schiffer, Vincent Huang
Cc: David Heidelberg, linux-input, devicetree, linux-kernel,
Casey Connolly, phone-devel
In-Reply-To: <20260320-synaptics-rmi4-v7-0-379360de18d0@ixit.cz>
From: Kaustabh Chakraborty <kauschluss@disroot.org>
Some replacement displays include third-party touch ICs which do not
expose the function number and the interrupt status in its PDT entries.
OnePlus 6 (original touch IC)
rmi4_i2c 12-0020: read 6 bytes at 0x00e3: 0 (2b 22 0d 06 01 01)
OnePlus 6 (aftermarket touch IC)
rmi4_i2c 12-0020: read 6 bytes at 0x00e3: 0 (2c 23 0d 06 00 00)
Signed-off-by: Kaustabh Chakraborty <kauschluss@disroot.org>
[codeflow adjustments, checkpatch fixes, wording]
Signed-off-by: Casey Connolly <casey.connolly@linaro.org>
Co-developed-by: David Heidelberg <david@ixit.cz>
Signed-off-by: David Heidelberg <david@ixit.cz>
---
drivers/input/rmi4/rmi_driver.c | 62 +++++++++++++++++++++++++++++++++++------
drivers/input/rmi4/rmi_driver.h | 2 ++
include/linux/rmi.h | 3 ++
3 files changed, 59 insertions(+), 8 deletions(-)
diff --git a/drivers/input/rmi4/rmi_driver.c b/drivers/input/rmi4/rmi_driver.c
index 93a190e333c66..bb1db5bbb3abb 100644
--- a/drivers/input/rmi4/rmi_driver.c
+++ b/drivers/input/rmi4/rmi_driver.c
@@ -462,9 +462,10 @@ static int rmi_driver_reset_handler(struct rmi_device *rmi_dev)
return 0;
}
-static int rmi_read_pdt_entry(struct rmi_device *rmi_dev,
- struct pdt_entry *entry, u16 pdt_address)
+static int rmi_read_pdt_entry(struct rmi_device *rmi_dev, struct pdt_entry *entry,
+ struct pdt_scan_state *state, u16 pdt_address)
{
+ const struct rmi_device_platform_data *pdata = rmi_get_platform_data(rmi_dev);
u8 buf[RMI_PDT_ENTRY_SIZE];
int error;
@@ -475,6 +476,21 @@ static int rmi_read_pdt_entry(struct rmi_device *rmi_dev,
return error;
}
+ if (pdata->pdt_fallback_size > state->pdt_count * RMI_OF_PDT_DESC_CELLS + 1) {
+ /* Use the description bytes from the driver */
+ buf[5] = pdata->pdt_fallback_desc[state->pdt_count * RMI_OF_PDT_DESC_CELLS];
+ buf[4] = pdata->pdt_fallback_desc[state->pdt_count * RMI_OF_PDT_DESC_CELLS + 1];
+
+ error = rmi_read_block(rmi_dev, pdt_address, buf,
+ RMI_PDT_ENTRY_SIZE - 2);
+ if (error) {
+ dev_err(&rmi_dev->dev,
+ "Read PDT entry at %#06x failed, code: %d.\n",
+ pdt_address, error);
+ return error;
+ }
+ }
+
entry->page_start = pdt_address & RMI4_PAGE_MASK;
entry->query_base_addr = buf[0];
entry->command_base_addr = buf[1];
@@ -547,7 +563,7 @@ static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
int retval;
for (addr = pdt_start; addr >= pdt_end; addr -= RMI_PDT_ENTRY_SIZE) {
- error = rmi_read_pdt_entry(rmi_dev, &pdt_entry, addr);
+ error = rmi_read_pdt_entry(rmi_dev, &pdt_entry, state, addr);
if (error)
return error;
@@ -1024,9 +1040,13 @@ static int rmi_driver_remove(struct device *dev)
}
#ifdef CONFIG_OF
-static int rmi_driver_of_probe(struct device *dev,
- struct rmi_device_platform_data *pdata)
+static const u8 rmi_s3706_fallback_pdt[] = {34, 41, 01, 01, 12, 01};
+
+static int rmi_driver_of_probe(struct rmi_device *rmi_dev,
+ struct rmi_device_platform_data *pdata)
{
+ struct device *dev = rmi_dev->xport->dev;
+ u8 buf[RMI_PDT_ENTRY_SIZE];
int retval;
retval = rmi_of_property_read_u32(dev, &pdata->reset_delay_ms,
@@ -1034,11 +1054,37 @@ static int rmi_driver_of_probe(struct device *dev,
if (retval)
return retval;
+ /*
+ * In some aftermerket touch ICs, the first PDT entry is empty and
+ * the function number register is 0. If so, the driver
+ * may have provide backup PDT entries.
+ */
+
+ retval = rmi_read_block(rmi_dev, PDT_START_SCAN_LOCATION,
+ buf, RMI_PDT_ENTRY_SIZE);
+ if (retval) {
+ dev_err(dev, "Read PDT entry at %#06x failed, code: %d.\n",
+ PDT_START_SCAN_LOCATION, retval);
+ return retval;
+ }
+
+ if (!RMI4_END_OF_PDT(buf[5]))
+ return 0;
+
+ /* List of known PDT entries per compatible. */
+ if (of_device_is_compatible(dev->of_node, "syna,rmi4-s3706b")) {
+ pdata->pdt_fallback_desc = rmi_s3706_fallback_pdt;
+ pdata->pdt_fallback_size = ARRAY_SIZE(rmi_s3706_fallback_pdt);
+ } else {
+ dev_err(dev, "First PDT entry is empty and no backup values provided.\n");
+ return -EINVAL;
+ }
+
return 0;
}
#else
-static inline int rmi_driver_of_probe(struct device *dev,
- struct rmi_device_platform_data *pdata)
+static inline int rmi_driver_of_probe(struct rmi_device *rmi_dev,
+ struct rmi_device_platform_data *pdata)
{
return -ENODEV;
}
@@ -1159,7 +1205,7 @@ static int rmi_driver_probe(struct device *dev)
pdata = rmi_get_platform_data(rmi_dev);
if (rmi_dev->xport->dev->of_node) {
- retval = rmi_driver_of_probe(rmi_dev->xport->dev, pdata);
+ retval = rmi_driver_of_probe(rmi_dev, pdata);
if (retval)
return retval;
}
diff --git a/drivers/input/rmi4/rmi_driver.h b/drivers/input/rmi4/rmi_driver.h
index a4ae2af93ce3a..b931f428713bf 100644
--- a/drivers/input/rmi4/rmi_driver.h
+++ b/drivers/input/rmi4/rmi_driver.h
@@ -31,6 +31,8 @@
#define RMI_PDT_FUNCTION_VERSION_MASK 0x60
#define RMI_PDT_INT_SOURCE_COUNT_MASK 0x07
+#define RMI_OF_PDT_DESC_CELLS 2
+
#define PDT_START_SCAN_LOCATION 0x00e9
#define PDT_END_SCAN_LOCATION 0x0005
#define RMI4_END_OF_PDT(id) ((id) == 0x00 || (id) == 0xff)
diff --git a/include/linux/rmi.h b/include/linux/rmi.h
index ab7eea01ab427..4ba2cefac8558 100644
--- a/include/linux/rmi.h
+++ b/include/linux/rmi.h
@@ -214,6 +214,9 @@ struct rmi_device_platform_data {
int reset_delay_ms;
int irq;
+ unsigned int pdt_fallback_size;
+ const u8 *pdt_fallback_desc;
+
struct rmi_device_platform_data_spi spi_data;
/* function handler pdata */
--
2.53.0
^ permalink raw reply related
* Re: [PATCH v7 2/7] Input: synaptics-rmi4 - handle duplicate/unknown PDT entries
From: Casey Connolly @ 2026-03-20 16:49 UTC (permalink / raw)
To: david, Kaustabh Chakraborty, Dmitry Torokhov, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Jason A. Donenfeld,
Matthias Schiffer, Vincent Huang
Cc: linux-input, devicetree, linux-kernel, phone-devel
In-Reply-To: <20260320-synaptics-rmi4-v7-2-379360de18d0@ixit.cz>
Hi David,
Nice timing with the series, I hit an OOB access (found it when I
enabled UBSAN) with this patch the other day.
The pdt_scan_state->pdts array should actually be of size (RMI_PDT_MAX+1).
Additionally, I think rmi_pdt_entry_is_valid() is missing a bounds check.
Kind regards,
On 20/03/2026 17:44, David Heidelberg via B4 Relay wrote:
> From: Casey Connolly <casey.connolly@linaro.org>
>
> Some third party rmi4-compatible ICs don't expose their PDT entries
> very well. Add a few checks to skip duplicate entries as well as entries
> for unsupported functions.
>
> This is required to support some phones with third party displays.
>
> Validated on a stock OnePlus 6T (original parts):
> manufacturer: Synaptics, product: S3706B, fw id: 2852315
>
> Co-developed-by: Kaustabh Chakraborty <kauschluss@disroot.org>
> Signed-off-by: Kaustabh Chakraborty <kauschluss@disroot.org>
> Signed-off-by: Casey Connolly <casey.connolly@linaro.org>
> Co-developed-by: David Heidelberg <david@ixit.cz>
> Signed-off-by: David Heidelberg <david@ixit.cz>
> ---
> drivers/input/rmi4/rmi_driver.c | 42 +++++++++++++++++++++++++++++++++++------
> drivers/input/rmi4/rmi_driver.h | 8 ++++++++
> 2 files changed, 44 insertions(+), 6 deletions(-)
>
> diff --git a/drivers/input/rmi4/rmi_driver.c b/drivers/input/rmi4/rmi_driver.c
> index ccd9338a44dbe..c7d2f68e65487 100644
> --- a/drivers/input/rmi4/rmi_driver.c
> +++ b/drivers/input/rmi4/rmi_driver.c
> @@ -494,12 +494,39 @@ static void rmi_driver_copy_pdt_to_fd(const struct pdt_entry *pdt,
> fd->function_version = pdt->function_version;
> }
>
> +static bool rmi_pdt_entry_is_valid(struct rmi_device *rmi_dev,
> + struct pdt_scan_state *state, u8 fn)
> +{
> + switch (fn) {
> + case 0x01:
> + case 0x03:
> + case 0x11:
> + case 0x12:
> + case 0x30:
> + case 0x34:
> + case 0x3a:
> + case 0x54:
> + case 0x55:
> + if (state->pdts[fn] == true)
> + return false;
> + break;
> + default:
> + rmi_dbg(RMI_DEBUG_CORE, &rmi_dev->dev,
> + "PDT has unknown function number %#02x\n", fn);
> + return false;
> + }
> +
> + state->pdts[fn] = true;
> + state->pdt_count++;
> + return true;
> +}
> +
> #define RMI_SCAN_CONTINUE 0
> #define RMI_SCAN_DONE 1
>
> static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
> int page,
> - int *empty_pages,
> + struct pdt_scan_state *state,
> void *ctx,
> int (*callback)(struct rmi_device *rmi_dev,
> void *ctx,
> @@ -522,6 +549,9 @@ static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
> if (RMI4_END_OF_PDT(pdt_entry.function_number))
> break;
>
> + if (!rmi_pdt_entry_is_valid(rmi_dev, state, pdt_entry.function_number))
> + continue;
> +
> retval = callback(rmi_dev, ctx, &pdt_entry);
> if (retval != RMI_SCAN_CONTINUE)
> return retval;
> @@ -532,11 +562,11 @@ static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
> * or more is found, stop scanning.
> */
> if (addr == pdt_start)
> - ++*empty_pages;
> + ++state->empty_pages;
> else
> - *empty_pages = 0;
> + state->empty_pages = 0;
>
> - return (data->bootloader_mode || *empty_pages >= 2) ?
> + return (data->bootloader_mode || state->empty_pages >= 2) ?
> RMI_SCAN_DONE : RMI_SCAN_CONTINUE;
> }
>
> @@ -545,11 +575,11 @@ int rmi_scan_pdt(struct rmi_device *rmi_dev, void *ctx,
> void *ctx, const struct pdt_entry *entry))
> {
> int page;
> - int empty_pages = 0;
> + struct pdt_scan_state state = {0, 0, {0}};
> int retval = RMI_SCAN_DONE;
>
> for (page = 0; page <= RMI4_MAX_PAGE; page++) {
> - retval = rmi_scan_pdt_page(rmi_dev, page, &empty_pages,
> + retval = rmi_scan_pdt_page(rmi_dev, page, &state,
> ctx, callback);
> if (retval != RMI_SCAN_CONTINUE)
> break;
> diff --git a/drivers/input/rmi4/rmi_driver.h b/drivers/input/rmi4/rmi_driver.h
> index e84495caab151..a4ae2af93ce3a 100644
> --- a/drivers/input/rmi4/rmi_driver.h
> +++ b/drivers/input/rmi4/rmi_driver.h
> @@ -46,6 +46,14 @@ struct pdt_entry {
> u8 function_number;
> };
>
> +#define RMI_PDT_MAX 0x55
> +
> +struct pdt_scan_state {
> + u8 empty_pages;
> + u8 pdt_count;
> + bool pdts[RMI_PDT_MAX];
> +};
> +
> #define RMI_REG_DESC_PRESENSE_BITS (32 * BITS_PER_BYTE)
> #define RMI_REG_DESC_SUBPACKET_BITS (37 * BITS_PER_BYTE)
>
>
--
// Casey (she/her)
^ permalink raw reply
* Re: [PATCH v7 2/7] Input: synaptics-rmi4 - handle duplicate/unknown PDT entries
From: David Heidelberg @ 2026-03-20 16:54 UTC (permalink / raw)
To: Casey Connolly, Kaustabh Chakraborty, Dmitry Torokhov,
Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jason A. Donenfeld, Matthias Schiffer, Vincent Huang
Cc: linux-input, devicetree, linux-kernel, phone-devel
In-Reply-To: <fba73d66-4300-4c4d-9bf6-4b38a4e847d7@postmarketos.org>
On 20/03/2026 17:49, Casey Connolly wrote:
> Hi David,
>
> Nice timing with the series, I hit an OOB access (found it when I
> enabled UBSAN) with this patch the other day.
>
> The pdt_scan_state->pdts array should actually be of size (RMI_PDT_MAX+1).
>
> Additionally, I think rmi_pdt_entry_is_valid() is missing a bounds check.
>
> Kind regards,
Thanks a lot for catching this and for the detailed notes — that’s very helpful.
Since you’re the original author of the commit, I’m completely fine with you
taking over the b4 series if you’d prefer. Alternatively, if it’s easier, feel
free to just send me a fixed patch and I can incorporate it.
Whichever works best for you.
David>
> On 20/03/2026 17:44, David Heidelberg via B4 Relay wrote:
>> From: Casey Connolly <casey.connolly@linaro.org>
>>
>> Some third party rmi4-compatible ICs don't expose their PDT entries
>> very well. Add a few checks to skip duplicate entries as well as entries
>> for unsupported functions.
>>
>> This is required to support some phones with third party displays.
>>
>> Validated on a stock OnePlus 6T (original parts):
>> manufacturer: Synaptics, product: S3706B, fw id: 2852315
>>
>> Co-developed-by: Kaustabh Chakraborty <kauschluss@disroot.org>
>> Signed-off-by: Kaustabh Chakraborty <kauschluss@disroot.org>
>> Signed-off-by: Casey Connolly <casey.connolly@linaro.org>
>> Co-developed-by: David Heidelberg <david@ixit.cz>
>> Signed-off-by: David Heidelberg <david@ixit.cz>
[...]
^ permalink raw reply
* Re: [PATCH v7 2/7] Input: synaptics-rmi4 - handle duplicate/unknown PDT entries
From: Casey Connolly @ 2026-03-20 17:03 UTC (permalink / raw)
To: David Heidelberg, Kaustabh Chakraborty, Dmitry Torokhov,
Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jason A. Donenfeld, Matthias Schiffer, Vincent Huang
Cc: linux-input, devicetree, linux-kernel, phone-devel
In-Reply-To: <3dca0fc0-fe1c-4f84-b336-856f55a6e3da@ixit.cz>
On 20/03/2026 17:54, David Heidelberg wrote:
> On 20/03/2026 17:49, Casey Connolly wrote:
>> Hi David,
>>
>> Nice timing with the series, I hit an OOB access (found it when I
>> enabled UBSAN) with this patch the other day.
>>
>> The pdt_scan_state->pdts array should actually be of size
>> (RMI_PDT_MAX+1).
>>
>> Additionally, I think rmi_pdt_entry_is_valid() is missing a bounds check.
>>
>> Kind regards,
>
>
> Thanks a lot for catching this and for the detailed notes — that’s very
> helpful.
>
> Since you’re the original author of the commit, I’m completely fine with
> you taking over the b4 series if you’d prefer. Alternatively, if it’s
> easier, feel free to just send me a fixed patch and I can incorporate it.
>
Uh sure, not sure this will apply cleanly I just edited inline it's a
3-line delta. Also figured we can drop pdt_count since it's unused.
---
diff --git a/drivers/input/rmi4/rmi_driver.c
b/drivers/input/rmi4/rmi_driver.c
index ccd9338a44dbe..c7d2f68e65487 100644
--- a/drivers/input/rmi4/rmi_driver.c
+++ b/drivers/input/rmi4/rmi_driver.c
@@ -494,12 +494,39 @@ static void rmi_driver_copy_pdt_to_fd(const struct
pdt_entry *pdt,
fd->function_version = pdt->function_version;
}
+static bool rmi_pdt_entry_is_valid(struct rmi_device *rmi_dev,
+ struct pdt_scan_state *state, u8 fn)
+{
+ if (fn > RMI_PDT_MAX)
+ return false;
+
+ switch (fn) {
+ case 0x01:
+ case 0x03:
+ case 0x11:
+ case 0x12:
+ case 0x30:
+ case 0x34:
+ case 0x3a:
+ case 0x54:
+ case 0x55:
+ if (state->pdts[fn] == true)
+ return false;
+ break;
+ default:
+ rmi_dbg(RMI_DEBUG_CORE, &rmi_dev->dev,
+ "PDT has unknown function number %#02x\n", fn);
+ return false;
+ }
+
+ state->pdts[fn] = true;
+ return true;
+}
+
#define RMI_SCAN_CONTINUE 0
#define RMI_SCAN_DONE 1
static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
int page,
- int *empty_pages,
+ struct pdt_scan_state *state,
void *ctx,
int (*callback)(struct rmi_device *rmi_dev,
void *ctx,
@@ -522,6 +549,9 @@ static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
if (RMI4_END_OF_PDT(pdt_entry.function_number))
break;
+ if (!rmi_pdt_entry_is_valid(rmi_dev, state, pdt_entry.function_number))
+ continue;
+
retval = callback(rmi_dev, ctx, &pdt_entry);
if (retval != RMI_SCAN_CONTINUE)
return retval;
@@ -532,11 +562,11 @@ static int rmi_scan_pdt_page(struct rmi_device
*rmi_dev,
* or more is found, stop scanning.
*/
if (addr == pdt_start)
- ++*empty_pages;
+ ++state->empty_pages;
else
- *empty_pages = 0;
+ state->empty_pages = 0;
- return (data->bootloader_mode || *empty_pages >= 2) ?
+ return (data->bootloader_mode || state->empty_pages >= 2) ?
RMI_SCAN_DONE : RMI_SCAN_CONTINUE;
}
@@ -545,11 +575,11 @@ int rmi_scan_pdt(struct rmi_device *rmi_dev, void
*ctx,
void *ctx, const struct pdt_entry *entry))
{
int page;
- int empty_pages = 0;
+ struct pdt_scan_state state = {0, {0}};
int retval = RMI_SCAN_DONE;
for (page = 0; page <= RMI4_MAX_PAGE; page++) {
- retval = rmi_scan_pdt_page(rmi_dev, page, &empty_pages,
+ retval = rmi_scan_pdt_page(rmi_dev, page, &state,
ctx, callback);
if (retval != RMI_SCAN_CONTINUE)
break;
diff --git a/drivers/input/rmi4/rmi_driver.h
b/drivers/input/rmi4/rmi_driver.h
index e84495caab151..a4ae2af93ce3a 100644
--- a/drivers/input/rmi4/rmi_driver.h
+++ b/drivers/input/rmi4/rmi_driver.h
@@ -46,6 +46,14 @@ struct pdt_entry {
u8 function_number;
};
+#define RMI_PDT_MAX 0x55
+
+struct pdt_scan_state {
+ u8 empty_pages;
+ bool pdts[RMI_PDT_MAX + 1];
+};
+
#define RMI_REG_DESC_PRESENSE_BITS (32 * BITS_PER_BYTE)
#define RMI_REG_DESC_SUBPACKET_BITS (37 * BITS_PER_BYTE)
--
2.53.0
--
// Casey (she/her)
^ permalink raw reply related
* Re: [PATCH v7 2/7] Input: synaptics-rmi4 - handle duplicate/unknown PDT entries
From: David Heidelberg @ 2026-03-20 17:12 UTC (permalink / raw)
To: Casey Connolly, Kaustabh Chakraborty, Dmitry Torokhov,
Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jason A. Donenfeld, Matthias Schiffer, Vincent Huang
Cc: linux-input, devicetree, linux-kernel, phone-devel
In-Reply-To: <55dab1d8-87af-4285-9ab1-924bf392c78d@postmarketos.org>
On 20/03/2026 18:03, Casey Connolly wrote:
>
>
> On 20/03/2026 17:54, David Heidelberg wrote:
>> On 20/03/2026 17:49, Casey Connolly wrote:
>>> Hi David,
>>>
>>> Nice timing with the series, I hit an OOB access (found it when I
>>> enabled UBSAN) with this patch the other day.
>>>
>>> The pdt_scan_state->pdts array should actually be of size
>>> (RMI_PDT_MAX+1).
>>>
>>> Additionally, I think rmi_pdt_entry_is_valid() is missing a bounds check.
>>>
>>> Kind regards,
>>
>>
>> Thanks a lot for catching this and for the detailed notes — that’s very
>> helpful.
>>
>> Since you’re the original author of the commit, I’m completely fine with
>> you taking over the b4 series if you’d prefer. Alternatively, if it’s
>> easier, feel free to just send me a fixed patch and I can incorporate it.
>>
>
> Uh sure, not sure this will apply cleanly I just edited inline it's a
> 3-line delta. Also figured we can drop pdt_count since it's unused.
The pdt_count is used in
Input: synaptics-rmi4 - support fallback values for PDT descriptor bytes
thus should be moved there I assume, but can be dropped here.
David
>
> ---
>
> diff --git a/drivers/input/rmi4/rmi_driver.c
> b/drivers/input/rmi4/rmi_driver.c
> index ccd9338a44dbe..c7d2f68e65487 100644
> --- a/drivers/input/rmi4/rmi_driver.c
> +++ b/drivers/input/rmi4/rmi_driver.c
> @@ -494,12 +494,39 @@ static void rmi_driver_copy_pdt_to_fd(const struct
> pdt_entry *pdt,
> fd->function_version = pdt->function_version;
> }
>
> +static bool rmi_pdt_entry_is_valid(struct rmi_device *rmi_dev,
> + struct pdt_scan_state *state, u8 fn)
> +{
> + if (fn > RMI_PDT_MAX)
> + return false;
> +
> + switch (fn) {
> + case 0x01:
> + case 0x03:
> + case 0x11:
> + case 0x12:
> + case 0x30:
> + case 0x34:
> + case 0x3a:
> + case 0x54:
> + case 0x55:
> + if (state->pdts[fn] == true)
> + return false;
> + break;
> + default:
> + rmi_dbg(RMI_DEBUG_CORE, &rmi_dev->dev,
> + "PDT has unknown function number %#02x\n", fn);
> + return false;
> + }
> +
> + state->pdts[fn] = true;
> + return true;
> +}
> +
> #define RMI_SCAN_CONTINUE 0
> #define RMI_SCAN_DONE 1
>
> static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
> int page,
> - int *empty_pages,
> + struct pdt_scan_state *state,
> void *ctx,
> int (*callback)(struct rmi_device *rmi_dev,
> void *ctx,
> @@ -522,6 +549,9 @@ static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
> if (RMI4_END_OF_PDT(pdt_entry.function_number))
> break;
>
> + if (!rmi_pdt_entry_is_valid(rmi_dev, state, pdt_entry.function_number))
> + continue;
> +
> retval = callback(rmi_dev, ctx, &pdt_entry);
> if (retval != RMI_SCAN_CONTINUE)
> return retval;
> @@ -532,11 +562,11 @@ static int rmi_scan_pdt_page(struct rmi_device
> *rmi_dev,
> * or more is found, stop scanning.
> */
> if (addr == pdt_start)
> - ++*empty_pages;
> + ++state->empty_pages;
> else
> - *empty_pages = 0;
> + state->empty_pages = 0;
>
> - return (data->bootloader_mode || *empty_pages >= 2) ?
> + return (data->bootloader_mode || state->empty_pages >= 2) ?
> RMI_SCAN_DONE : RMI_SCAN_CONTINUE;
> }
>
> @@ -545,11 +575,11 @@ int rmi_scan_pdt(struct rmi_device *rmi_dev, void
> *ctx,
> void *ctx, const struct pdt_entry *entry))
> {
> int page;
> - int empty_pages = 0;
> + struct pdt_scan_state state = {0, {0}};
> int retval = RMI_SCAN_DONE;
>
> for (page = 0; page <= RMI4_MAX_PAGE; page++) {
> - retval = rmi_scan_pdt_page(rmi_dev, page, &empty_pages,
> + retval = rmi_scan_pdt_page(rmi_dev, page, &state,
> ctx, callback);
> if (retval != RMI_SCAN_CONTINUE)
> break;
> diff --git a/drivers/input/rmi4/rmi_driver.h
> b/drivers/input/rmi4/rmi_driver.h
> index e84495caab151..a4ae2af93ce3a 100644
> --- a/drivers/input/rmi4/rmi_driver.h
> +++ b/drivers/input/rmi4/rmi_driver.h
> @@ -46,6 +46,14 @@ struct pdt_entry {
> u8 function_number;
> };
>
> +#define RMI_PDT_MAX 0x55
> +
> +struct pdt_scan_state {
> + u8 empty_pages;
> + bool pdts[RMI_PDT_MAX + 1];
> +};
> +
> #define RMI_REG_DESC_PRESENSE_BITS (32 * BITS_PER_BYTE)
> #define RMI_REG_DESC_SUBPACKET_BITS (37 * BITS_PER_BYTE)
>
>
--
David Heidelberg
^ permalink raw reply
* Re: [PATCH v7 2/7] Input: synaptics-rmi4 - handle duplicate/unknown PDT entries
From: David Heidelberg @ 2026-03-20 17:19 UTC (permalink / raw)
To: Casey Connolly, Kaustabh Chakraborty, Dmitry Torokhov,
Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jason A. Donenfeld, Matthias Schiffer, Vincent Huang
Cc: linux-input, devicetree, linux-kernel, phone-devel
In-Reply-To: <f31d10ce-a3dc-4599-8201-8a78aa2fc65f@ixit.cz>
Pushed changes to
https://codeberg.org/sdm845/linux/commits/branch/b4/synaptics-rmi4
On 20/03/2026 18:12, David Heidelberg wrote:
> On 20/03/2026 18:03, Casey Connolly wrote:
>>
>>
>> On 20/03/2026 17:54, David Heidelberg wrote:
>>> On 20/03/2026 17:49, Casey Connolly wrote:
>>>> Hi David,
>>>>
>>>> Nice timing with the series, I hit an OOB access (found it when I
>>>> enabled UBSAN) with this patch the other day.
>>>>
>>>> The pdt_scan_state->pdts array should actually be of size
>>>> (RMI_PDT_MAX+1).
>>>>
>>>> Additionally, I think rmi_pdt_entry_is_valid() is missing a bounds check.
>>>>
>>>> Kind regards,
>>>
>>>
>>> Thanks a lot for catching this and for the detailed notes — that’s very
>>> helpful.
>>>
>>> Since you’re the original author of the commit, I’m completely fine with
>>> you taking over the b4 series if you’d prefer. Alternatively, if it’s
>>> easier, feel free to just send me a fixed patch and I can incorporate it.
>>>
>>
>> Uh sure, not sure this will apply cleanly I just edited inline it's a
>> 3-line delta. Also figured we can drop pdt_count since it's unused.
>
> The pdt_count is used in
>
> Input: synaptics-rmi4 - support fallback values for PDT descriptor bytes
>
> thus should be moved there I assume, but can be dropped here.
>
> David
>
>>
>> ---
>>
>> diff --git a/drivers/input/rmi4/rmi_driver.c
>> b/drivers/input/rmi4/rmi_driver.c
>> index ccd9338a44dbe..c7d2f68e65487 100644
>> --- a/drivers/input/rmi4/rmi_driver.c
>> +++ b/drivers/input/rmi4/rmi_driver.c
>> @@ -494,12 +494,39 @@ static void rmi_driver_copy_pdt_to_fd(const struct
>> pdt_entry *pdt,
>> fd->function_version = pdt->function_version;
>> }
>>
>> +static bool rmi_pdt_entry_is_valid(struct rmi_device *rmi_dev,
>> + struct pdt_scan_state *state, u8 fn)
>> +{
>> + if (fn > RMI_PDT_MAX)
>> + return false;
>> +
>> + switch (fn) {
>> + case 0x01:
>> + case 0x03:
>> + case 0x11:
>> + case 0x12:
>> + case 0x30:
>> + case 0x34:
>> + case 0x3a:
>> + case 0x54:
>> + case 0x55:
>> + if (state->pdts[fn] == true)
>> + return false;
>> + break;
>> + default:
>> + rmi_dbg(RMI_DEBUG_CORE, &rmi_dev->dev,
>> + "PDT has unknown function number %#02x\n", fn);
>> + return false;
>> + }
>> +
>> + state->pdts[fn] = true;
>> + return true;
>> +}
>> +
>> #define RMI_SCAN_CONTINUE 0
>> #define RMI_SCAN_DONE 1
>>
>> static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
>> int page,
>> - int *empty_pages,
>> + struct pdt_scan_state *state,
>> void *ctx,
>> int (*callback)(struct rmi_device *rmi_dev,
>> void *ctx,
>> @@ -522,6 +549,9 @@ static int rmi_scan_pdt_page(struct rmi_device *rmi_dev,
>> if (RMI4_END_OF_PDT(pdt_entry.function_number))
>> break;
>>
>> + if (!rmi_pdt_entry_is_valid(rmi_dev, state, pdt_entry.function_number))
>> + continue;
>> +
>> retval = callback(rmi_dev, ctx, &pdt_entry);
>> if (retval != RMI_SCAN_CONTINUE)
>> return retval;
>> @@ -532,11 +562,11 @@ static int rmi_scan_pdt_page(struct rmi_device
>> *rmi_dev,
>> * or more is found, stop scanning.
>> */
>> if (addr == pdt_start)
>> - ++*empty_pages;
>> + ++state->empty_pages;
>> else
>> - *empty_pages = 0;
>> + state->empty_pages = 0;
>>
>> - return (data->bootloader_mode || *empty_pages >= 2) ?
>> + return (data->bootloader_mode || state->empty_pages >= 2) ?
>> RMI_SCAN_DONE : RMI_SCAN_CONTINUE;
>> }
>>
>> @@ -545,11 +575,11 @@ int rmi_scan_pdt(struct rmi_device *rmi_dev, void
>> *ctx,
>> void *ctx, const struct pdt_entry *entry))
>> {
>> int page;
>> - int empty_pages = 0;
>> + struct pdt_scan_state state = {0, {0}};
>> int retval = RMI_SCAN_DONE;
>>
>> for (page = 0; page <= RMI4_MAX_PAGE; page++) {
>> - retval = rmi_scan_pdt_page(rmi_dev, page, &empty_pages,
>> + retval = rmi_scan_pdt_page(rmi_dev, page, &state,
>> ctx, callback);
>> if (retval != RMI_SCAN_CONTINUE)
>> break;
>> diff --git a/drivers/input/rmi4/rmi_driver.h
>> b/drivers/input/rmi4/rmi_driver.h
>> index e84495caab151..a4ae2af93ce3a 100644
>> --- a/drivers/input/rmi4/rmi_driver.h
>> +++ b/drivers/input/rmi4/rmi_driver.h
>> @@ -46,6 +46,14 @@ struct pdt_entry {
>> u8 function_number;
>> };
>>
>> +#define RMI_PDT_MAX 0x55
>> +
>> +struct pdt_scan_state {
>> + u8 empty_pages;
>> + bool pdts[RMI_PDT_MAX + 1];
>> +};
>> +
>> #define RMI_REG_DESC_PRESENSE_BITS (32 * BITS_PER_BYTE)
>> #define RMI_REG_DESC_SUBPACKET_BITS (37 * BITS_PER_BYTE)
>>
>>
>
--
David Heidelberg
^ permalink raw reply
* Re: [PATCH] Input: gpio-keys - add full support of EV_REL and EV_ABS
From: Dmitry Torokhov @ 2026-03-21 7:06 UTC (permalink / raw)
To: Xiong Nandi
Cc: linux-input, linux-kernel, Gatien Chevallier, Ingo Molnar,
Thomas Gleixner, Marco Crivellari, Fabrice Gasnier
In-Reply-To: <20260320145217.9088-1-xndchn@gmail.com>
Hi Xiong,
On Fri, Mar 20, 2026 at 10:52:14PM +0800, Xiong Nandi wrote:
> gpio_keys_gpio_report_event() handled EV_ABS but silently ignored EV_REL,
> while the polled driver supports both. Extend the interrupt-driven driver
> to fire an EV_REL event on button press.
>
> For EV_ABS, use a shared atomic counter per (type, code) pair so that
> a zero-value reset is sent only when the last active button on an axis
> is released, avoiding premature axis resets when multiple buttons share
> the same axis code.
>
> Add gpio_keys_set_abs_params() to call input_set_abs_params() at setup
> time, deriving the axis min/max from the configured button values.
> Without this the input subsystem reports unbounded axis ranges.
Could you please split this out? It looks like there are 2 or 3
logically separate changes.
Thanks.
--
Dmitry
^ permalink raw reply
* [PATCH] Input: goodix-berlin: report a resolution of 10 units/mm
From: Val Packett @ 2026-03-21 7:30 UTC (permalink / raw)
To: Hans de Goede, Dmitry Torokhov, Henrik Rydberg
Cc: Val Packett, Stanislav Zaikin, linux-input, phone-devel,
~postmarketos/upstreaming, linux-kernel
Without a reported resolution, userspace was assuming 1 unit/mm which
is wildly wrong: a regular smartphone is clearly not 2.4 meters tall.
Most applications do not care much for this kind of raw mm value,
but Phosh's on-screen keyboard would accidentally trigger swipe-to-close
gestures due to misinterpreting small movements as huge ones.
Do what the older goodix.c driver does and set the resolution to 10
units/mm to make sure the numbers calculated by userspace are reasonable.
Signed-off-by: Val Packett <val@packett.cool>
---
drivers/input/touchscreen/goodix_berlin_core.c | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/drivers/input/touchscreen/goodix_berlin_core.c b/drivers/input/touchscreen/goodix_berlin_core.c
index 83f28b870531..b0938a4f3fec 100644
--- a/drivers/input/touchscreen/goodix_berlin_core.c
+++ b/drivers/input/touchscreen/goodix_berlin_core.c
@@ -628,6 +628,14 @@ static int goodix_berlin_input_dev_config(struct goodix_berlin_core *cd,
touchscreen_parse_properties(cd->input_dev, true, &cd->props);
+ /*
+ * The resolution of these touchscreens is about 10 units/mm, the actual
+ * resolution does not matter much since we set INPUT_PROP_DIRECT.
+ * Set it to 10 to ensure userspace isn't off by an order of magnitude.
+ */
+ input_abs_set_res(cd->input_dev, ABS_MT_POSITION_X, 10);
+ input_abs_set_res(cd->input_dev, ABS_MT_POSITION_Y, 10);
+
error = input_mt_init_slots(cd->input_dev, GOODIX_BERLIN_MAX_TOUCH,
INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
if (error)
--
2.53.0
^ permalink raw reply related
* [PATCH v2] HID: rakk: add support for Rakk Dasig X side buttons
From: Karl Cayme @ 2026-03-21 12:42 UTC (permalink / raw)
To: jikos, bentiss; +Cc: linux-input, linux-kernel, linuxhid, Karl Cayme
In-Reply-To: <3284f00a-bea0-4141-a009-b367f527c466@cosmicgizmosystems.com>
The Rakk Dasig X gaming mouse has a faulty HID report descriptor that
declares USAGE_MAXIMUM=3 (buttons 1-3) while actually sending 5 button
bits (REPORT_COUNT=5). This causes the kernel to ignore side buttons
(buttons 4 and 5).
Fix by patching the descriptor to set USAGE_MAXIMUM=5 in the
report_fixup callback.
The mouse uses Telink vendor ID 0x248a with three product IDs for USB
direct (0xfb01), wireless dongle (0xfa02), and Bluetooth (0x8266)
connection modes. All three variants have the same bug at byte offset 17.
Suggested-by: Terry Junge <linuxhid@cosmicgizmosystems.com>
Signed-off-by: Karl Cayme <kcayme@gmail.com>
---
Hi,
Thanks for the feedback. I updated the patch with your suggestion to
check PIDs.
Best,
Karl
v2 Changes:
- included PID checking alongside descriptor size in report_fixup
drivers/hid/Kconfig | 9 +++++
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 5 +++
drivers/hid/hid-rakk.c | 75 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 90 insertions(+)
create mode 100644 drivers/hid/hid-rakk.c
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index c1d9f7c6a5f2..11c48cb1c6a6 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -755,6 +755,15 @@ config HID_MEGAWORLD_FF
Say Y here if you have a Mega World based game controller and want
to have force feedback support for it.
+config HID_RAKK
+ tristate "Rakk support"
+ help
+ Support for Rakk gaming peripherals.
+
+ Fixes the HID report descriptor of the Rakk Dasig X mouse,
+ which declares USAGE_MAXIMUM=3 (buttons 1-3) while actually
+ sending 5 button bits. This causes side buttons to be ignored.
+
config HID_REDRAGON
tristate "Redragon keyboards"
default !EXPERT
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index e01838239ae6..7800613f5b2b 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -113,6 +113,7 @@ obj-$(CONFIG_HID_PLANTRONICS) += hid-plantronics.o
obj-$(CONFIG_HID_PLAYSTATION) += hid-playstation.o
obj-$(CONFIG_HID_PRIMAX) += hid-primax.o
obj-$(CONFIG_HID_PXRC) += hid-pxrc.o
+obj-$(CONFIG_HID_RAKK) += hid-rakk.o
obj-$(CONFIG_HID_RAPOO) += hid-rapoo.o
obj-$(CONFIG_HID_RAZER) += hid-razer.o
obj-$(CONFIG_HID_REDRAGON) += hid-redragon.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 3e299a30dcde..68fab837e8b3 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1369,6 +1369,11 @@
#define USB_DEVICE_ID_SYNAPTICS_ACER_SWITCH5_017 0x73f6
#define USB_DEVICE_ID_SYNAPTICS_ACER_SWITCH5 0x81a7
+#define USB_VENDOR_ID_TELINK 0x248a
+#define USB_DEVICE_ID_TELINK_RAKK_DASIG_X 0xfb01
+#define USB_DEVICE_ID_TELINK_RAKK_DASIG_X_DONGLE 0xfa02
+#define USB_DEVICE_ID_TELINK_RAKK_DASIG_X_BT 0x8266
+
#define USB_VENDOR_ID_TEXAS_INSTRUMENTS 0x2047
#define USB_DEVICE_ID_TEXAS_INSTRUMENTS_LENOVO_YOGA 0x0855
diff --git a/drivers/hid/hid-rakk.c b/drivers/hid/hid-rakk.c
new file mode 100644
index 000000000000..c59ea47b8996
--- /dev/null
+++ b/drivers/hid/hid-rakk.c
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * HID driver for Rakk devices
+ *
+ * Copyright (c) 2026 Karl Cayme
+ *
+ * The Rakk Dasig X gaming mouse has a faulty HID report descriptor that
+ * declares USAGE_MAXIMUM = 3 (buttons 1-3) while actually sending 5 button
+ * bits (REPORT_COUNT = 5). This causes the kernel to ignore side buttons
+ * (buttons 4 and 5). This driver fixes the descriptor so all 5 buttons
+ * are properly recognized across 3 modes (wired, dongle, and Bluetooth).
+ */
+
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include "hid-ids.h"
+
+/*
+ * The faulty byte is at offset 17 in the report descriptor for all three
+ * connection modes (USB direct, wireless dongle, and Bluetooth).
+ *
+ * Bytes 16-17 are: 0x29 0x03 (USAGE_MAXIMUM = 3)
+ * The fix changes byte 17 to 0x05 (USAGE_MAXIMUM = 5).
+ *
+ * Original descriptor bytes 0-17:
+ * 05 01 09 02 a1 01 85 01 09 01 a1 00 05 09 19 01 29 03
+ * ^^
+ * Should be 0x05 to declare 5 buttons instead of 3.
+ */
+#define RAKK_RDESC_USAGE_MAX_OFFSET 17
+#define RAKK_RDESC_USAGE_MAX_ORIG 0x03
+#define RAKK_RDESC_USAGE_MAX_FIXED 0x05
+#define RAKK_RDESC_USB_SIZE 193
+#define RAKK_RDESC_DONGLE_SIZE 150
+#define RAKK_RDESC_BT_SIZE 89
+
+static const __u8 *rakk_report_fixup(struct hid_device *hdev, __u8 *rdesc,
+ unsigned int *rsize)
+{
+ if (((*rsize == RAKK_RDESC_USB_SIZE &&
+ hdev->product == USB_DEVICE_ID_TELINK_RAKK_DASIG_X) ||
+ (*rsize == RAKK_RDESC_DONGLE_SIZE &&
+ hdev->product == USB_DEVICE_ID_TELINK_RAKK_DASIG_X_DONGLE) ||
+ (*rsize == RAKK_RDESC_BT_SIZE &&
+ hdev->product == USB_DEVICE_ID_TELINK_RAKK_DASIG_X_BT)) &&
+ rdesc[RAKK_RDESC_USAGE_MAX_OFFSET] == RAKK_RDESC_USAGE_MAX_ORIG) {
+ hid_info(hdev, "fixing Rakk Dasig X button count (3 -> 5)\n");
+ rdesc[RAKK_RDESC_USAGE_MAX_OFFSET] = RAKK_RDESC_USAGE_MAX_FIXED;
+ }
+
+ return rdesc;
+}
+
+static const struct hid_device_id rakk_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_TELINK,
+ USB_DEVICE_ID_TELINK_RAKK_DASIG_X) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_TELINK,
+ USB_DEVICE_ID_TELINK_RAKK_DASIG_X_DONGLE) },
+ { HID_BLUETOOTH_DEVICE(USB_VENDOR_ID_TELINK,
+ USB_DEVICE_ID_TELINK_RAKK_DASIG_X_BT) },
+ { }
+};
+MODULE_DEVICE_TABLE(hid, rakk_devices);
+
+static struct hid_driver rakk_driver = {
+ .name = "rakk",
+ .id_table = rakk_devices,
+ .report_fixup = rakk_report_fixup,
+};
+module_hid_driver(rakk_driver);
+
+MODULE_DESCRIPTION("HID driver for Rakk Dasig X mouse - fix side button support");
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Karl Cayme");
--
2.53.0
^ permalink raw reply related
* [PATCH] HID: drop 'default !EXPERT' from tristate symbols
From: Thomas Weißschuh @ 2026-03-21 13:15 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: linux-input, linux-kernel, Thomas Weißschuh
There is no reason to build random drivers for obscure hardware into the
core kernel by default.
The usages of 'default !EXPERT' for the HID_PICOLCD suboptions are kept,
as these make some sense, although they probably should use 'default y'.
Signed-off-by: Thomas Weißschuh <linux@weissschuh.net>
---
drivers/hid/Kconfig | 13 -------------
1 file changed, 13 deletions(-)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 10c12d8e6557..7b1d8d81bd65 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -107,7 +107,6 @@ menu "Special HID drivers"
config HID_A4TECH
tristate "A4TECH mice"
- default !EXPERT
help
Support for some A4TECH mice with two scroll wheels.
@@ -140,7 +139,6 @@ config HID_APPLE
tristate "Apple {i,Power,Mac}Books"
depends on LEDS_CLASS
depends on NEW_LEDS
- default !EXPERT
help
Support for some Apple devices which less or more break
HID specification.
@@ -209,7 +207,6 @@ config HID_AUREAL
config HID_BELKIN
tristate "Belkin Flip KVM and Wireless keyboard"
- default !EXPERT
help
Support for Belkin Flip KVM and Wireless keyboard.
@@ -237,14 +234,12 @@ config HID_BIGBEN_FF
config HID_CHERRY
tristate "Cherry Cymotion keyboard"
- default !EXPERT
help
Support for Cherry Cymotion keyboard.
config HID_CHICONY
tristate "Chicony devices"
depends on USB_HID
- default !EXPERT
help
Support for Chicony Tactical pad and special keys on Chicony keyboards.
@@ -322,7 +317,6 @@ config HID_CREATIVE_SB0540
config HID_CYPRESS
tristate "Cypress mouse and barcode readers"
- default !EXPERT
help
Support for cypress mouse and barcode readers.
@@ -388,7 +382,6 @@ config HID_EVISION
config HID_EZKEY
tristate "Ezkey BTC 8193 keyboard"
- default !EXPERT
help
Support for Ezkey BTC 8193 keyboard.
@@ -564,7 +557,6 @@ config HID_ICADE
config HID_ITE
tristate "ITE devices"
- default !EXPERT
help
Support for ITE devices not fully compliant with HID standard.
@@ -585,7 +577,6 @@ config HID_TWINHAN
config HID_KENSINGTON
tristate "Kensington Slimblade Trackball"
- default !EXPERT
help
Support for Kensington Slimblade Trackball.
@@ -666,7 +657,6 @@ config HID_LOGITECH
depends on USB_HID
depends on LEDS_CLASS
depends on LEDS_CLASS_MULTICOLOR
- default !EXPERT
help
Support for Logitech devices that are not fully compliant with HID standard.
@@ -781,20 +771,17 @@ config HID_MEGAWORLD_FF
config HID_REDRAGON
tristate "Redragon keyboards"
- default !EXPERT
help
Support for Redragon keyboards that need fix-ups to work properly.
config HID_MICROSOFT
tristate "Microsoft non-fully HID-compliant devices"
- default !EXPERT
select INPUT_FF_MEMLESS
help
Support for Microsoft devices that are not fully compliant with HID standard.
config HID_MONTEREY
tristate "Monterey Genius KB29E keyboard"
- default !EXPERT
help
Support for Monterey Genius KB29E.
---
base-commit: 2bbe306cc6f970474cb2ad7d5332220e81119bbd
change-id: 20260321-hid-expert-87fcf609230a
Best regards,
--
Thomas Weißschuh <linux@weissschuh.net>
^ permalink raw reply related
* [PATCH 0/3] Input: adafruit-seesaw: use dev_err_probe and add IRQ support
From: charles.embedded @ 2026-03-21 20:24 UTC (permalink / raw)
To: Anshul Dalal, Dmitry Torokhov
Cc: Shuah Khan, Brigham Campbell, linux-input, linux-kernel,
Charles Dias
From: Charles Dias <charlesdias.cd@outlook.com>
This series improves the Adafruit seesaw gamepad driver in two steps.
The first patch switches to using dev_err_probe() in seesaw_probe()
to improve error handling.
The second patch adds optional interrupt support for button events when
an IRQ is described in DTS. Joystick axes remain polled because the
default Adafruit seesaw gamepad firmware exposes button interrupts
through the GPIO module, while the joystick positions are read from ADC
channels.
When no IRQ is described in DTS, the driver continues to operate in
pure polling mode.
This series was validated on a BeaglePlay board with the Adafruit
Seesaw Gamepad, both with the interrupt enabled in DTS and without it.
Charles Dias (3):
Input: adafruit-seesaw - switch to using dev_err_probe()
Input: adafruit-seesaw - add interrupt support
dt-bindings: input: adafruit-seesaw-gamepad: fix interrupt polarity
.../input/adafruit,seesaw-gamepad.yaml | 12 +-
drivers/input/joystick/adafruit-seesaw.c | 160 ++++++++++++------
2 files changed, 119 insertions(+), 53 deletions(-)
^ permalink raw reply
* [PATCH 1/3] Input: adafruit-seesaw - switch to using dev_err_probe()
From: charles.embedded @ 2026-03-21 20:24 UTC (permalink / raw)
To: Anshul Dalal, Dmitry Torokhov
Cc: Shuah Khan, Brigham Campbell, linux-input, linux-kernel,
Charles Dias
In-Reply-To: <20260321202446.724277-1-charles.embedded@gmail.com>
From: Charles Dias <charlesdias.cd@outlook.com>
Use dev_err_probe() instead of dev_err() in seesaw_probe function
to improve error handling.
Signed-off-by: Charles Dias <charlesdias.cd@outlook.com>
---
drivers/input/joystick/adafruit-seesaw.c | 19 ++++++-------------
1 file changed, 6 insertions(+), 13 deletions(-)
diff --git a/drivers/input/joystick/adafruit-seesaw.c b/drivers/input/joystick/adafruit-seesaw.c
index c248c15b849d..177b42446e9b 100644
--- a/drivers/input/joystick/adafruit-seesaw.c
+++ b/drivers/input/joystick/adafruit-seesaw.c
@@ -277,17 +277,12 @@ static int seesaw_probe(struct i2c_client *client)
SEESAW_JOYSTICK_FUZZ, SEESAW_JOYSTICK_FLAT);
err = sparse_keymap_setup(seesaw->input_dev, seesaw_buttons_new, NULL);
- if (err) {
- dev_err(&client->dev,
- "failed to set up input device keymap: %d\n", err);
- return err;
- }
+ if (err)
+ return dev_err_probe(&client->dev, err, "failed to set up input device keymap\n");
err = input_setup_polling(seesaw->input_dev, seesaw_poll);
- if (err) {
- dev_err(&client->dev, "failed to set up polling: %d\n", err);
- return err;
- }
+ if (err)
+ return dev_err_probe(&client->dev, err, "failed to set up polling\n");
input_set_poll_interval(seesaw->input_dev,
SEESAW_GAMEPAD_POLL_INTERVAL_MS);
@@ -295,10 +290,8 @@ static int seesaw_probe(struct i2c_client *client)
input_set_min_poll_interval(seesaw->input_dev, SEESAW_GAMEPAD_POLL_MIN);
err = input_register_device(seesaw->input_dev);
- if (err) {
- dev_err(&client->dev, "failed to register joystick: %d\n", err);
- return err;
- }
+ if (err)
+ return dev_err_probe(&client->dev, err, "failed to register joystick\n");
return 0;
}
^ permalink raw reply related
* [PATCH 2/3] Input: adafruit-seesaw - add interrupt support
From: charles.embedded @ 2026-03-21 20:24 UTC (permalink / raw)
To: Anshul Dalal, Dmitry Torokhov
Cc: Shuah Khan, Brigham Campbell, linux-input, linux-kernel,
Charles Dias
In-Reply-To: <20260321202446.724277-1-charles.embedded@gmail.com>
From: Charles Dias <charlesdias.cd@outlook.com>
Use IRQ-driven reporting for button events when an interrupt is
described in DTS. Joystick axis values still have no interrupt source,
so axis reporting continues to use polling.
When the DTS specifies an interrupt, the IRQ thread calls only
seesaw_report_buttons(), avoiding unnecessary ADC reads on each button
event. The polling path always calls seesaw_report_axes(), and calls
seesaw_report_buttons() only when no IRQ is configured. This avoids
concurrent access to button_state between the IRQ thread and the poll
timer.
When no interrupt is available, the driver falls back to fully polled
operation.
Also remove the now-unused seesaw_data structure. Axis values are read
directly in seesaw_report_axes(), and button_state already lives in the
driver data, where it persists across calls for state comparison.
Signed-off-by: Charles Dias <charlesdias.cd@outlook.com>
---
drivers/input/joystick/adafruit-seesaw.c | 141 +++++++++++++++++------
1 file changed, 103 insertions(+), 38 deletions(-)
diff --git a/drivers/input/joystick/adafruit-seesaw.c b/drivers/input/joystick/adafruit-seesaw.c
index 177b42446e9b..ed8896c639ce 100644
--- a/drivers/input/joystick/adafruit-seesaw.c
+++ b/drivers/input/joystick/adafruit-seesaw.c
@@ -11,8 +11,10 @@
* Product page: https://www.adafruit.com/product/5743
* Firmware and hardware sources: https://github.com/adafruit/Adafruit_Seesaw
*
- * TODO:
- * - Add interrupt support
+ * Interrupt support is available when the DTS defines an interrupt for the
+ * device. Button events are then driven by the seesaw INT line, while joystick
+ * axes are always polled since the seesaw ADC has no interrupt source.
+ * Without an interrupt, the driver falls back to fully polling mode.
*/
#include <linux/unaligned.h>
@@ -21,6 +23,7 @@
#include <linux/i2c.h>
#include <linux/input.h>
#include <linux/input/sparse-keymap.h>
+#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/module.h>
@@ -31,12 +34,14 @@
#define SEESAW_GPIO_DIRCLR_BULK 0x0103
#define SEESAW_GPIO_BULK 0x0104
#define SEESAW_GPIO_BULK_SET 0x0105
+#define SEESAW_GPIO_INTENSET 0x0108
#define SEESAW_GPIO_PULLENSET 0x010b
#define SEESAW_STATUS_HW_ID 0x0001
#define SEESAW_STATUS_SWRST 0x007f
#define SEESAW_ADC_OFFSET 0x07
+#define SEESAW_ADC_REG(ch) (SEESAW_ADC_BASE | (SEESAW_ADC_OFFSET + (ch)))
#define SEESAW_BUTTON_A 0x05
#define SEESAW_BUTTON_B 0x01
@@ -67,12 +72,6 @@ struct seesaw_gamepad {
u32 button_state;
};
-struct seesaw_data {
- u16 x;
- u16 y;
- u32 button_state;
-};
-
static const struct key_entry seesaw_buttons_new[] = {
{ KE_KEY, SEESAW_BUTTON_A, .keycode = BTN_SOUTH },
{ KE_KEY, SEESAW_BUTTON_B, .keycode = BTN_EAST },
@@ -142,39 +141,61 @@ static int seesaw_register_write_u32(struct i2c_client *client, u16 reg,
return 0;
}
-static int seesaw_read_data(struct i2c_client *client, struct seesaw_data *data)
+static int seesaw_report_buttons(struct seesaw_gamepad *private)
{
- __be16 adc_data;
+ struct input_dev *input = private->input_dev;
+ unsigned long changed;
+ u32 button_state;
__be32 read_buf;
- int err;
+ int err, i;
- err = seesaw_register_read(client, SEESAW_GPIO_BULK,
+ err = seesaw_register_read(private->i2c_client, SEESAW_GPIO_BULK,
&read_buf, sizeof(read_buf));
if (err)
return err;
- data->button_state = ~be32_to_cpu(read_buf);
+ button_state = ~be32_to_cpu(read_buf) & SEESAW_BUTTON_MASK;
+ changed = private->button_state ^ button_state;
+ private->button_state = button_state;
+
+ for_each_set_bit(i, &changed, fls(SEESAW_BUTTON_MASK)) {
+ if (!sparse_keymap_report_event(input, i,
+ button_state & BIT(i), false))
+ dev_err_ratelimited(&input->dev,
+ "failed to report keymap event");
+ }
+
+ input_sync(input);
+ return 0;
+}
+
+static int seesaw_report_axes(struct seesaw_gamepad *private)
+{
+ struct input_dev *input = private->input_dev;
+ __be16 adc_data;
+ int err;
- err = seesaw_register_read(client,
- SEESAW_ADC_BASE |
- (SEESAW_ADC_OFFSET + SEESAW_ANALOG_X),
+ err = seesaw_register_read(private->i2c_client,
+ SEESAW_ADC_REG(SEESAW_ANALOG_X),
&adc_data, sizeof(adc_data));
if (err)
return err;
+
/*
* ADC reads left as max and right as 0, must be reversed since kernel
* expects reports in opposite order.
*/
- data->x = SEESAW_JOYSTICK_MAX_AXIS - be16_to_cpu(adc_data);
+ input_report_abs(input, ABS_X,
+ SEESAW_JOYSTICK_MAX_AXIS - be16_to_cpu(adc_data));
- err = seesaw_register_read(client,
- SEESAW_ADC_BASE |
- (SEESAW_ADC_OFFSET + SEESAW_ANALOG_Y),
+ err = seesaw_register_read(private->i2c_client,
+ SEESAW_ADC_REG(SEESAW_ANALOG_Y),
&adc_data, sizeof(adc_data));
if (err)
return err;
- data->y = be16_to_cpu(adc_data);
+ input_report_abs(input, ABS_Y, be16_to_cpu(adc_data));
+ input_sync(input);
return 0;
}
@@ -182,42 +203,72 @@ static int seesaw_read_data(struct i2c_client *client, struct seesaw_data *data)
static int seesaw_open(struct input_dev *input)
{
struct seesaw_gamepad *private = input_get_drvdata(input);
+ int err;
private->button_state = 0;
+ if (private->i2c_client->irq) {
+ /*
+ * Read and report current button state before enabling the
+ * edge-triggered IRQ. This deasserts any pending INT already
+ * latched by the chip since probe(), preventing the IRQ line
+ * from being stuck low on the first open.
+ */
+ err = seesaw_report_buttons(private);
+ if (err)
+ return err;
+
+ enable_irq(private->i2c_client->irq);
+ }
+
return 0;
}
+static void seesaw_close(struct input_dev *input)
+{
+ struct seesaw_gamepad *private = input_get_drvdata(input);
+
+ if (private->i2c_client->irq)
+ disable_irq(private->i2c_client->irq);
+}
+
static void seesaw_poll(struct input_dev *input)
{
struct seesaw_gamepad *private = input_get_drvdata(input);
- struct seesaw_data data;
- unsigned long changed;
- int err, i;
+ int err;
- err = seesaw_read_data(private->i2c_client, &data);
+ err = seesaw_report_axes(private);
if (err) {
dev_err_ratelimited(&input->dev,
- "failed to read joystick state: %d\n", err);
+ "failed to read joystick axes: %d\n", err);
return;
}
- input_report_abs(input, ABS_X, data.x);
- input_report_abs(input, ABS_Y, data.y);
+ /*
+ * In interrupt mode, buttons are reported exclusively by
+ * seesaw_irq_thread() to avoid concurrent access to button_state.
+ */
+ if (!private->i2c_client->irq) {
+ err = seesaw_report_buttons(private);
+ if (err)
+ dev_err_ratelimited(&input->dev,
+ "failed to read button state: %d\n", err);
+ }
+}
- data.button_state &= SEESAW_BUTTON_MASK;
- changed = private->button_state ^ data.button_state;
- private->button_state = data.button_state;
+static irqreturn_t seesaw_irq_thread(int irq, void *dev_id)
+{
+ struct seesaw_gamepad *private = dev_id;
+ int err;
- for_each_set_bit(i, &changed, fls(SEESAW_BUTTON_MASK)) {
- if (!sparse_keymap_report_event(input, i,
- data.button_state & BIT(i),
- false))
- dev_err_ratelimited(&input->dev,
- "failed to report keymap event");
+ err = seesaw_report_buttons(private);
+ if (err) {
+ dev_err_ratelimited(&private->input_dev->dev,
+ "failed to read button state: %d\n", err);
+ return IRQ_NONE;
}
- input_sync(input);
+ return IRQ_HANDLED;
}
static int seesaw_probe(struct i2c_client *client)
@@ -268,6 +319,7 @@ static int seesaw_probe(struct i2c_client *client)
seesaw->input_dev->name = "Adafruit Seesaw Gamepad";
seesaw->input_dev->phys = "i2c/" SEESAW_DEVICE_NAME;
seesaw->input_dev->open = seesaw_open;
+ seesaw->input_dev->close = seesaw_close;
input_set_drvdata(seesaw->input_dev, seesaw);
input_set_abs_params(seesaw->input_dev, ABS_X,
0, SEESAW_JOYSTICK_MAX_AXIS,
@@ -289,6 +341,19 @@ static int seesaw_probe(struct i2c_client *client)
input_set_max_poll_interval(seesaw->input_dev, SEESAW_GAMEPAD_POLL_MAX);
input_set_min_poll_interval(seesaw->input_dev, SEESAW_GAMEPAD_POLL_MIN);
+ if (client->irq) {
+ err = seesaw_register_write_u32(client, SEESAW_GPIO_INTENSET, SEESAW_BUTTON_MASK);
+ if (err)
+ return dev_err_probe(&client->dev, err,
+ "failed to enable hardware interrupts\n");
+
+ err = devm_request_threaded_irq(&client->dev, client->irq, NULL,
+ seesaw_irq_thread, IRQF_ONESHOT | IRQF_NO_AUTOEN,
+ SEESAW_DEVICE_NAME, seesaw);
+ if (err)
+ return dev_err_probe(&client->dev, err, "failed to request IRQ\n");
+ }
+
err = input_register_device(seesaw->input_dev);
if (err)
return dev_err_probe(&client->dev, err, "failed to register joystick\n");
^ permalink raw reply related
* [PATCH 3/3] dt-bindings: input: adafruit-seesaw-gamepad: fix interrupt polarity
From: charles.embedded @ 2026-03-21 20:24 UTC (permalink / raw)
To: Anshul Dalal, Dmitry Torokhov
Cc: Shuah Khan, Brigham Campbell, linux-input, linux-kernel,
Charles Dias
In-Reply-To: <20260321202446.724277-1-charles.embedded@gmail.com>
From: Charles Dias <charlesdias.cd@outlook.com>
The INT line is open-drain and asserts low on button GPIO changes, so
the binding should describe a falling-edge trigger rather than rising
edge. Also update the example to use IRQ_TYPE_EDGE_FALLING and add
interrupt-parent, and clarify that the driver can fall back to polling
when no IRQ is wired.
Signed-off-by: Charles Dias <charlesdias.cd@outlook.com>
---
.../bindings/input/adafruit,seesaw-gamepad.yaml | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/Documentation/devicetree/bindings/input/adafruit,seesaw-gamepad.yaml b/Documentation/devicetree/bindings/input/adafruit,seesaw-gamepad.yaml
index 5e86f6de6978..f0ebb326bf74 100644
--- a/Documentation/devicetree/bindings/input/adafruit,seesaw-gamepad.yaml
+++ b/Documentation/devicetree/bindings/input/adafruit,seesaw-gamepad.yaml
@@ -25,6 +25,11 @@ description: |
SE -> Select
X, A, B, Y -> Digital action buttons
+ The gamepad exposes button events through the seesaw GPIO block and joystick
+ axes through the seesaw ADC block. If the optional IRQ pin is wired, button
+ presses can be interrupt-driven while joystick axes remain polled. Without an
+ IRQ, the driver falls back to fully polled operation.
+
Datasheet: https://cdn-learn.adafruit.com/downloads/pdf/gamepad-qt.pdf
Product page: https://www.adafruit.com/product/5743
Arduino Driver: https://github.com/adafruit/Adafruit_Seesaw
@@ -39,7 +44,9 @@ properties:
interrupts:
maxItems: 1
description:
- The gamepad's IRQ pin triggers a rising edge if interrupts are enabled.
+ Optional interrupt from the gamepad's open-drain INT pin. The device
+ asserts INT low on button GPIO changes when interrupts are enabled in the
+ seesaw firmware, so the host should typically use a falling-edge trigger.
required:
- compatible
@@ -57,7 +64,8 @@ examples:
joystick@50 {
compatible = "adafruit,seesaw-gamepad";
- interrupts = <18 IRQ_TYPE_EDGE_RISING>;
+ interrupt-parent = <&gpio1>;
+ interrupts = <18 IRQ_TYPE_EDGE_FALLING>;
reg = <0x50>;
};
};
^ permalink raw reply related
* [PATCH] Docs: hid: intel-ish-hid: make long URL usable
From: Randy Dunlap @ 2026-03-21 23:09 UTC (permalink / raw)
To: linux-kernel
Cc: Randy Dunlap, Jiri Kosina, Benjamin Tissoires,
Srinivas Pandruvada, linux-input, Jonathan Corbet, Shuah Khan,
linux-doc
The '\' line continuation character in this long URL
doesn't help anything. There is no documentation tooling that
handles the line continuation character to join the 2 lines
to make a usable URL. Web browsers terminate the URL just
before the '\' character so that the second line of the URL
is lost. See:
https://docs.kernel.org/hid/intel-ish-hid.html
Join the 2 lines together so that the URL is usable.
Signed-off-by: Randy Dunlap <rdunlap@infradead.org>
---
Cc: Jiri Kosina <jikos@kernel.org>
Cc: Benjamin Tissoires <bentiss@kernel.org>
Cc: Srinivas Pandruvada <srinivas.pandruvada@linux.intel.com>
Cc: linux-input@vger.kernel.org
Cc: Jonathan Corbet <corbet@lwn.net>
Cc: Shuah Khan <skhan@linuxfoundation.org>
Cc: linux-doc@vger.kernel.org
Documentation/hid/intel-ish-hid.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
--- linux-next-20260320.orig/Documentation/hid/intel-ish-hid.rst
+++ linux-next-20260320/Documentation/hid/intel-ish-hid.rst
@@ -163,8 +163,8 @@ The transport layer is a bi-directional
- A flow control mechanism to avoid buffer overflows
This protocol resembles bus messages described in the following document:
-http://www.intel.com/content/dam/www/public/us/en/documents/technical-\
-specifications/dcmi-hi-1-0-spec.pdf "Chapter 7: Bus Message Layer"
+http://www.intel.com/content/dam/www/public/us/en/documents/technical-specifications/dcmi-hi-1-0-spec.pdf
+"Chapter 7: Bus Message Layer".
Connection and Flow Control Mechanism
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
^ permalink raw reply
* [PATCH 0/4] Add OneXPlayer Configuration HID Driver
From: Derek J. Clark @ 2026-03-22 3:16 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
linux-input, linux-doc, linux-kernel
Adds an HID driver for OneXPlayer HID configuration devices. There are
currently 2 generations of OneXPlayer HID protocol. The first generation
(OneXPlayer F1 series) only provides an RGB control interface over HID.
The Second generation (X1 mini series, G1 series, AOKZOE A1X) also
includes a hardware level button mapping interface, as well as a
"takeover" mode that was added by the ODM for debugging the button map.
This takeover mode can be useful for exposing the M1 and M2 accessory
buttons as unique inputs with some userspace tools that can consume it.
Signed-off-by: Derel J. Clark <derekjohn.clark@gmail.com>
Derek J. Clark (4):
HID: hid-oxp: Add OneXPlayer configuration driver
HID: hid-oxp: Add Second Generation RGB Control
HID: hid-oxp: Add Second Generation Takeover Mode
HID: hid-oxp: Add Button Mapping Interface
MAINTAINERS | 6 +
drivers/hid/Kconfig | 12 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 6 +
drivers/hid/hid-oxp.c | 1340 +++++++++++++++++++++++++++++++++++++++++
5 files changed, 1365 insertions(+)
create mode 100644 drivers/hid/hid-oxp.c
--
2.53.0
^ permalink raw reply
* [PATCH 2/4] HID: hid-oxp: Add Second Generation RGB Control
From: Derek J. Clark @ 2026-03-22 3:16 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260322031615.1524307-1-derekjohn.clark@gmail.com>
Adds support for the second generation of RGB Control for OneXPlayer
devices. The interface mirrors the first generation, with some
differences to how messages are formatted.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-ids.h | 3 ++
drivers/hid/hid-oxp.c | 96 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 99 insertions(+)
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 8b272d1ab9ba..b33782ba6556 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1105,6 +1105,9 @@
#define USB_VENDOR_ID_CRSC 0x1a2c
#define USB_DEVICE_ID_ONEXPLAYER_GEN1 0xb001
+#define USB_VENDOR_ID_WCH 0x1a86
+#define USB_DEVICE_ID_ONEXPLAYER_GEN2 0xfe00
+
#define USB_VENDOR_ID_ONTRAK 0x0a07
#define USB_DEVICE_ID_ONTRAK_ADU100 0x0064
diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index 391de2798320..587e0d57c85f 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -24,12 +24,15 @@
#define OXP_PACKET_SIZE 64
#define GEN1_MESSAGE_ID 0xff
+#define GEN2_MESSAGE_ID 0x3f
#define GEN1_USAGE_PAGE 0xff01
+#define GEN2_USAGE_PAGE 0xff00
enum oxp_function_index {
OXP_FID_GEN1_RGB_SET = 0x07,
OXP_FID_GEN1_RGB_REPLY = 0x0f,
+ OXP_FID_GEN2_RGB_EVENT = 0xb8,
};
static struct oxp_hid_cfg {
@@ -121,6 +124,22 @@ struct oxp_gen_1_rgb_report {
u8 blue;
} __packed;
+struct oxp_gen_2_rgb_report {
+ u8 report_id;
+ u8 header_id;
+ u8 padding_2;
+ u8 message_id;
+ u8 padding_4[2];
+ u8 enabled;
+ u8 speed;
+ u8 brightness;
+ u8 red;
+ u8 green;
+ u8 blue;
+ u8 padding_12[3];
+ u8 effect;
+} __packed;
+
static u16 get_usage_page(struct hid_device *hdev)
{
return hdev->collection[0].usage >> 16;
@@ -162,6 +181,45 @@ static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
return 0;
}
+static int oxp_hid_raw_event_gen_2(struct hid_device *hdev,
+ struct hid_report *report, u8 *data,
+ int size)
+{
+ struct led_classdev_mc *led_mc = drvdata.led_mc;
+ struct oxp_gen_2_rgb_report *rgb_rep;
+
+ if (data[0] != OXP_FID_GEN2_RGB_EVENT)
+ return 0;
+
+ if (data[3] != OXP_GET_PROPERTY)
+ return 0;
+
+ rgb_rep = (struct oxp_gen_2_rgb_report *)data;
+ /* Ensure we save monocolor as the list value */
+ drvdata.rgb_effect =
+ rgb_rep->effect == OXP_EFFECT_MONO_TRUE ?
+ OXP_EFFECT_MONO_LIST :
+ rgb_rep->effect;
+ drvdata.rgb_speed = rgb_rep->speed;
+ drvdata.rgb_en = rgb_rep->enabled == 0 ? OXP_FEAT_DISABLED :
+ OXP_FEAT_ENABLED;
+ drvdata.rgb_brightness = rgb_rep->brightness;
+ led_mc->led_cdev.brightness = rgb_rep->brightness / 4 *
+ led_mc->led_cdev.max_brightness;
+ /* If monocolor had less than 100% brightness on the previous boot,
+ * there will be no reliable way to determine the real intensity.
+ * Since intensity scaling is used with a hardware brightness set at max,
+ * our brightness will always look like 100%. Use the last set value to
+ * prevent successive boots from lowering the brightness further.
+ * Brightness will be "wrong" but the effect will remain the same visually.
+ */
+ led_mc->subled_info[0].intensity = rgb_rep->red;
+ led_mc->subled_info[1].intensity = rgb_rep->green;
+ led_mc->subled_info[2].intensity = rgb_rep->blue;
+
+ return 0;
+}
+
static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report,
u8 *data, int size)
{
@@ -172,6 +230,8 @@ static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report,
switch (up) {
case GEN1_USAGE_PAGE:
return oxp_hid_raw_event_gen_1(hdev, report, data, size);
+ case GEN2_USAGE_PAGE:
+ return oxp_hid_raw_event_gen_2(hdev, report, data, size);
default:
break;
}
@@ -217,6 +277,18 @@ static int oxp_gen_1_property_out(enum oxp_function_index fid, u8 *data,
return mcu_property_out(header, header_size, data, data_size, NULL, 0);
}
+static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data,
+ u8 data_size)
+{
+ u8 header[] = { fid, GEN2_MESSAGE_ID, 0x01 };
+ u8 footer[] = { GEN2_MESSAGE_ID, fid };
+ size_t header_size = ARRAY_SIZE(header);
+ size_t footer_size = ARRAY_SIZE(footer);
+
+ return mcu_property_out(header, header_size, data, data_size, footer,
+ footer_size);
+}
+
static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
{
u16 up = get_usage_page(drvdata.hdev);
@@ -231,6 +303,11 @@ static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST)
data[3] = 0x04;
return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 4);
+ case GEN2_USAGE_PAGE:
+ data = (u8[6]) { OXP_SET_PROPERTY, 0x00, 0x02, enabled, speed, brightness };
+ if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST)
+ data[5] = 0x04;
+ return oxp_gen_2_property_out(OXP_FID_GEN2_RGB_EVENT, data, 6);
default:
return -ENODEV;
}
@@ -245,6 +322,9 @@ static ssize_t oxp_rgb_status_show(void)
case GEN1_USAGE_PAGE:
data = (u8[1]) { OXP_GET_PROPERTY };
return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
+ case GEN2_USAGE_PAGE:
+ data = (u8[3]) { OXP_GET_PROPERTY, 0x00, 0x02 };
+ return oxp_gen_2_property_out(OXP_FID_GEN2_RGB_EVENT, data, 3);
default:
return -ENODEV;
}
@@ -275,6 +355,16 @@ static int oxp_rgb_color_set(void)
data[3 * i + 3] = blue;
}
return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, size);
+ case GEN2_USAGE_PAGE:
+ size = 57;
+ data = (u8[57]) { OXP_EFFECT_MONO_TRUE, 0x00, 0x02 };
+
+ for (i = 1; i < size / 3; i++) {
+ data[3 * i] = red;
+ data[3 * i + 1] = green;
+ data[3 * i + 2] = blue;
+ }
+ return oxp_gen_2_property_out(OXP_FID_GEN2_RGB_EVENT, data, size);
default:
return -ENODEV;
}
@@ -311,6 +401,10 @@ static int oxp_rgb_effect_set(u8 effect)
data = (u8[1]) { effect };
ret = oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
break;
+ case GEN2_USAGE_PAGE:
+ data = (u8[3]) { effect, 0x00, 0x02 };
+ ret = oxp_gen_2_property_out(OXP_FID_GEN2_RGB_EVENT, data, 3);
+ break;
default:
ret = -ENODEV;
}
@@ -614,6 +708,7 @@ static int oxp_hid_probe(struct hid_device *hdev,
switch (up) {
case GEN1_USAGE_PAGE:
+ case GEN2_USAGE_PAGE:
ret = oxp_cfg_probe(hdev, up);
if (ret) {
hid_hw_close(hdev);
@@ -634,6 +729,7 @@ static void oxp_hid_remove(struct hid_device *hdev)
static const struct hid_device_id oxp_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_CRSC, USB_DEVICE_ID_ONEXPLAYER_GEN1) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_WCH, USB_DEVICE_ID_ONEXPLAYER_GEN2) },
{}
};
--
2.53.0
^ permalink raw reply related
* [PATCH 1/4] HID: hid-oxp: Add OneXPlayer configuration driver
From: Derek J. Clark @ 2026-03-22 3:16 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260322031615.1524307-1-derekjohn.clark@gmail.com>
Adds OneXPlayer HID configuration driver. In this initial driver patch,
add the RGB interface for the first generation of HID based RGB control.
This interface provides the following attributes:
- brightness: provided by the LED core, this works in a fairly unique
way on this device. The hardware accepts 5 brightness values (0-4),
which affects the brightness of the multicolor and animated effects
built into the MCU firmware. For monocolor settings, the device
expects the hardware brightness value to be pushed to maximum, then we
apply brightness adjustments mathematically based on % (0-100). This
leads to some odd conversion as we need the brightness slider to reach
the full range, but it has no affect when incrementing between the
division points for other effects.
- multi-intensity: provided by the LED core for red, green, and blue.
- effect: Allows the MCU to set 19 individual effects.
- effect_index: Lists the 19 valid effect names for the interface.
- enabled: Allows the MCU to toggle the RGB interface on/off.
- enabled_index: Lists the valid states for enabled.
- speed: Allows the MCU to set the animation rate for the various
effects.
- speed_range: Lists the valid range of speed (0-9).
The MCU also has a few odd quirks that make sending multiple synchronous
events challenging. It will essentially freeze if it receives another
message before it has finished processing the last command. It also will
not reply if you wait on it using a completion. To get around this, we
do a 200ms sleep inside a work queue thread and debounce all but the most
recent message using a 50ms mod_delayed_work. This will cache the last
write, queue the work, then return so userspace can release its write
thread. The work queue is only used for brightness/multi-intensity as
that is the path likely to receive rapid successive writes.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
MAINTAINERS | 6 +
drivers/hid/Kconfig | 12 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 3 +
drivers/hid/hid-oxp.c | 652 ++++++++++++++++++++++++++++++++++++++++++
5 files changed, 674 insertions(+)
create mode 100644 drivers/hid/hid-oxp.c
diff --git a/MAINTAINERS b/MAINTAINERS
index eeb8fcfa32eb..ba44ab2452be 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19228,6 +19228,12 @@ S: Maintained
F: drivers/mtd/nand/onenand/
F: include/linux/mtd/onenand*.h
+ONEXPLAYER HID DRIVER
+M: Derek J. Clark <derekjohn.clark@gmail.com>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-oxp.c
+
ONEXPLAYER PLATFORM EC DRIVER
M: Antheas Kapenekakis <lkml@antheas.dev>
M: Derek John Clark <derekjohn.clark@gmail.com>
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index a797549b580e..42af8fc15476 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -920,6 +920,18 @@ config HID_ORTEK
- Ortek WKB-2000
- Skycable wireless presenter
+config HID_OXP
+ tristate "OneXPlayer handheld controller configuration support"
+ depends on USB_HID
+ depends on LEDS_CLASS
+ depends on LEDS_CLASS_MULTICOLOR
+ help
+ Say Y here if you would like to enable support for OneXPlayer handheld
+ devices that come with RGB LED rings around the joysticks and macro buttons.
+
+ To compile this driver as a module, choose M here: the module will
+ be called hid-oxp.
+
config HID_PANTHERLORD
tristate "Pantherlord/GreenAsia game controller"
help
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 95fac34e8499..52e26a1d9df7 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -99,6 +99,7 @@ obj-$(CONFIG_HID_NTI) += hid-nti.o
obj-$(CONFIG_HID_NTRIG) += hid-ntrig.o
obj-$(CONFIG_HID_NVIDIA_SHIELD) += hid-nvidia-shield.o
obj-$(CONFIG_HID_ORTEK) += hid-ortek.o
+obj-$(CONFIG_HID_OXP) += hid-oxp.o
obj-$(CONFIG_HID_PRODIKEYS) += hid-prodikeys.o
obj-$(CONFIG_HID_PANTHERLORD) += hid-pl.o
obj-$(CONFIG_HID_PENMOUNT) += hid-penmount.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index f01020569dea..8b272d1ab9ba 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1102,6 +1102,9 @@
#define USB_VENDOR_ID_NVIDIA 0x0955
#define USB_DEVICE_ID_NVIDIA_THUNDERSTRIKE_CONTROLLER 0x7214
+#define USB_VENDOR_ID_CRSC 0x1a2c
+#define USB_DEVICE_ID_ONEXPLAYER_GEN1 0xb001
+
#define USB_VENDOR_ID_ONTRAK 0x0a07
#define USB_DEVICE_ID_ONTRAK_ADU100 0x0064
diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
new file mode 100644
index 000000000000..391de2798320
--- /dev/null
+++ b/drivers/hid/hid-oxp.c
@@ -0,0 +1,652 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * HID driver for OneXPlayer gamepad configuration devices.
+ *
+ * Copyright (c) 2026 Valve Corporation
+ */
+
+#include <linux/array_size.h>
+#include <linux/cleanup.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/kstrtox.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/mutex.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+#include <linux/workqueue.h>
+
+#include "hid-ids.h"
+
+#define OXP_PACKET_SIZE 64
+
+#define GEN1_MESSAGE_ID 0xff
+
+#define GEN1_USAGE_PAGE 0xff01
+
+enum oxp_function_index {
+ OXP_FID_GEN1_RGB_SET = 0x07,
+ OXP_FID_GEN1_RGB_REPLY = 0x0f,
+};
+
+static struct oxp_hid_cfg {
+ struct led_classdev_mc *led_mc;
+ struct hid_device *hdev;
+ struct mutex cfg_mutex; /*ensure single synchronous output report*/
+ u8 rgb_brightness;
+ u8 rgb_effect;
+ u8 rgb_speed;
+ u8 rgb_en;
+} drvdata;
+
+enum oxp_feature_en_index {
+ OXP_FEAT_DISABLED,
+ OXP_FEAT_ENABLED,
+};
+
+static const char *const oxp_feature_en_text[] = {
+ [OXP_FEAT_DISABLED] = "false",
+ [OXP_FEAT_ENABLED] = "true",
+};
+
+enum oxp_rgb_effect_index {
+ OXP_UNKNOWN,
+ OXP_EFFECT_AURORA,
+ OXP_EFFECT_BIRTHDAY,
+ OXP_EFFECT_FLOWING,
+ OXP_EFFECT_CHROMA_1,
+ OXP_EFFECT_NEON,
+ OXP_EFFECT_CHROMA_2,
+ OXP_EFFECT_DREAMY,
+ OXP_EFFECT_WARM,
+ OXP_EFFECT_CYBERPUNK,
+ OXP_EFFECT_SEA,
+ OXP_EFFECT_SUNSET,
+ OXP_EFFECT_COLORFUL,
+ OXP_EFFECT_MONSTER,
+ OXP_EFFECT_GREEN,
+ OXP_EFFECT_BLUE,
+ OXP_EFFECT_YELLOW,
+ OXP_EFFECT_TEAL,
+ OXP_EFFECT_PURPLE,
+ OXP_EFFECT_FOGGY,
+ OXP_EFFECT_MONO_LIST, /* placeholder for effect_index_show */
+};
+
+/* These belong to rgb_effect_index, but we want to hide them from
+ * rgb_effect_text
+ */
+
+#define OXP_GET_PROPERTY 0xfc
+#define OXP_SET_PROPERTY 0xfd
+#define OXP_EFFECT_MONO_TRUE 0xfe /* actual index for monocolor */
+
+static const char *const oxp_rgb_effect_text[] = {
+ [OXP_UNKNOWN] = "unknown",
+ [OXP_EFFECT_AURORA] = "aurora",
+ [OXP_EFFECT_BIRTHDAY] = "birthday_cake",
+ [OXP_EFFECT_FLOWING] = "flowing_light",
+ [OXP_EFFECT_CHROMA_1] = "chroma_popping",
+ [OXP_EFFECT_NEON] = "neon",
+ [OXP_EFFECT_CHROMA_2] = "chroma_breathing",
+ [OXP_EFFECT_DREAMY] = "dreamy",
+ [OXP_EFFECT_WARM] = "warm_sun",
+ [OXP_EFFECT_CYBERPUNK] = "cyberpunk",
+ [OXP_EFFECT_SEA] = "sea_foam",
+ [OXP_EFFECT_SUNSET] = "sunset_afterglow",
+ [OXP_EFFECT_COLORFUL] = "colorful",
+ [OXP_EFFECT_MONSTER] = "monster_woke",
+ [OXP_EFFECT_GREEN] = "green_breathing",
+ [OXP_EFFECT_BLUE] = "blue_breathing",
+ [OXP_EFFECT_YELLOW] = "yellow_breathing",
+ [OXP_EFFECT_TEAL] = "teal_breathing",
+ [OXP_EFFECT_PURPLE] = "purple_breathing",
+ [OXP_EFFECT_FOGGY] = "foggy_haze",
+ [OXP_EFFECT_MONO_LIST] = "monocolor",
+};
+
+struct oxp_gen_1_rgb_report {
+ u8 report_id;
+ u8 message_id;
+ u8 padding_2[2];
+ u8 effect;
+ u8 enabled;
+ u8 speed;
+ u8 brightness;
+ u8 red;
+ u8 green;
+ u8 blue;
+} __packed;
+
+static u16 get_usage_page(struct hid_device *hdev)
+{
+ return hdev->collection[0].usage >> 16;
+}
+
+static int oxp_hid_raw_event_gen_1(struct hid_device *hdev,
+ struct hid_report *report, u8 *data,
+ int size)
+{
+ struct led_classdev_mc *led_mc = drvdata.led_mc;
+ struct oxp_gen_1_rgb_report *rgb_rep;
+
+ if (data[1] != OXP_FID_GEN1_RGB_REPLY)
+ return 0;
+
+ rgb_rep = (struct oxp_gen_1_rgb_report *)data;
+ /* Ensure we save monocolor as the list value */
+ drvdata.rgb_effect =
+ rgb_rep->effect == OXP_EFFECT_MONO_TRUE ?
+ OXP_EFFECT_MONO_LIST :
+ rgb_rep->effect;
+ drvdata.rgb_speed = rgb_rep->speed;
+ drvdata.rgb_en = rgb_rep->enabled == 0 ? OXP_FEAT_DISABLED :
+ OXP_FEAT_ENABLED;
+ drvdata.rgb_brightness = rgb_rep->brightness;
+ led_mc->led_cdev.brightness = rgb_rep->brightness / 4 *
+ led_mc->led_cdev.max_brightness;
+ /* If monocolor had less than 100% brightness on the previous boot,
+ * there will be no reliable way to determine the real intensity.
+ * Since intensity scaling is used with a hardware brightness set at max,
+ * our brightness will always look like 100%. Use the last set value to
+ * prevent successive boots from lowering the brightness further.
+ * Brightness will be "wrong" but the effect will remain the same visually.
+ */
+ led_mc->subled_info[0].intensity = rgb_rep->red;
+ led_mc->subled_info[1].intensity = rgb_rep->green;
+ led_mc->subled_info[2].intensity = rgb_rep->blue;
+
+ return 0;
+}
+
+static int oxp_hid_raw_event(struct hid_device *hdev, struct hid_report *report,
+ u8 *data, int size)
+{
+ u16 up = get_usage_page(hdev);
+
+ dev_dbg(&hdev->dev, "raw event data: [%*ph]\n", OXP_PACKET_SIZE, data);
+
+ switch (up) {
+ case GEN1_USAGE_PAGE:
+ return oxp_hid_raw_event_gen_1(hdev, report, data, size);
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+static int mcu_property_out(u8 *header, size_t header_size, u8 *data,
+ size_t data_size, u8 *footer, size_t footer_size)
+{
+ unsigned char *dmabuf __free(kfree) = kzalloc(OXP_PACKET_SIZE, GFP_KERNEL);
+ int ret;
+
+ if (!dmabuf)
+ return -ENOMEM;
+
+ if (header_size + data_size + footer_size > OXP_PACKET_SIZE)
+ return -EINVAL;
+
+ guard(mutex)(&drvdata.cfg_mutex);
+ memcpy(dmabuf, header, header_size);
+ memcpy(dmabuf + header_size, data, data_size);
+ if (footer_size)
+ memcpy(dmabuf + OXP_PACKET_SIZE - footer_size, footer, footer_size);
+
+ dev_dbg(&drvdata.hdev->dev, "raw data: [%*ph]\n", OXP_PACKET_SIZE, dmabuf);
+
+ ret = hid_hw_output_report(drvdata.hdev, dmabuf, OXP_PACKET_SIZE);
+ if (ret < 0)
+ return ret;
+
+ /* MCU takes 200ms to be ready for another command. */
+ msleep(200);
+ return ret == OXP_PACKET_SIZE ? 0 : -EIO;
+}
+
+static int oxp_gen_1_property_out(enum oxp_function_index fid, u8 *data,
+ u8 data_size)
+{
+ u8 header[] = { fid, GEN1_MESSAGE_ID };
+ size_t header_size = ARRAY_SIZE(header);
+
+ return mcu_property_out(header, header_size, data, data_size, NULL, 0);
+}
+
+static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
+{
+ u16 up = get_usage_page(drvdata.hdev);
+ u8 *data;
+
+ /* Always default to max brightness and use intensity scaling when in
+ * monocolor mode.
+ */
+ switch (up) {
+ case GEN1_USAGE_PAGE:
+ data = (u8[4]) { OXP_SET_PROPERTY, enabled, speed, brightness };
+ if (drvdata.rgb_effect == OXP_EFFECT_MONO_LIST)
+ data[3] = 0x04;
+ return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 4);
+ default:
+ return -ENODEV;
+ }
+}
+
+static ssize_t oxp_rgb_status_show(void)
+{
+ u16 up = get_usage_page(drvdata.hdev);
+ u8 *data;
+
+ switch (up) {
+ case GEN1_USAGE_PAGE:
+ data = (u8[1]) { OXP_GET_PROPERTY };
+ return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
+ default:
+ return -ENODEV;
+ }
+}
+
+static int oxp_rgb_color_set(void)
+{
+ u8 max_br = drvdata.led_mc->led_cdev.max_brightness;
+ u8 br = drvdata.led_mc->led_cdev.brightness;
+ u16 up = get_usage_page(drvdata.hdev);
+ u8 green, red, blue;
+ size_t size;
+ u8 *data;
+ int i;
+
+ red = br * drvdata.led_mc->subled_info[0].intensity / max_br;
+ green = br * drvdata.led_mc->subled_info[1].intensity / max_br;
+ blue = br * drvdata.led_mc->subled_info[2].intensity / max_br;
+
+ switch (up) {
+ case GEN1_USAGE_PAGE:
+ size = 55;
+ data = (u8[55]) { OXP_EFFECT_MONO_TRUE };
+
+ for (i = 0; i < (size - 1) / 3; i++) {
+ data[3 * i + 1] = red;
+ data[3 * i + 2] = green;
+ data[3 * i + 3] = blue;
+ }
+ return oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, size);
+ default:
+ return -ENODEV;
+ }
+}
+
+static int oxp_rgb_effect_set(u8 effect)
+{
+ u16 up = get_usage_page(drvdata.hdev);
+ u8 *data;
+ int ret;
+
+ switch (effect) {
+ case OXP_EFFECT_AURORA:
+ case OXP_EFFECT_BIRTHDAY:
+ case OXP_EFFECT_FLOWING:
+ case OXP_EFFECT_CHROMA_1:
+ case OXP_EFFECT_NEON:
+ case OXP_EFFECT_CHROMA_2:
+ case OXP_EFFECT_DREAMY:
+ case OXP_EFFECT_WARM:
+ case OXP_EFFECT_CYBERPUNK:
+ case OXP_EFFECT_SEA:
+ case OXP_EFFECT_SUNSET:
+ case OXP_EFFECT_COLORFUL:
+ case OXP_EFFECT_MONSTER:
+ case OXP_EFFECT_GREEN:
+ case OXP_EFFECT_BLUE:
+ case OXP_EFFECT_YELLOW:
+ case OXP_EFFECT_TEAL:
+ case OXP_EFFECT_PURPLE:
+ case OXP_EFFECT_FOGGY:
+ switch (up) {
+ case GEN1_USAGE_PAGE:
+ data = (u8[1]) { effect };
+ ret = oxp_gen_1_property_out(OXP_FID_GEN1_RGB_SET, data, 1);
+ break;
+ default:
+ ret = -ENODEV;
+ }
+ break;
+ case OXP_EFFECT_MONO_LIST:
+ ret = oxp_rgb_color_set();
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (ret)
+ return ret;
+
+ drvdata.rgb_effect = effect;
+
+ return 0;
+}
+
+static ssize_t enabled_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+ u8 val;
+
+ ret = sysfs_match_string(oxp_feature_en_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+
+ ret = oxp_rgb_status_store(val, drvdata.rgb_speed,
+ drvdata.rgb_brightness);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_en = val;
+ return count;
+}
+
+static ssize_t enabled_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = oxp_rgb_status_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_en >= ARRAY_SIZE(oxp_feature_en_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", oxp_feature_en_text[drvdata.rgb_en]);
+}
+static DEVICE_ATTR_RW(enabled);
+
+static ssize_t enabled_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ size_t count = 0;
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(oxp_feature_en_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", oxp_feature_en_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(enabled_index);
+
+static ssize_t effect_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+ u8 val;
+
+ ret = sysfs_match_string(oxp_rgb_effect_text, buf);
+ if (ret < 0)
+ return ret;
+
+ val = ret;
+
+ ret = oxp_rgb_status_store(drvdata.rgb_en, drvdata.rgb_speed,
+ drvdata.rgb_brightness);
+ if (ret)
+ return ret;
+
+ ret = oxp_rgb_effect_set(val);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t effect_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = oxp_rgb_status_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_effect >= ARRAY_SIZE(oxp_rgb_effect_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", oxp_rgb_effect_text[drvdata.rgb_effect]);
+}
+
+static DEVICE_ATTR_RW(effect);
+
+static ssize_t effect_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ size_t count = 0;
+ unsigned int i;
+
+ for (i = 1; i < ARRAY_SIZE(oxp_rgb_effect_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", oxp_rgb_effect_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(effect_index);
+
+static ssize_t speed_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ int ret;
+ u8 val;
+
+ ret = kstrtou8(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val > 9)
+ return -EINVAL;
+
+ ret = oxp_rgb_status_store(drvdata.rgb_en, val, drvdata.rgb_brightness);
+ if (ret)
+ return ret;
+
+ drvdata.rgb_speed = val;
+ return count;
+}
+
+static ssize_t speed_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ int ret;
+
+ ret = oxp_rgb_status_show();
+ if (ret)
+ return ret;
+
+ if (drvdata.rgb_speed > 9)
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%hhu\n", drvdata.rgb_speed);
+}
+static DEVICE_ATTR_RW(speed);
+
+static ssize_t speed_range_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "0-9\n");
+}
+static DEVICE_ATTR_RO(speed_range);
+
+static void oxp_rgb_queue_fn(struct work_struct *work)
+{
+ unsigned int max_brightness = drvdata.led_mc->led_cdev.max_brightness;
+ unsigned int brightness = drvdata.led_mc->led_cdev.brightness;
+ u8 val = 4 * brightness / max_brightness;
+ int ret;
+
+ if (drvdata.rgb_brightness != val) {
+ ret = oxp_rgb_status_store(drvdata.rgb_en, drvdata.rgb_speed, val);
+ if (ret)
+ dev_err(drvdata.led_mc->led_cdev.dev,
+ "Error: Failed to write RGB Status: %i\n", ret);
+
+ drvdata.rgb_brightness = val;
+ }
+
+ if (drvdata.rgb_effect != OXP_EFFECT_MONO_LIST)
+ return;
+
+ ret = oxp_rgb_effect_set(drvdata.rgb_effect);
+ if (ret)
+ dev_err(drvdata.led_mc->led_cdev.dev, "Error: Failed to write RGB color: %i\n",
+ ret);
+}
+
+static DECLARE_DELAYED_WORK(oxp_rgb_queue, oxp_rgb_queue_fn);
+
+static void oxp_rgb_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ led_cdev->brightness = brightness;
+ mod_delayed_work(system_wq, &oxp_rgb_queue, msecs_to_jiffies(50));
+}
+
+static struct attribute *oxp_rgb_attrs[] = {
+ &dev_attr_effect.attr,
+ &dev_attr_effect_index.attr,
+ &dev_attr_enabled.attr,
+ &dev_attr_enabled_index.attr,
+ &dev_attr_speed.attr,
+ &dev_attr_speed_range.attr,
+ NULL,
+};
+
+static const struct attribute_group oxp_rgb_attr_group = {
+ .attrs = oxp_rgb_attrs,
+};
+
+static struct mc_subled oxp_rgb_subled_info[] = {
+ {
+ .color_index = LED_COLOR_ID_RED,
+ .intensity = 0x24,
+ .channel = 0x1,
+ },
+ {
+ .color_index = LED_COLOR_ID_GREEN,
+ .intensity = 0x22,
+ .channel = 0x2,
+ },
+ {
+ .color_index = LED_COLOR_ID_BLUE,
+ .intensity = 0x99,
+ .channel = 0x3,
+ },
+};
+
+static struct led_classdev_mc oxp_cdev_rgb = {
+ .led_cdev = {
+ .name = "oxp:rgb:joystick_rings",
+ .color = LED_COLOR_ID_RGB,
+ .brightness = 0x64,
+ .max_brightness = 0x64,
+ .brightness_set = oxp_rgb_brightness_set,
+ },
+ .num_colors = ARRAY_SIZE(oxp_rgb_subled_info),
+ .subled_info = oxp_rgb_subled_info,
+};
+
+static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
+{
+ int ret;
+
+ hid_set_drvdata(hdev, &drvdata);
+ drvdata.hdev = hdev;
+ drvdata.led_mc = &oxp_cdev_rgb;
+ mutex_init(&drvdata.cfg_mutex);
+
+ ret = devm_led_classdev_multicolor_register(&hdev->dev, &oxp_cdev_rgb);
+ if (ret)
+ return dev_err_probe(&hdev->dev, ret,
+ "Failed to create RGB device\n");
+
+ ret = devm_device_add_group(drvdata.led_mc->led_cdev.dev,
+ &oxp_rgb_attr_group);
+ if (ret)
+ return dev_err_probe(drvdata.led_mc->led_cdev.dev, ret,
+ "Failed to create RGB configuration attributes\n");
+
+ ret = oxp_rgb_status_show();
+ if (ret)
+ dev_warn(drvdata.led_mc->led_cdev.dev,
+ "Failed to query RGB initial state: %i\n", ret);
+
+ return 0;
+}
+
+static int oxp_hid_probe(struct hid_device *hdev,
+ const struct hid_device_id *id)
+{
+ int ret;
+ u16 up;
+
+ ret = hid_parse(hdev);
+ if (ret)
+ return dev_err_probe(&hdev->dev, ret, "Failed to parse HID device\n");
+
+ ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
+ if (ret)
+ return dev_err_probe(&hdev->dev, ret, "Failed to start HID device\n");
+
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_hw_stop(hdev);
+ return dev_err_probe(&hdev->dev, ret, "Failed to open HID device\n");
+ }
+
+ up = get_usage_page(hdev);
+ dev_dbg(&hdev->dev, "Got usage page %04x\n", up);
+
+ switch (up) {
+ case GEN1_USAGE_PAGE:
+ ret = oxp_cfg_probe(hdev, up);
+ if (ret) {
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+ }
+
+ return ret;
+ default:
+ return 0;
+ }
+}
+
+static void oxp_hid_remove(struct hid_device *hdev)
+{
+ hid_hw_close(hdev);
+ hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id oxp_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_CRSC, USB_DEVICE_ID_ONEXPLAYER_GEN1) },
+ {}
+};
+
+MODULE_DEVICE_TABLE(hid, oxp_devices);
+static struct hid_driver hid_oxp = {
+ .name = "hid-oxp",
+ .id_table = oxp_devices,
+ .probe = oxp_hid_probe,
+ .remove = oxp_hid_remove,
+ .raw_event = oxp_hid_raw_event,
+};
+module_hid_driver(hid_oxp);
+
+MODULE_AUTHOR("Derek J. Clark <derekjohn.clark@gmail.com>");
+MODULE_DESCRIPTION("Driver for OneXPlayer HID Interfaces");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related
* [PATCH 3/4] HID: hid-oxp: Add Second Generation Takeover Mode
From: Derek J. Clark @ 2026-03-22 3:16 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260322031615.1524307-1-derekjohn.clark@gmail.com>
Adds "takeover_enabled" attribute to second generation OneXPlayer
configuration HID devices. This attribute initiates a mode shift in the
device MCU that puts it into a state where all events are routed to an
hidraw interface instead of the xpad evdev interface. This allows for
debugging the hardware input mapping, and allows some userspace tools to
consume the interface to add support for features that are unable to be
exposed through the evdev, such as treating the M1 and M2 accessory
buttons as unique inputs.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-oxp.c | 81 +++++++++++++++++++++++++++++++++++++++++++
1 file changed, 81 insertions(+)
diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index 587e0d57c85f..5fed2799a2ad 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -32,6 +32,7 @@
enum oxp_function_index {
OXP_FID_GEN1_RGB_SET = 0x07,
OXP_FID_GEN1_RGB_REPLY = 0x0f,
+ OXP_FID_GEN2_TOGGLE_MODE = 0xb2,
OXP_FID_GEN2_RGB_EVENT = 0xb8,
};
@@ -39,12 +40,15 @@ static struct oxp_hid_cfg {
struct led_classdev_mc *led_mc;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
+ u8 takeover_enabled;
u8 rgb_brightness;
u8 rgb_effect;
u8 rgb_speed;
u8 rgb_en;
} drvdata;
+#define OXP_TAKEOVER_ENABLED_TRUE 0x03
+
enum oxp_feature_en_index {
OXP_FEAT_DISABLED,
OXP_FEAT_ENABLED,
@@ -289,6 +293,74 @@ static int oxp_gen_2_property_out(enum oxp_function_index fid, u8 *data,
footer_size);
}
+static ssize_t button_takeover_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ u16 up = get_usage_page(drvdata.hdev);
+ u8 data[3] = { 0x00, 0x01, 0x02 };
+ u8 val = 0;
+ int ret;
+
+ if (up != GEN2_USAGE_PAGE)
+ return -EINVAL;
+
+ ret = sysfs_match_string(oxp_feature_en_text, buf);
+ if (ret < 0)
+ return ret;
+ val = ret;
+
+ switch (val) {
+ case OXP_FEAT_DISABLED:
+ break;
+ case OXP_FEAT_ENABLED:
+ data[0] = OXP_TAKEOVER_ENABLED_TRUE;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ ret = oxp_gen_2_property_out(OXP_FID_GEN2_TOGGLE_MODE, data, 3);
+ if (ret)
+ return ret;
+
+ return count;
+}
+
+static ssize_t button_takeover_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", oxp_feature_en_text[drvdata.takeover_enabled]);
+}
+static DEVICE_ATTR_RW(button_takeover);
+
+static ssize_t button_takeover_index_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(oxp_feature_en_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", oxp_feature_en_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(button_takeover_index);
+
+static struct attribute *oxp_cfg_attrs[] = {
+ &dev_attr_button_takeover.attr,
+ &dev_attr_button_takeover_index.attr,
+ NULL,
+};
+
+static const struct attribute_group oxp_cfg_attrs_group = {
+ .attrs = oxp_cfg_attrs,
+};
+
static int oxp_rgb_status_store(u8 enabled, u8 speed, u8 brightness)
{
u16 up = get_usage_page(drvdata.hdev);
@@ -680,6 +752,15 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
dev_warn(drvdata.led_mc->led_cdev.dev,
"Failed to query RGB initial state: %i\n", ret);
+ /* Below features are only implemented in gen 2 */
+ if (up != GEN2_USAGE_PAGE)
+ return 0;
+
+ ret = devm_device_add_group(&hdev->dev, &oxp_cfg_attrs_group);
+ if (ret)
+ return dev_err_probe(&hdev->dev, ret,
+ "Failed to attach configuration attributes\n");
+
return 0;
}
--
2.53.0
^ permalink raw reply related
* [PATCH 4/4] HID: hid-oxp: Add Button Mapping Interface
From: Derek J. Clark @ 2026-03-22 3:16 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires
Cc: Pierre-Loup A . Griffais, Lambert Fan, Derek J . Clark,
linux-input, linux-doc, linux-kernel
In-Reply-To: <20260322031615.1524307-1-derekjohn.clark@gmail.com>
Adds button mapping interface for second generation OneXPlayer
configuration HID interfaces. This interface allows the MCU to swap
button mappings at the hardware level. The current state cannot be
retrieved, and the mappings may have been modified in Windows prior, so
we reset the button mapping at init and expose an attribute to allow
userspace to do this again at any time.
The interface requires two pages of button mapping data to be sent
before the settings will take place. Since the MCU requires a 200ms
delay after each message (total 400ms for these attributes) use the same
debounce work queue method we used for RGB. This will allow for
userspace or udev rules to rapidly map all buttons. The values will
be cached before the final write is finally sent to the device.
Signed-off-by: Derek J. Clark <derekjohn.clark@gmail.com>
---
drivers/hid/hid-oxp.c | 510 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 510 insertions(+)
diff --git a/drivers/hid/hid-oxp.c b/drivers/hid/hid-oxp.c
index 5fed2799a2ad..915c17b97db0 100644
--- a/drivers/hid/hid-oxp.c
+++ b/drivers/hid/hid-oxp.c
@@ -33,10 +33,126 @@ enum oxp_function_index {
OXP_FID_GEN1_RGB_SET = 0x07,
OXP_FID_GEN1_RGB_REPLY = 0x0f,
OXP_FID_GEN2_TOGGLE_MODE = 0xb2,
+ OXP_FID_GEN2_KEY_STATE = 0xb4,
OXP_FID_GEN2_RGB_EVENT = 0xb8,
};
+enum oxp_joybutton_index {
+ BUTTON_A = 0x01,
+ BUTTON_B,
+ BUTTON_X,
+ BUTTON_Y,
+ BUTTON_LB,
+ BUTTON_RB,
+ BUTTON_LT,
+ BUTTON_RT,
+ BUTTON_START,
+ BUTTON_SELECT,
+ BUTTON_L3,
+ BUTTON_R3,
+ BUTTON_DUP,
+ BUTTON_DDOWN,
+ BUTTON_DLEFT,
+ BUTTON_DRIGHT,
+ JOY_L_UP,
+ JOY_L_UP_RIGHT,
+ JOY_L_RIGHT,
+ JOY_L_DOWN_RIGHT,
+ JOY_L_DOWN,
+ JOY_L_DOWN_LEFT,
+ JOY_L_LEFT,
+ JOY_L_UP_LEFT,
+ JOY_R_UP,
+ JOY_R_UP_RIGHT,
+ JOY_R_RIGHT,
+ JOY_R_DOWN_RIGHT,
+ JOY_R_DOWN,
+ JOY_R_DOWN_LEFT,
+ JOY_R_LEFT,
+ JOY_R_UP_LEFT,
+ BUTTON_GUIDE = 0x22,
+};
+
+static const char *const oxp_joybutton_text[] = {
+ [BUTTON_A] = "a",
+ [BUTTON_B] = "b",
+ [BUTTON_X] = "x",
+ [BUTTON_Y] = "y",
+ [BUTTON_LB] = "lb",
+ [BUTTON_RB] = "rb",
+ [BUTTON_LT] = "lt",
+ [BUTTON_RT] = "rt",
+ [BUTTON_START] = "start",
+ [BUTTON_SELECT] = "select",
+ [BUTTON_L3] = "l3",
+ [BUTTON_R3] = "r3",
+ [BUTTON_DUP] = "d_up",
+ [BUTTON_DDOWN] = "d_down",
+ [BUTTON_DLEFT] = "d_left",
+ [BUTTON_DRIGHT] = "d_right",
+ [JOY_L_UP] = "joy_l_up",
+ [JOY_L_UP_RIGHT] = "joy_l_up_right",
+ [JOY_L_RIGHT] = "joy_l_right",
+ [JOY_L_DOWN_RIGHT] = "joy_l_down_right",
+ [JOY_L_DOWN] = "joy_l_down",
+ [JOY_L_DOWN_LEFT] = "joy_l_down_left",
+ [JOY_L_LEFT] = "joy_l_left",
+ [JOY_L_UP_LEFT] = "joy_l_up_left",
+ [JOY_R_UP] = "joy_r_up",
+ [JOY_R_UP_RIGHT] = "joy_r_up_right",
+ [JOY_R_RIGHT] = "joy_r_right",
+ [JOY_R_DOWN_RIGHT] = "joy_r_down_right",
+ [JOY_R_DOWN] = "joy_r_down",
+ [JOY_R_DOWN_LEFT] = "joy_r_down_left",
+ [JOY_R_LEFT] = "joy_r_left",
+ [JOY_R_UP_LEFT] = "joy_r_up_left",
+ [BUTTON_GUIDE] = "guide",
+};
+
+enum oxp_custom_button_index {
+ BUTTON_M1 = 0x22,
+ BUTTON_M2,
+ /* These are unused currently, reserved for future devices */
+ BUTTON_M3,
+ BUTTON_M4,
+ BUTTON_M5,
+ BUTTON_M6,
+};
+
+struct oxp_button {
+ u8 index;
+ u8 mode;
+ u8 mapping;
+ u8 padding[3];
+} __packed;
+
+struct oxp_bmap_page_1 {
+ struct oxp_button btn_a;
+ struct oxp_button btn_b;
+ struct oxp_button btn_x;
+ struct oxp_button btn_y;
+ struct oxp_button btn_lb;
+ struct oxp_button btn_rb;
+ struct oxp_button btn_lt;
+ struct oxp_button btn_rt;
+ struct oxp_button btn_start;
+} __packed;
+
+struct oxp_bmap_page_2 {
+ struct oxp_button btn_select;
+ struct oxp_button btn_l3;
+ struct oxp_button btn_r3;
+ struct oxp_button btn_dup;
+ struct oxp_button btn_ddown;
+ struct oxp_button btn_dleft;
+ struct oxp_button btn_dright;
+ struct oxp_button btn_m1;
+ struct oxp_button btn_m2;
+} __packed;
+
static struct oxp_hid_cfg {
+ struct oxp_bmap_page_1 *bmap_1;
+ struct oxp_bmap_page_2 *bmap_2;
struct led_classdev_mc *led_mc;
struct hid_device *hdev;
struct mutex cfg_mutex; /*ensure single synchronous output report*/
@@ -144,6 +260,10 @@ struct oxp_gen_2_rgb_report {
u8 effect;
} __packed;
+struct oxp_button_attr {
+ u8 index;
+};
+
static u16 get_usage_page(struct hid_device *hdev)
{
return hdev->collection[0].usage >> 16;
@@ -351,9 +471,380 @@ static ssize_t button_takeover_index_show(struct device *dev,
}
static DEVICE_ATTR_RO(button_takeover_index);
+static void oxp_set_defaults_bmap_1(struct oxp_bmap_page_1 *bmap)
+{
+ bmap->btn_a.index = BUTTON_A;
+ bmap->btn_a.mode = 0x01;
+ bmap->btn_a.mapping = BUTTON_A;
+ bmap->btn_b.index = BUTTON_B;
+ bmap->btn_b.mode = 0x01;
+ bmap->btn_b.mapping = BUTTON_B;
+ bmap->btn_x.index = BUTTON_X;
+ bmap->btn_x.mode = 0x01;
+ bmap->btn_x.mapping = BUTTON_X;
+ bmap->btn_y.index = BUTTON_Y;
+ bmap->btn_y.mode = 0x01;
+ bmap->btn_y.mapping = BUTTON_Y;
+ bmap->btn_lb.index = BUTTON_LB;
+ bmap->btn_lb.mode = 0x01;
+ bmap->btn_lb.mapping = BUTTON_LB;
+ bmap->btn_rb.index = BUTTON_RB;
+ bmap->btn_rb.mode = 0x01;
+ bmap->btn_rb.mapping = BUTTON_RB;
+ bmap->btn_lt.index = BUTTON_LT;
+ bmap->btn_lt.mode = 0x01;
+ bmap->btn_lt.mapping = BUTTON_LT;
+ bmap->btn_rt.index = BUTTON_RT;
+ bmap->btn_rt.mode = 0x01;
+ bmap->btn_rt.mapping = BUTTON_RT;
+ bmap->btn_start.index = BUTTON_START;
+ bmap->btn_start.mode = 0x01;
+ bmap->btn_start.mapping = BUTTON_START;
+}
+
+static void oxp_set_defaults_bmap_2(struct oxp_bmap_page_2 *bmap)
+{
+ bmap->btn_select.index = BUTTON_SELECT;
+ bmap->btn_select.mode = 0x01;
+ bmap->btn_select.mapping = BUTTON_SELECT;
+ bmap->btn_l3.index = BUTTON_L3;
+ bmap->btn_l3.mode = 0x01;
+ bmap->btn_l3.mapping = BUTTON_L3;
+ bmap->btn_r3.index = BUTTON_R3;
+ bmap->btn_r3.mode = 0x01;
+ bmap->btn_r3.mapping = BUTTON_R3;
+ bmap->btn_dup.index = BUTTON_DUP;
+ bmap->btn_dup.mode = 0x01;
+ bmap->btn_dup.mapping = BUTTON_DUP;
+ bmap->btn_ddown.index = BUTTON_DDOWN;
+ bmap->btn_ddown.mode = 0x01;
+ bmap->btn_ddown.mapping = BUTTON_DDOWN;
+ bmap->btn_dleft.index = BUTTON_DLEFT;
+ bmap->btn_dleft.mode = 0x01;
+ bmap->btn_dleft.mapping = BUTTON_DLEFT;
+ bmap->btn_dright.index = BUTTON_DRIGHT;
+ bmap->btn_dright.mode = 0x01;
+ bmap->btn_dright.mapping = BUTTON_DRIGHT;
+ bmap->btn_m1.index = BUTTON_M1;
+ bmap->btn_m1.mode = 0x01;
+ bmap->btn_m1.mapping = BUTTON_LT;
+ bmap->btn_m2.index = BUTTON_M2;
+ bmap->btn_m2.mode = 0x01;
+ bmap->btn_m2.mapping = BUTTON_RT;
+}
+
+static int oxp_set_buttons(void)
+{
+ u8 data[59] = { 0x02, 0x00, 0x00, 0x00, 0x01 };
+ u16 up = get_usage_page(drvdata.hdev);
+ int ret;
+
+ if (up != GEN2_USAGE_PAGE)
+ return -EINVAL;
+
+ memcpy(data + 5, drvdata.bmap_1, sizeof(struct oxp_bmap_page_1));
+ ret = oxp_gen_2_property_out(OXP_FID_GEN2_KEY_STATE, data, ARRAY_SIZE(data));
+ if (ret)
+ return ret;
+
+ memcpy(data + 5, drvdata.bmap_2, sizeof(struct oxp_bmap_page_2));
+ return oxp_gen_2_property_out(OXP_FID_GEN2_KEY_STATE, data, ARRAY_SIZE(data));
+}
+
+static int oxp_reset_buttons(void)
+{
+ oxp_set_defaults_bmap_1(drvdata.bmap_1);
+ oxp_set_defaults_bmap_2(drvdata.bmap_2);
+ return oxp_set_buttons();
+}
+
+static ssize_t reset_buttons_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count)
+{
+ int val, ret;
+
+ ret = kstrtoint(buf, 10, &val);
+ if (ret)
+ return ret;
+
+ if (val != 1)
+ return -EINVAL;
+
+ ret = oxp_reset_buttons();
+ if (ret)
+ return ret;
+
+ return count;
+}
+static DEVICE_ATTR_WO(reset_buttons);
+
+static void oxp_btn_queue_fn(struct work_struct *work)
+{
+ int ret;
+
+ ret = oxp_set_buttons();
+ if (ret)
+ dev_err(&drvdata.hdev->dev,
+ "Error: Failed to write button mapping: %i\n", ret);
+}
+
+static DECLARE_DELAYED_WORK(oxp_btn_queue, oxp_btn_queue_fn);
+
+static ssize_t map_button_store(struct device *dev,
+ struct device_attribute *attr, const char *buf,
+ size_t count, u8 index)
+{
+ int ret;
+ u8 val;
+
+ ret = sysfs_match_string(oxp_joybutton_text, buf);
+ if (ret < 0)
+ return ret;
+
+ val = ret;
+
+ switch (index) {
+ case BUTTON_A:
+ drvdata.bmap_1->btn_a.mapping = val;
+ break;
+ case BUTTON_B:
+ drvdata.bmap_1->btn_b.mapping = val;
+ break;
+ case BUTTON_X:
+ drvdata.bmap_1->btn_x.mapping = val;
+ break;
+ case BUTTON_Y:
+ drvdata.bmap_1->btn_y.mapping = val;
+ break;
+ case BUTTON_LB:
+ drvdata.bmap_1->btn_lb.mapping = val;
+ break;
+ case BUTTON_RB:
+ drvdata.bmap_1->btn_rb.mapping = val;
+ break;
+ case BUTTON_LT:
+ drvdata.bmap_1->btn_lt.mapping = val;
+ break;
+ case BUTTON_RT:
+ drvdata.bmap_1->btn_rt.mapping = val;
+ break;
+ case BUTTON_START:
+ drvdata.bmap_1->btn_start.mapping = val;
+ break;
+ case BUTTON_SELECT:
+ drvdata.bmap_2->btn_select.mapping = val;
+ break;
+ case BUTTON_L3:
+ drvdata.bmap_2->btn_l3.mapping = val;
+ break;
+ case BUTTON_R3:
+ drvdata.bmap_2->btn_r3.mapping = val;
+ break;
+ case BUTTON_DUP:
+ drvdata.bmap_2->btn_dup.mapping = val;
+ break;
+ case BUTTON_DDOWN:
+ drvdata.bmap_2->btn_ddown.mapping = val;
+ break;
+ case BUTTON_DLEFT:
+ drvdata.bmap_2->btn_dleft.mapping = val;
+ break;
+ case BUTTON_DRIGHT:
+ drvdata.bmap_2->btn_dright.mapping = val;
+ break;
+ case BUTTON_M1:
+ drvdata.bmap_2->btn_m1.mapping = val;
+ break;
+ case BUTTON_M2:
+ drvdata.bmap_2->btn_m2.mapping = val;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ mod_delayed_work(system_wq, &oxp_btn_queue, msecs_to_jiffies(50));
+ return count;
+}
+
+static ssize_t map_button_show(struct device *dev,
+ struct device_attribute *attr, char *buf,
+ u8 index)
+{
+ u8 i;
+
+ switch (index) {
+ case BUTTON_A:
+ i = drvdata.bmap_1->btn_a.mapping;
+ break;
+ case BUTTON_B:
+ i = drvdata.bmap_1->btn_b.mapping;
+ break;
+ case BUTTON_X:
+ i = drvdata.bmap_1->btn_x.mapping;
+ break;
+ case BUTTON_Y:
+ i = drvdata.bmap_1->btn_y.mapping;
+ break;
+ case BUTTON_LB:
+ i = drvdata.bmap_1->btn_lb.mapping;
+ break;
+ case BUTTON_RB:
+ i = drvdata.bmap_1->btn_rb.mapping;
+ break;
+ case BUTTON_LT:
+ i = drvdata.bmap_1->btn_lt.mapping;
+ break;
+ case BUTTON_RT:
+ i = drvdata.bmap_1->btn_rt.mapping;
+ break;
+ case BUTTON_START:
+ i = drvdata.bmap_1->btn_start.mapping;
+ break;
+ case BUTTON_SELECT:
+ i = drvdata.bmap_2->btn_select.mapping;
+ break;
+ case BUTTON_L3:
+ i = drvdata.bmap_2->btn_l3.mapping;
+ break;
+ case BUTTON_R3:
+ i = drvdata.bmap_2->btn_r3.mapping;
+ break;
+ case BUTTON_DUP:
+ i = drvdata.bmap_2->btn_dup.mapping;
+ break;
+ case BUTTON_DDOWN:
+ i = drvdata.bmap_2->btn_ddown.mapping;
+ break;
+ case BUTTON_DLEFT:
+ i = drvdata.bmap_2->btn_dleft.mapping;
+ break;
+ case BUTTON_DRIGHT:
+ i = drvdata.bmap_2->btn_dright.mapping;
+ break;
+ case BUTTON_M1:
+ i = drvdata.bmap_2->btn_m1.mapping;
+ break;
+ case BUTTON_M2:
+ i = drvdata.bmap_2->btn_m2.mapping;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ if (i >= ARRAY_SIZE(oxp_joybutton_text))
+ return -EINVAL;
+
+ return sysfs_emit(buf, "%s\n", oxp_joybutton_text[i]);
+}
+
+static ssize_t button_mapping_options_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t count = 0;
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(oxp_joybutton_text); i++)
+ count += sysfs_emit_at(buf, count, "%s ", oxp_joybutton_text[i]);
+
+ if (count)
+ buf[count - 1] = '\n';
+
+ return count;
+}
+static DEVICE_ATTR_RO(button_mapping_options);
+
+#define OXP_DEVICE_ATTR_RW(_name, _group) \
+ static ssize_t _name##_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+ { \
+ return _group##_store(dev, attr, buf, count, _name.index); \
+ } \
+ static ssize_t _name##_show(struct device *dev, \
+ struct device_attribute *attr, char *buf) \
+ { \
+ return _group##_show(dev, attr, buf, _name.index); \
+ } \
+ static DEVICE_ATTR_RW(_name)
+
+static struct oxp_button_attr button_a = { BUTTON_A };
+OXP_DEVICE_ATTR_RW(button_a, map_button);
+
+static struct oxp_button_attr button_b = { BUTTON_B };
+OXP_DEVICE_ATTR_RW(button_b, map_button);
+
+static struct oxp_button_attr button_x = { BUTTON_X };
+OXP_DEVICE_ATTR_RW(button_x, map_button);
+
+static struct oxp_button_attr button_y = { BUTTON_Y };
+OXP_DEVICE_ATTR_RW(button_y, map_button);
+
+static struct oxp_button_attr button_lb = { BUTTON_LB };
+OXP_DEVICE_ATTR_RW(button_lb, map_button);
+
+static struct oxp_button_attr button_rb = { BUTTON_RB };
+OXP_DEVICE_ATTR_RW(button_rb, map_button);
+
+static struct oxp_button_attr button_lt = { BUTTON_LT };
+OXP_DEVICE_ATTR_RW(button_lt, map_button);
+
+static struct oxp_button_attr button_rt = { BUTTON_RT };
+OXP_DEVICE_ATTR_RW(button_rt, map_button);
+
+static struct oxp_button_attr button_start = { BUTTON_START };
+OXP_DEVICE_ATTR_RW(button_start, map_button);
+
+static struct oxp_button_attr button_select = { BUTTON_SELECT };
+OXP_DEVICE_ATTR_RW(button_select, map_button);
+
+static struct oxp_button_attr button_l3 = { BUTTON_L3 };
+OXP_DEVICE_ATTR_RW(button_l3, map_button);
+
+static struct oxp_button_attr button_r3 = { BUTTON_R3 };
+OXP_DEVICE_ATTR_RW(button_r3, map_button);
+
+static struct oxp_button_attr button_d_up = { BUTTON_DUP };
+OXP_DEVICE_ATTR_RW(button_d_up, map_button);
+
+static struct oxp_button_attr button_d_down = { BUTTON_DDOWN };
+OXP_DEVICE_ATTR_RW(button_d_down, map_button);
+
+static struct oxp_button_attr button_d_left = { BUTTON_DLEFT };
+OXP_DEVICE_ATTR_RW(button_d_left, map_button);
+
+static struct oxp_button_attr button_d_right = { BUTTON_DRIGHT };
+OXP_DEVICE_ATTR_RW(button_d_right, map_button);
+
+static struct oxp_button_attr button_m1 = { BUTTON_M1 };
+OXP_DEVICE_ATTR_RW(button_m1, map_button);
+
+static struct oxp_button_attr button_m2 = { BUTTON_M2 };
+OXP_DEVICE_ATTR_RW(button_m2, map_button);
+
static struct attribute *oxp_cfg_attrs[] = {
+ &dev_attr_button_a.attr,
+ &dev_attr_button_b.attr,
+ &dev_attr_button_d_down.attr,
+ &dev_attr_button_d_left.attr,
+ &dev_attr_button_d_right.attr,
+ &dev_attr_button_d_up.attr,
+ &dev_attr_button_l3.attr,
+ &dev_attr_button_lb.attr,
+ &dev_attr_button_lt.attr,
+ &dev_attr_button_m1.attr,
+ &dev_attr_button_m2.attr,
+ &dev_attr_button_mapping_options.attr,
+ &dev_attr_button_r3.attr,
+ &dev_attr_button_rb.attr,
+ &dev_attr_button_rt.attr,
+ &dev_attr_button_select.attr,
+ &dev_attr_button_start.attr,
&dev_attr_button_takeover.attr,
&dev_attr_button_takeover_index.attr,
+ &dev_attr_button_x.attr,
+ &dev_attr_button_y.attr,
+ &dev_attr_reset_buttons.attr,
NULL,
};
@@ -729,6 +1220,8 @@ static struct led_classdev_mc oxp_cdev_rgb = {
static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
{
+ struct oxp_bmap_page_1 *bmap_1;
+ struct oxp_bmap_page_2 *bmap_2;
int ret;
hid_set_drvdata(hdev, &drvdata);
@@ -756,6 +1249,23 @@ static int oxp_cfg_probe(struct hid_device *hdev, u16 up)
if (up != GEN2_USAGE_PAGE)
return 0;
+ bmap_1 = devm_kzalloc(&hdev->dev, sizeof(struct oxp_bmap_page_1), GFP_KERNEL);
+ if (!bmap_1)
+ return dev_err_probe(&hdev->dev, -ENOMEM,
+ "Unable to allocate button map page 1\n");
+
+ bmap_2 = devm_kzalloc(&hdev->dev, sizeof(struct oxp_bmap_page_2), GFP_KERNEL);
+ if (!bmap_2)
+ return dev_err_probe(&hdev->dev, -ENOMEM,
+ "Unable to allocate button map page 2\n");
+
+ drvdata.bmap_1 = bmap_1;
+ drvdata.bmap_2 = bmap_2;
+ ret = oxp_reset_buttons();
+ if (ret)
+ return dev_err_probe(&hdev->dev, ret,
+ "Failed to reset button mapping\n");
+
ret = devm_device_add_group(&hdev->dev, &oxp_cfg_attrs_group);
if (ret)
return dev_err_probe(&hdev->dev, ret,
--
2.53.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox