* [PATCH v9 04/11] HID: asus: fortify keyboard handshake
From: Antheas Kapenekakis @ 2025-11-20 9:46 UTC (permalink / raw)
To: platform-driver-x86, linux-input
Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Corentin Chary,
Luke D . Jones, Hans de Goede, Ilpo Järvinen, Denis Benato,
Antheas Kapenekakis
In-Reply-To: <20251120094617.11672-1-lkml@antheas.dev>
Handshaking with an Asus device involves sending it a feature report
with the string "ASUS Tech.Inc." and then reading it back to verify the
handshake was successful, under the feature ID the interaction will
take place.
Currently, the driver only does the first part. Add the readback to
verify the handshake was successful. As this could cause breakages,
allow the verification to fail with a dmesg error until we verify
all devices work with it (they seem to).
Since the response is more than 16 bytes, increase the buffer size
to 64 as well to avoid overflow errors.
Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
---
drivers/hid/hid-asus.c | 32 +++++++++++++++++++++++++++++---
1 file changed, 29 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 6de402d215d0..5149dc7edfc5 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -48,7 +48,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define FEATURE_REPORT_ID 0x0d
#define INPUT_REPORT_ID 0x5d
#define FEATURE_KBD_REPORT_ID 0x5a
-#define FEATURE_KBD_REPORT_SIZE 16
+#define FEATURE_KBD_REPORT_SIZE 64
#define FEATURE_KBD_LED_REPORT_ID1 0x5d
#define FEATURE_KBD_LED_REPORT_ID2 0x5e
@@ -394,14 +394,40 @@ static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t bu
static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
{
+ /*
+ * The handshake is first sent as a set_report, then retrieved
+ * from a get_report. They should be equal.
+ */
const u8 buf[] = { report_id, 0x41, 0x53, 0x55, 0x53, 0x20, 0x54,
0x65, 0x63, 0x68, 0x2e, 0x49, 0x6e, 0x63, 0x2e, 0x00 };
+ u8 *readbuf;
int ret;
ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
- if (ret < 0)
- hid_err(hdev, "Asus failed to send init command: %d\n", ret);
+ if (ret < 0) {
+ hid_err(hdev, "Asus failed to send handshake: %d\n", ret);
+ return ret;
+ }
+
+ readbuf = kzalloc(FEATURE_KBD_REPORT_SIZE, GFP_KERNEL);
+ if (!readbuf)
+ return -ENOMEM;
+
+ ret = hid_hw_raw_request(hdev, report_id, readbuf,
+ FEATURE_KBD_REPORT_SIZE, HID_FEATURE_REPORT,
+ HID_REQ_GET_REPORT);
+ if (ret < 0) {
+ hid_err(hdev, "Asus failed to receive handshake ack: %d\n", ret);
+ } else if (memcmp(readbuf, buf, sizeof(buf)) != 0) {
+ hid_warn(hdev, "Asus handshake returned invalid response: %*ph\n",
+ FEATURE_KBD_REPORT_SIZE, readbuf);
+ /*
+ * Do not return error if handshake is wrong until this is
+ * verified to work for all devices.
+ */
+ }
+ kfree(readbuf);
return ret;
}
--
2.52.0
^ permalink raw reply related
* [PATCH v9 00/11] HID: asus: Fix ASUS ROG Laptop's Keyboard backlight handling
From: Antheas Kapenekakis @ 2025-11-20 9:46 UTC (permalink / raw)
To: platform-driver-x86, linux-input
Cc: linux-kernel, Jiri Kosina, Benjamin Tissoires, Corentin Chary,
Luke D . Jones, Hans de Goede, Ilpo Järvinen, Denis Benato,
Antheas Kapenekakis
This is a two part series which does the following:
- Clean-up init sequence
- Unify backlight handling to happen under asus-wmi so that all Aura
devices have synced brightness controls and the backlight button works
properly when it is on a USB laptop keyboard instead of one w/ WMI.
For more context, see cover letter of V1. Since V5, I removed some patches
to make this easier to merge.
---
V8: https://lore.kernel.org/all/20251101104712.8011-1-lkml@antheas.dev/
V7: https://lore.kernel.org/all/20251018101759.4089-1-lkml@antheas.dev/
V6: https://lore.kernel.org/all/20251013201535.6737-1-lkml@antheas.dev/
V5: https://lore.kernel.org/all/20250325184601.10990-1-lkml@antheas.dev/
V4: https://lore.kernel.org/lkml/20250324210151.6042-1-lkml@antheas.dev/
V3: https://lore.kernel.org/lkml/20250322102804.418000-1-lkml@antheas.dev/
V2: https://lore.kernel.org/all/20250320220924.5023-1-lkml@antheas.dev/
V1: https://lore.kernel.org/all/20250319191320.10092-1-lkml@antheas.dev/
Changes since V8:
- No functional changes
- Move legacy init patch to second, modify first patch so that their
diff is minimized
- Split "prevent binding to all HID devices on ROG" into two patches:
- moving backlight initialization into probe
- early exit to skip ->init check and rename
- Remove skipping vendor fixups for non-vendor devices. It is not possible
to read usages before the report fixups are applied, so it did not work
- In that patch, reword a comment to be single line and make is_vendor a bool
- Dismiss Luke's tags from "Add support for multiple kbd led handlers" as it
has drifted too far since he reviewed/tested it.
Changes since V7:
- Readd legacy init quirk for Dennis
- Remove HID_QUIRK_INPUT_PER_APP as a courtesy to asusctl
- Fix warning due to enum_backlight receiving negative values
Changes since V6:
- Split initialization refactor into three patches, update commit text
to be clearer in what it does
- Replace spinlock accesses with guard and scoped guard in all patches
- Add missing includes mentioned by Ilpo
- Reflow, tweak comment in prevent binding to all HID devices on ROG
- Replace asus_ref.asus with local reference in all patches
- Add missing kernel doc comments
- Other minor nits from Ilpo
- User reported warning due to scheduling work while holding a spinlock.
Restructure patch for multiple handlers to limit when spinlock is held to
variable access only. In parallel, setup a workqueue to handle registration
of led device and setting brightness. This is required as registering the
led device triggers kbd_led_get which needs to hold the spinlock to
protect the led_wk value. The workqueue is also required for the hid
event passthrough to avoid scheduling work while holding the spinlock.
Apply the workqueue to wmi brightness buttons as well, as that was
omitted before this series and WMI access was performed.
- On "HID: asus: prevent binding to all HID devices on ROG", rename
quirk HANDLE_GENERIC to SKIP_REPORT_FIXUP and only skip report fixup.
This allows other quirks to apply (applies quirk that fixes keyboard
being named as a pointer device).
Changes since V5:
- It's been a long time
- Remove addition of RGB as that had some comments I need to work on
- Remove folio patch (already merged)
- Remove legacy fix patch 11 from V4. There is a small chance that
without this patch, some old NKEY keyboards might not respond to
RGB commands according to Luke, but the kernel driver does not do
RGB currently. The 0x5d init is done by Armoury crate software in
Windows. If an issue is found, we can re-add it or just remove patches
1/2 before merging. However, init could use the cleanup.
Changes since V4:
- Fix KConfig (reported by kernel robot)
- Fix Ilpo's nits, if I missed anything lmk
Changes since V3:
- Add initializer for 0x5d for old NKEY keyboards until it is verified
that it is not needed for their media keys to function.
- Cover init in asus-wmi with spinlock as per Hans
- If asus-wmi registers WMI handler with brightness, init the brightness
in USB Asus keyboards, per Hans.
- Change hid handler name to asus-UNIQ:rgb:peripheral to match led class
- Fix oops when unregistering asus-wmi by moving unregister outside of
the spin lock (but after the asus reference is set to null)
Changes since V2:
- Check lazy init succeds in asus-wmi before setting register variable
- make explicit check in asus_hid_register_listener for listener existing
to avoid re-init
- rename asus_brt to asus_hid in most places and harmonize everything
- switch to a spinlock instead of a mutex to avoid kernel ooops
- fixup hid device quirks to avoid multiple RGB devices while still exposing
all input vendor devices. This includes moving rgb init to probe
instead of the input_configured callbacks.
- Remove fan key (during retest it appears to be 0xae that is already
supported by hid-asus)
- Never unregister asus::kbd_backlight while asus-wmi is active, as that
- removes fds from userspace and breaks backlight functionality. All
- current mainline drivers do not support backlight hotplugging, so most
userspace software (e.g., KDE, UPower) is built with that assumption.
For the Ally, since it disconnects its controller during sleep, this
caused the backlight slider to not work in KDE.
Changes since V1:
- Add basic RGB support on hid-asus, (Z13/Ally) tested in KDE/Z13
- Fix ifdef else having an invalid signature (reported by kernel robot)
- Restore input arguments to init and keyboard function so they can
be re-used for RGB controls.
- Remove Z13 delay (it did not work to fix the touchpad) and replace it
with a HID_GROUP_GENERIC quirk to allow hid-multitouch to load. Squash
keyboard rename into it.
- Unregister brightness listener before removing work queue to avoid
a race condition causing corruption
- Remove spurious mutex unlock in asus_brt_event
- Place mutex lock in kbd_led_set after LED_UNREGISTERING check to avoid
relocking the mutex and causing a deadlock when unregistering leds
- Add extra check during unregistering to avoid calling unregister when
no led device is registered.
- Temporarily HID_QUIRK_INPUT_PER_APP from the ROG endpoint as it causes
the driver to create 4 RGB handlers per device. I also suspect some
extra events sneak through (KDE had the @@@@@@).
Antheas Kapenekakis (11):
HID: asus: simplify RGB init sequence
HID: asus: initialize additional endpoints only for legacy devices
HID: asus: use same report_id in response
HID: asus: fortify keyboard handshake
HID: asus: move vendor initialization to probe
HID: asus: early return for ROG devices
platform/x86: asus-wmi: Add support for multiple kbd led handlers
HID: asus: listen to the asus-wmi brightness device instead of
creating one
platform/x86: asus-wmi: remove unused keyboard backlight quirk
platform/x86: asus-wmi: add keyboard brightness event handler
HID: asus: add support for the asus-wmi brightness handler
drivers/hid/hid-asus.c | 206 ++++++++++----------
drivers/platform/x86/asus-wmi.c | 214 ++++++++++++++++++---
include/linux/platform_data/x86/asus-wmi.h | 70 +++----
3 files changed, 318 insertions(+), 172 deletions(-)
base-commit: 6a23ae0a96a600d1d12557add110e0bb6e32730c
--
2.52.0
^ permalink raw reply
* Re: [PATCH v3 1/1] HID: input: Add support for multiple batteries per device
From: kernel test robot @ 2025-11-20 8:40 UTC (permalink / raw)
To: Lucas Zampieri, linux-input
Cc: llvm, oe-kbuild-all, Lucas Zampieri, linux-kernel, Jiri Kosina,
Benjamin Tissoires, Sebastian Reichel, Bastien Nocera, linux-pm
In-Reply-To: <20251119143005.1513531-2-lzampier@redhat.com>
Hi Lucas,
kernel test robot noticed the following build errors:
[auto build test ERROR on hid/for-next]
[also build test ERROR on dtor-input/for-linus linus/master v6.18-rc6 next-20251119]
[cannot apply to dtor-input/next]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]
url: https://github.com/intel-lab-lkp/linux/commits/Lucas-Zampieri/HID-input-Add-support-for-multiple-batteries-per-device/20251119-223834
base: https://git.kernel.org/pub/scm/linux/kernel/git/hid/hid.git for-next
patch link: https://lore.kernel.org/r/20251119143005.1513531-2-lzampier%40redhat.com
patch subject: [PATCH v3 1/1] HID: input: Add support for multiple batteries per device
config: um-defconfig (https://download.01.org/0day-ci/archive/20251120/202511201651.tkKTEKpn-lkp@intel.com/config)
compiler: clang version 22.0.0git (https://github.com/llvm/llvm-project 9e9fe08b16ea2c4d9867fb4974edf2a3776d6ece)
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20251120/202511201651.tkKTEKpn-lkp@intel.com/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202511201651.tkKTEKpn-lkp@intel.com/
All errors (new ones prefixed by >>):
>> drivers/hid/hid-input.c:1550:4: error: call to undeclared function 'hidinput_update_battery'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
1550 | hidinput_update_battery(bat, usage->hid, value);
| ^
1 error generated.
vim +/hidinput_update_battery +1550 drivers/hid/hid-input.c
1536
1537 void hidinput_hid_event(struct hid_device *hid, struct hid_field *field, struct hid_usage *usage, __s32 value)
1538 {
1539 struct input_dev *input;
1540 struct hid_report *report = field->report;
1541 unsigned *quirks = &hid->quirks;
1542
1543 if (!usage->type)
1544 return;
1545
1546 if (usage->type == EV_PWR) {
1547 struct hid_battery *bat = hidinput_find_battery(hid, report->id);
1548
1549 if (bat)
> 1550 hidinput_update_battery(bat, usage->hid, value);
1551 return;
1552 }
1553
1554 if (!field->hidinput)
1555 return;
1556
1557 input = field->hidinput->input;
1558
1559 if (usage->hat_min < usage->hat_max || usage->hat_dir) {
1560 int hat_dir = usage->hat_dir;
1561 if (!hat_dir)
1562 hat_dir = (value - usage->hat_min) * 8 / (usage->hat_max - usage->hat_min + 1) + 1;
1563 if (hat_dir < 0 || hat_dir > 8) hat_dir = 0;
1564 input_event(input, usage->type, usage->code , hid_hat_to_axis[hat_dir].x);
1565 input_event(input, usage->type, usage->code + 1, hid_hat_to_axis[hat_dir].y);
1566 return;
1567 }
1568
1569 /*
1570 * Ignore out-of-range values as per HID specification,
1571 * section 5.10 and 6.2.25, when NULL state bit is present.
1572 * When it's not, clamp the value to match Microsoft's input
1573 * driver as mentioned in "Required HID usages for digitizers":
1574 * https://msdn.microsoft.com/en-us/library/windows/hardware/dn672278(v=vs.85).asp
1575 *
1576 * The logical_minimum < logical_maximum check is done so that we
1577 * don't unintentionally discard values sent by devices which
1578 * don't specify logical min and max.
1579 */
1580 if ((field->flags & HID_MAIN_ITEM_VARIABLE) &&
1581 field->logical_minimum < field->logical_maximum) {
1582 if (field->flags & HID_MAIN_ITEM_NULL_STATE &&
1583 (value < field->logical_minimum ||
1584 value > field->logical_maximum)) {
1585 dbg_hid("Ignoring out-of-range value %x\n", value);
1586 return;
1587 }
1588 value = clamp(value,
1589 field->logical_minimum,
1590 field->logical_maximum);
1591 }
1592
1593 switch (usage->hid) {
1594 case HID_DG_ERASER:
1595 report->tool_active |= !!value;
1596
1597 /*
1598 * if eraser is set, we must enforce BTN_TOOL_RUBBER
1599 * to accommodate for devices not following the spec.
1600 */
1601 if (value)
1602 hid_report_set_tool(report, input, BTN_TOOL_RUBBER);
1603 else if (report->tool != BTN_TOOL_RUBBER)
1604 /* value is off, tool is not rubber, ignore */
1605 return;
1606 else if (*quirks & HID_QUIRK_NOINVERT &&
1607 !test_bit(BTN_TOUCH, input->key)) {
1608 /*
1609 * There is no invert to release the tool, let hid_input
1610 * send BTN_TOUCH with scancode and release the tool after.
1611 */
1612 hid_report_release_tool(report, input, BTN_TOOL_RUBBER);
1613 return;
1614 }
1615
1616 /* let hid-input set BTN_TOUCH */
1617 break;
1618
1619 case HID_DG_INVERT:
1620 report->tool_active |= !!value;
1621
1622 /*
1623 * If invert is set, we store BTN_TOOL_RUBBER.
1624 */
1625 if (value)
1626 hid_report_set_tool(report, input, BTN_TOOL_RUBBER);
1627 else if (!report->tool_active)
1628 /* tool_active not set means Invert and Eraser are not set */
1629 hid_report_release_tool(report, input, BTN_TOOL_RUBBER);
1630
1631 /* no further processing */
1632 return;
1633
1634 case HID_DG_INRANGE:
1635 report->tool_active |= !!value;
1636
1637 if (report->tool_active) {
1638 /*
1639 * if tool is not set but is marked as active,
1640 * assume ours
1641 */
1642 if (!report->tool)
1643 report->tool = usage->code;
1644
1645 /* drivers may have changed the value behind our back, resend it */
1646 hid_report_set_tool(report, input, report->tool);
1647 } else {
1648 hid_report_release_tool(report, input, usage->code);
1649 }
1650
1651 /* reset tool_active for the next event */
1652 report->tool_active = false;
1653
1654 /* no further processing */
1655 return;
1656
1657 case HID_DG_TIPSWITCH:
1658 report->tool_active |= !!value;
1659
1660 /* if tool is set to RUBBER we should ignore the current value */
1661 if (report->tool == BTN_TOOL_RUBBER)
1662 return;
1663
1664 break;
1665
1666 case HID_DG_TIPPRESSURE:
1667 if (*quirks & HID_QUIRK_NOTOUCH) {
1668 int a = field->logical_minimum;
1669 int b = field->logical_maximum;
1670
1671 if (value > a + ((b - a) >> 3)) {
1672 input_event(input, EV_KEY, BTN_TOUCH, 1);
1673 report->tool_active = true;
1674 }
1675 }
1676 break;
1677
1678 case HID_UP_PID | 0x83UL: /* Simultaneous Effects Max */
1679 dbg_hid("Maximum Effects - %d\n",value);
1680 return;
1681
1682 case HID_UP_PID | 0x7fUL:
1683 dbg_hid("PID Pool Report\n");
1684 return;
1685 }
1686
1687 switch (usage->type) {
1688 case EV_KEY:
1689 if (usage->code == 0) /* Key 0 is "unassigned", not KEY_UNKNOWN */
1690 return;
1691 break;
1692
1693 case EV_REL:
1694 if (usage->code == REL_WHEEL_HI_RES ||
1695 usage->code == REL_HWHEEL_HI_RES) {
1696 hidinput_handle_scroll(usage, input, value);
1697 return;
1698 }
1699 break;
1700
1701 case EV_ABS:
1702 if ((field->flags & HID_MAIN_ITEM_RELATIVE) &&
1703 usage->code == ABS_VOLUME) {
1704 int count = abs(value);
1705 int direction = value > 0 ? KEY_VOLUMEUP : KEY_VOLUMEDOWN;
1706 int i;
1707
1708 for (i = 0; i < count; i++) {
1709 input_event(input, EV_KEY, direction, 1);
1710 input_sync(input);
1711 input_event(input, EV_KEY, direction, 0);
1712 input_sync(input);
1713 }
1714 return;
1715
1716 } else if (((*quirks & HID_QUIRK_X_INVERT) && usage->code == ABS_X) ||
1717 ((*quirks & HID_QUIRK_Y_INVERT) && usage->code == ABS_Y))
1718 value = field->logical_maximum - value;
1719 break;
1720 }
1721
1722 /*
1723 * Ignore reports for absolute data if the data didn't change. This is
1724 * not only an optimization but also fixes 'dead' key reports. Some
1725 * RollOver implementations for localized keys (like BACKSLASH/PIPE; HID
1726 * 0x31 and 0x32) report multiple keys, even though a localized keyboard
1727 * can only have one of them physically available. The 'dead' keys
1728 * report constant 0. As all map to the same keycode, they'd confuse
1729 * the input layer. If we filter the 'dead' keys on the HID level, we
1730 * skip the keycode translation and only forward real events.
1731 */
1732 if (!(field->flags & (HID_MAIN_ITEM_RELATIVE |
1733 HID_MAIN_ITEM_BUFFERED_BYTE)) &&
1734 (field->flags & HID_MAIN_ITEM_VARIABLE) &&
1735 usage->usage_index < field->maxusage &&
1736 value == field->value[usage->usage_index])
1737 return;
1738
1739 /* report the usage code as scancode if the key status has changed */
1740 if (usage->type == EV_KEY &&
1741 (!test_bit(usage->code, input->key)) == value)
1742 input_event(input, EV_MSC, MSC_SCAN, usage->hid);
1743
1744 input_event(input, usage->type, usage->code, value);
1745
1746 if ((field->flags & HID_MAIN_ITEM_RELATIVE) &&
1747 usage->type == EV_KEY && value) {
1748 input_sync(input);
1749 input_event(input, usage->type, usage->code, 0);
1750 }
1751 }
1752
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* Re: [PATCH v3 1/1] HID: input: Add support for multiple batteries per device
From: kernel test robot @ 2025-11-20 8:17 UTC (permalink / raw)
To: Lucas Zampieri, linux-input
Cc: oe-kbuild-all, Lucas Zampieri, linux-kernel, Jiri Kosina,
Benjamin Tissoires, Sebastian Reichel, Bastien Nocera, linux-pm
In-Reply-To: <20251119143005.1513531-2-lzampier@redhat.com>
Hi Lucas,
kernel test robot noticed the following build errors:
[auto build test ERROR on hid/for-next]
[also build test ERROR on dtor-input/for-linus linus/master v6.18-rc6 next-20251119]
[cannot apply to dtor-input/next]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]
url: https://github.com/intel-lab-lkp/linux/commits/Lucas-Zampieri/HID-input-Add-support-for-multiple-batteries-per-device/20251119-223834
base: https://git.kernel.org/pub/scm/linux/kernel/git/hid/hid.git for-next
patch link: https://lore.kernel.org/r/20251119143005.1513531-2-lzampier%40redhat.com
patch subject: [PATCH v3 1/1] HID: input: Add support for multiple batteries per device
config: sh-defconfig (https://download.01.org/0day-ci/archive/20251120/202511201624.yUv4VtBv-lkp@intel.com/config)
compiler: sh4-linux-gcc (GCC) 15.1.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20251120/202511201624.yUv4VtBv-lkp@intel.com/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202511201624.yUv4VtBv-lkp@intel.com/
All errors (new ones prefixed by >>):
drivers/hid/hid-input.c: In function 'hidinput_hid_event':
>> drivers/hid/hid-input.c:1550:25: error: implicit declaration of function 'hidinput_update_battery'; did you mean 'hidinput_find_battery'? [-Wimplicit-function-declaration]
1550 | hidinput_update_battery(bat, usage->hid, value);
| ^~~~~~~~~~~~~~~~~~~~~~~
| hidinput_find_battery
vim +1550 drivers/hid/hid-input.c
1536
1537 void hidinput_hid_event(struct hid_device *hid, struct hid_field *field, struct hid_usage *usage, __s32 value)
1538 {
1539 struct input_dev *input;
1540 struct hid_report *report = field->report;
1541 unsigned *quirks = &hid->quirks;
1542
1543 if (!usage->type)
1544 return;
1545
1546 if (usage->type == EV_PWR) {
1547 struct hid_battery *bat = hidinput_find_battery(hid, report->id);
1548
1549 if (bat)
> 1550 hidinput_update_battery(bat, usage->hid, value);
1551 return;
1552 }
1553
1554 if (!field->hidinput)
1555 return;
1556
1557 input = field->hidinput->input;
1558
1559 if (usage->hat_min < usage->hat_max || usage->hat_dir) {
1560 int hat_dir = usage->hat_dir;
1561 if (!hat_dir)
1562 hat_dir = (value - usage->hat_min) * 8 / (usage->hat_max - usage->hat_min + 1) + 1;
1563 if (hat_dir < 0 || hat_dir > 8) hat_dir = 0;
1564 input_event(input, usage->type, usage->code , hid_hat_to_axis[hat_dir].x);
1565 input_event(input, usage->type, usage->code + 1, hid_hat_to_axis[hat_dir].y);
1566 return;
1567 }
1568
1569 /*
1570 * Ignore out-of-range values as per HID specification,
1571 * section 5.10 and 6.2.25, when NULL state bit is present.
1572 * When it's not, clamp the value to match Microsoft's input
1573 * driver as mentioned in "Required HID usages for digitizers":
1574 * https://msdn.microsoft.com/en-us/library/windows/hardware/dn672278(v=vs.85).asp
1575 *
1576 * The logical_minimum < logical_maximum check is done so that we
1577 * don't unintentionally discard values sent by devices which
1578 * don't specify logical min and max.
1579 */
1580 if ((field->flags & HID_MAIN_ITEM_VARIABLE) &&
1581 field->logical_minimum < field->logical_maximum) {
1582 if (field->flags & HID_MAIN_ITEM_NULL_STATE &&
1583 (value < field->logical_minimum ||
1584 value > field->logical_maximum)) {
1585 dbg_hid("Ignoring out-of-range value %x\n", value);
1586 return;
1587 }
1588 value = clamp(value,
1589 field->logical_minimum,
1590 field->logical_maximum);
1591 }
1592
1593 switch (usage->hid) {
1594 case HID_DG_ERASER:
1595 report->tool_active |= !!value;
1596
1597 /*
1598 * if eraser is set, we must enforce BTN_TOOL_RUBBER
1599 * to accommodate for devices not following the spec.
1600 */
1601 if (value)
1602 hid_report_set_tool(report, input, BTN_TOOL_RUBBER);
1603 else if (report->tool != BTN_TOOL_RUBBER)
1604 /* value is off, tool is not rubber, ignore */
1605 return;
1606 else if (*quirks & HID_QUIRK_NOINVERT &&
1607 !test_bit(BTN_TOUCH, input->key)) {
1608 /*
1609 * There is no invert to release the tool, let hid_input
1610 * send BTN_TOUCH with scancode and release the tool after.
1611 */
1612 hid_report_release_tool(report, input, BTN_TOOL_RUBBER);
1613 return;
1614 }
1615
1616 /* let hid-input set BTN_TOUCH */
1617 break;
1618
1619 case HID_DG_INVERT:
1620 report->tool_active |= !!value;
1621
1622 /*
1623 * If invert is set, we store BTN_TOOL_RUBBER.
1624 */
1625 if (value)
1626 hid_report_set_tool(report, input, BTN_TOOL_RUBBER);
1627 else if (!report->tool_active)
1628 /* tool_active not set means Invert and Eraser are not set */
1629 hid_report_release_tool(report, input, BTN_TOOL_RUBBER);
1630
1631 /* no further processing */
1632 return;
1633
1634 case HID_DG_INRANGE:
1635 report->tool_active |= !!value;
1636
1637 if (report->tool_active) {
1638 /*
1639 * if tool is not set but is marked as active,
1640 * assume ours
1641 */
1642 if (!report->tool)
1643 report->tool = usage->code;
1644
1645 /* drivers may have changed the value behind our back, resend it */
1646 hid_report_set_tool(report, input, report->tool);
1647 } else {
1648 hid_report_release_tool(report, input, usage->code);
1649 }
1650
1651 /* reset tool_active for the next event */
1652 report->tool_active = false;
1653
1654 /* no further processing */
1655 return;
1656
1657 case HID_DG_TIPSWITCH:
1658 report->tool_active |= !!value;
1659
1660 /* if tool is set to RUBBER we should ignore the current value */
1661 if (report->tool == BTN_TOOL_RUBBER)
1662 return;
1663
1664 break;
1665
1666 case HID_DG_TIPPRESSURE:
1667 if (*quirks & HID_QUIRK_NOTOUCH) {
1668 int a = field->logical_minimum;
1669 int b = field->logical_maximum;
1670
1671 if (value > a + ((b - a) >> 3)) {
1672 input_event(input, EV_KEY, BTN_TOUCH, 1);
1673 report->tool_active = true;
1674 }
1675 }
1676 break;
1677
1678 case HID_UP_PID | 0x83UL: /* Simultaneous Effects Max */
1679 dbg_hid("Maximum Effects - %d\n",value);
1680 return;
1681
1682 case HID_UP_PID | 0x7fUL:
1683 dbg_hid("PID Pool Report\n");
1684 return;
1685 }
1686
1687 switch (usage->type) {
1688 case EV_KEY:
1689 if (usage->code == 0) /* Key 0 is "unassigned", not KEY_UNKNOWN */
1690 return;
1691 break;
1692
1693 case EV_REL:
1694 if (usage->code == REL_WHEEL_HI_RES ||
1695 usage->code == REL_HWHEEL_HI_RES) {
1696 hidinput_handle_scroll(usage, input, value);
1697 return;
1698 }
1699 break;
1700
1701 case EV_ABS:
1702 if ((field->flags & HID_MAIN_ITEM_RELATIVE) &&
1703 usage->code == ABS_VOLUME) {
1704 int count = abs(value);
1705 int direction = value > 0 ? KEY_VOLUMEUP : KEY_VOLUMEDOWN;
1706 int i;
1707
1708 for (i = 0; i < count; i++) {
1709 input_event(input, EV_KEY, direction, 1);
1710 input_sync(input);
1711 input_event(input, EV_KEY, direction, 0);
1712 input_sync(input);
1713 }
1714 return;
1715
1716 } else if (((*quirks & HID_QUIRK_X_INVERT) && usage->code == ABS_X) ||
1717 ((*quirks & HID_QUIRK_Y_INVERT) && usage->code == ABS_Y))
1718 value = field->logical_maximum - value;
1719 break;
1720 }
1721
1722 /*
1723 * Ignore reports for absolute data if the data didn't change. This is
1724 * not only an optimization but also fixes 'dead' key reports. Some
1725 * RollOver implementations for localized keys (like BACKSLASH/PIPE; HID
1726 * 0x31 and 0x32) report multiple keys, even though a localized keyboard
1727 * can only have one of them physically available. The 'dead' keys
1728 * report constant 0. As all map to the same keycode, they'd confuse
1729 * the input layer. If we filter the 'dead' keys on the HID level, we
1730 * skip the keycode translation and only forward real events.
1731 */
1732 if (!(field->flags & (HID_MAIN_ITEM_RELATIVE |
1733 HID_MAIN_ITEM_BUFFERED_BYTE)) &&
1734 (field->flags & HID_MAIN_ITEM_VARIABLE) &&
1735 usage->usage_index < field->maxusage &&
1736 value == field->value[usage->usage_index])
1737 return;
1738
1739 /* report the usage code as scancode if the key status has changed */
1740 if (usage->type == EV_KEY &&
1741 (!test_bit(usage->code, input->key)) == value)
1742 input_event(input, EV_MSC, MSC_SCAN, usage->hid);
1743
1744 input_event(input, usage->type, usage->code, value);
1745
1746 if ((field->flags & HID_MAIN_ITEM_RELATIVE) &&
1747 usage->type == EV_KEY && value) {
1748 input_sync(input);
1749 input_event(input, usage->type, usage->code, 0);
1750 }
1751 }
1752
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* [PATCH] HID: switch2: Add preliminary Switch 2 controller driver
From: Vicki Pfau @ 2025-11-20 2:24 UTC (permalink / raw)
To: Dmitry Torokhov, Jiri Kosina, Benjamin Tissoires, linux-input
Cc: Luiz Augusto von Dentz, Bastien Nocera, Vicki Pfau
This adds a new driver for the Switch 2 controllers. The Switch 2 uses an
unusual split-interface design such that input and rumble occur on the main
HID interface, but all other communication occurs over a "configuration"
interface. This is the case on both USB and Bluetooth, so this new driver
uses a split-driver design with the HID interface being the "main" driver
and the configuration interface is a secondary driver that looks up to the
configuration interface, sharing resources on a shared struct.
Due to using a non-standard pairing interface as well as Bluetooth
communications being extremely limited in the kernel, a custom interface
between userspace and the kernel will need to be design, along with bringup
in BlueZ. That is beyond the scope of this initial patch, which only
contains the generic HID and USB configuration interface drivers.
This initial work supports general input for the Joy-Con 2, Pro Controller
2, and GameCube NSO controllers, as well as basic rumble support. IMU
support is not yet present.
Signed-off-by: Vicki Pfau <vi@endrift.com>
---
MAINTAINERS | 7 +
drivers/hid/Kconfig | 18 +
drivers/hid/Makefile | 1 +
drivers/hid/hid-ids.h | 4 +
drivers/hid/hid-switch2.c | 971 +++++++++++++++++++++++++++
drivers/hid/hid-switch2.h | 242 +++++++
drivers/input/joystick/Kconfig | 8 +
drivers/input/joystick/Makefile | 1 +
drivers/input/joystick/switch2-usb.c | 355 ++++++++++
9 files changed, 1607 insertions(+)
create mode 100644 drivers/hid/hid-switch2.c
create mode 100644 drivers/hid/hid-switch2.h
create mode 100644 drivers/input/joystick/switch2-usb.c
diff --git a/MAINTAINERS b/MAINTAINERS
index e64b94e6b5a97..a5bacbfc86b5a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18290,6 +18290,13 @@ L: linux-input@vger.kernel.org
S: Maintained
F: drivers/hid/hid-nintendo*
+NINTENDO SWITCH 2 CONTROLLER DRIVER
+M: Vicki Pfau <vi@endrift.com>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: drivers/hid/hid-switch2*
+F: drivers/input/joystick/switch2-usb.c
+
NIOS2 ARCHITECTURE
M: Dinh Nguyen <dinguyen@kernel.org>
S: Maintained
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 04420a713be08..7486212ff5d28 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -1141,6 +1141,24 @@ config HID_SUNPLUS
help
Support for Sunplus wireless desktop.
+config HID_SWITCH2
+ tristate "Nintendo Switch 2 controller support"
+ depends on HID
+ select USB_HID
+ help
+ Adds support for the Nintendo Switch 2 Controllers.
+
+ To compile this driver as a module, choose M here: the
+ module will be called hid-switch2.
+
+config SWITCH2_FF
+ bool "Nintendo Switch 2 force feedback support"
+ depends on HID_SWITCH2
+ select INPUT_FF_MEMLESS
+ help
+ Say Y here if you want to enable force feedback support for Nintendo
+ Switch 2 game controllers.
+
config HID_RMI
tristate "Synaptics RMI4 device support"
select RMI4_CORE
diff --git a/drivers/hid/Makefile b/drivers/hid/Makefile
index 361a7daedeb85..74a3f82ed82f1 100644
--- a/drivers/hid/Makefile
+++ b/drivers/hid/Makefile
@@ -131,6 +131,7 @@ obj-$(CONFIG_HID_SPEEDLINK) += hid-speedlink.o
obj-$(CONFIG_HID_STEAM) += hid-steam.o
obj-$(CONFIG_HID_STEELSERIES) += hid-steelseries.o
obj-$(CONFIG_HID_SUNPLUS) += hid-sunplus.o
+obj-$(CONFIG_HID_SWITCH2) += hid-switch2.o
obj-$(CONFIG_HID_GREENASIA) += hid-gaff.o
obj-$(CONFIG_HID_THRUSTMASTER) += hid-tmff.o hid-thrustmaster.o
obj-$(CONFIG_HID_TIVO) += hid-tivo.o
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index c4589075a5ed6..eef152f7b55f7 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -1052,6 +1052,10 @@
#define USB_DEVICE_ID_NINTENDO_SNESCON 0x2017
#define USB_DEVICE_ID_NINTENDO_GENCON 0x201e
#define USB_DEVICE_ID_NINTENDO_N64CON 0x2019
+#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONR 0x2066
+#define USB_DEVICE_ID_NINTENDO_NS2_JOYCONL 0x2067
+#define USB_DEVICE_ID_NINTENDO_NS2_PROCON 0x2069
+#define USB_DEVICE_ID_NINTENDO_NS2_GCCON 0x2073
#define USB_VENDOR_ID_NOVATEK 0x0603
#define USB_DEVICE_ID_NOVATEK_PCT 0x0600
diff --git a/drivers/hid/hid-switch2.c b/drivers/hid/hid-switch2.c
new file mode 100644
index 0000000000000..a06d181e76cc7
--- /dev/null
+++ b/drivers/hid/hid-switch2.c
@@ -0,0 +1,971 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * HID driver for Nintendo Switch controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the following work:
+ * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
+ * https://github.com/ndeadly/switch2_controller_research
+ * hid-nintendo driver
+ */
+
+#include "hid-switch2.h"
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/idr.h>
+#include <linux/jiffies.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/usb.h>
+#include "usbhid/usbhid.h"
+
+#define NS2_BTNR_B BIT(0)
+#define NS2_BTNR_A BIT(1)
+#define NS2_BTNR_Y BIT(2)
+#define NS2_BTNR_X BIT(3)
+#define NS2_BTNR_R BIT(4)
+#define NS2_BTNR_ZR BIT(5)
+#define NS2_BTNR_PLUS BIT(6)
+#define NS2_BTNR_RS BIT(7)
+
+#define NS2_BTNL_DOWN BIT(0)
+#define NS2_BTNL_RIGHT BIT(1)
+#define NS2_BTNL_LEFT BIT(2)
+#define NS2_BTNL_UP BIT(3)
+#define NS2_BTNL_L BIT(4)
+#define NS2_BTNL_ZL BIT(5)
+#define NS2_BTNL_MINUS BIT(6)
+#define NS2_BTNL_LS BIT(7)
+
+#define NS2_BTN3_C BIT(4)
+#define NS2_BTN3_SR BIT(6)
+#define NS2_BTN3_SL BIT(7)
+
+#define NS2_BTN_JCR_HOME BIT(0)
+#define NS2_BTN_JCR_GR BIT(2)
+#define NS2_BTN_JCR_C NS2_BTN3_C
+#define NS2_BTN_JCR_SR NS2_BTN3_SR
+#define NS2_BTN_JCR_SL NS2_BTN3_SL
+
+#define NS2_BTN_JCL_CAPTURE BIT(0)
+#define NS2_BTN_JCL_GL BIT(2)
+#define NS2_BTN_JCL_SR NS2_BTN3_SR
+#define NS2_BTN_JCL_SL NS2_BTN3_SL
+
+#define NS2_BTN_PRO_HOME BIT(0)
+#define NS2_BTN_PRO_CAPTURE BIT(1)
+#define NS2_BTN_PRO_GR BIT(2)
+#define NS2_BTN_PRO_GL BIT(3)
+#define NS2_BTN_PRO_C NS2_BTN3_C
+
+#define NS2_BTN_GC_HOME BIT(0)
+#define NS2_BTN_GC_CAPTURE BIT(1)
+#define NS2_BTN_GC_C NS2_BTN3_C
+
+#define NS2_TRIGGER_RANGE 4095
+#define NS2_AXIS_MIN -32768
+#define NS2_AXIS_MAX 32767
+
+#define NS2_MAX_PLAYER_ID 8
+
+enum gc_rumble {
+ GC_RUMBLE_OFF = 0,
+ GC_RUMBLE_ON = 1,
+ GC_RUMBLE_STOP = 2,
+};
+
+/*
+ * The highest rumble level for "HD Rumble" is strong enough to potentially damage the controller,
+ * and also leaves your hands feeling like melted jelly, so we set a semi-abitrary scaling factor
+ * to artificially limit the maximum for safety and comfort. It is currently unknown if the Switch
+ * 2 itself does something similar, but it's quite likely.
+ *
+ * This value must be between 0 and 1024, otherwise the math below will overflow.
+ */
+#define RUMBLE_MAX 450u
+
+/*
+ * Semi-arbitrary values used to simulate the "rumble" sensation of an eccentric rotating
+ * mass type haptic motor on the Switch 2 controllers' linear resonant actuator type haptics.
+ *
+ * The units used are unknown, but the values must be between 0 and 1023.
+ */
+#define RUMBLE_HI_FREQ 0x187
+#define RUMBLE_LO_FREQ 0x112
+
+static DEFINE_MUTEX(switch2_controllers_lock);
+static LIST_HEAD(switch2_controllers);
+
+struct switch2_ctlr_button_mapping {
+ uint32_t code;
+ int byte;
+ uint32_t bit;
+};
+
+static const struct switch2_ctlr_button_mapping left_joycon_button_mappings[] = {
+ { BTN_TL, 0, NS2_BTNL_L, },
+ { BTN_TL2, 0, NS2_BTNL_ZL, },
+ { BTN_SELECT, 0, NS2_BTNL_MINUS, },
+ { BTN_THUMBL, 0, NS2_BTNL_LS, },
+ { BTN_GRIPL, 1, NS2_BTN_JCL_GL, },
+ { KEY_RECORD, 1, NS2_BTN_JCL_CAPTURE, },
+ { /* sentinel */ },
+};
+
+static const struct switch2_ctlr_button_mapping right_joycon_button_mappings[] = {
+ { BTN_SOUTH, 0, NS2_BTNR_A, },
+ { BTN_EAST, 0, NS2_BTNR_B, },
+ { BTN_NORTH, 0, NS2_BTNR_X, },
+ { BTN_WEST, 0, NS2_BTNR_Y, },
+ { BTN_TR, 0, NS2_BTNR_R, },
+ { BTN_TR2, 0, NS2_BTNR_ZR, },
+ { BTN_START, 0, NS2_BTNR_PLUS, },
+ { BTN_THUMBR, 0, NS2_BTNR_RS, },
+ { BTN_C, 1, NS2_BTN_JCR_C, },
+ { BTN_GRIPR, 1, NS2_BTN_JCR_GR, },
+ { BTN_MODE, 1, NS2_BTN_JCR_HOME, },
+ { /* sentinel */ },
+};
+
+static const struct switch2_ctlr_button_mapping procon_mappings[] = {
+ { BTN_SOUTH, 0, NS2_BTNR_A, },
+ { BTN_EAST, 0, NS2_BTNR_B, },
+ { BTN_NORTH, 0, NS2_BTNR_X, },
+ { BTN_WEST, 0, NS2_BTNR_Y, },
+ { BTN_TL, 1, NS2_BTNL_L, },
+ { BTN_TR, 0, NS2_BTNR_R, },
+ { BTN_TL2, 1, NS2_BTNL_ZL, },
+ { BTN_TR2, 0, NS2_BTNR_ZR, },
+ { BTN_SELECT, 1, NS2_BTNL_MINUS, },
+ { BTN_START, 0, NS2_BTNR_PLUS, },
+ { BTN_THUMBL, 1, NS2_BTNL_LS, },
+ { BTN_THUMBR, 0, NS2_BTNR_RS, },
+ { BTN_MODE, 2, NS2_BTN_PRO_HOME },
+ { KEY_RECORD, 2, NS2_BTN_PRO_CAPTURE },
+ { BTN_GRIPR, 2, NS2_BTN_PRO_GR },
+ { BTN_GRIPL, 2, NS2_BTN_PRO_GL },
+ { BTN_C, 2, NS2_BTN_PRO_C },
+ { /* sentinel */ },
+};
+
+static const struct switch2_ctlr_button_mapping gccon_mappings[] = {
+ { BTN_SOUTH, 0, NS2_BTNR_A, },
+ { BTN_EAST, 0, NS2_BTNR_B, },
+ { BTN_NORTH, 0, NS2_BTNR_X, },
+ { BTN_WEST, 0, NS2_BTNR_Y, },
+ { BTN_TL, 1, NS2_BTNL_L, },
+ { BTN_TR, 0, NS2_BTNR_R, },
+ { BTN_TL2, 1, NS2_BTNL_ZL, },
+ { BTN_TR2, 0, NS2_BTNR_ZR, },
+ { BTN_SELECT, 1, NS2_BTNL_MINUS, },
+ { BTN_START, 0, NS2_BTNR_PLUS, },
+ { BTN_MODE, 2, NS2_BTN_GC_HOME },
+ { KEY_RECORD, 2, NS2_BTN_GC_CAPTURE },
+ { BTN_C, 2, NS2_BTN_GC_C },
+ { /* sentinel */ },
+};
+
+static const uint8_t switch2_init_cmd_data[] = {
+ /*
+ * The last 6 bytes of this packet are the MAC address of
+ * the console, but we don't need that for USB
+ */
+ 0x01, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+};
+
+static const uint8_t switch2_one_data[] = { 0x01, 0x00, 0x00, 0x00 };
+#ifdef CONFIG_SWITCH2_FF
+static const uint8_t switch2_zero_data[] = { 0x00, 0x00, 0x00, 0x00 };
+#endif
+
+static const uint8_t switch2_player_pattern[] = { 0x1, 0x3, 0x7, 0xf, 0x9, 0x5, 0xd, 0x6 };
+
+static const uint8_t switch2_feature_mask[] = {
+ NS2_FEATURE_BUTTONS | NS2_FEATURE_ANALOG | NS2_FEATURE_IMU | NS2_FEATURE_RUMBLE,
+ 0x00, 0x00, 0x00
+};
+
+static DEFINE_IDA(switch2_player_id_allocator);
+
+#ifdef CONFIG_SWITCH2_FF
+static void switch2_encode_rumble(struct switch2_hd_rumble *rumble, uint8_t buffer[5])
+{
+ buffer[0] = rumble->hi_freq;
+ buffer[1] = (rumble->hi_freq >> 8) | (rumble->hi_amp << 2);
+ buffer[2] = (rumble->hi_amp >> 6) | (rumble->lo_freq << 4);
+ buffer[3] = (rumble->lo_freq >> 4) | (rumble->lo_amp << 6);
+ buffer[4] = rumble->lo_amp >> 2;
+}
+
+static int switch2_play_effect(struct input_dev *dev, void *data, struct ff_effect *effect)
+{
+ struct switch2_controller *ns2 = input_get_drvdata(dev);
+
+ if (effect->type != FF_RUMBLE)
+ return 0;
+
+ guard(spinlock_irqsave)(&ns2->rumble_lock);
+ if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+ ns2->rumble.sd.amplitude = max(effect->u.rumble.strong_magnitude,
+ effect->u.rumble.weak_magnitude >> 1);
+ } else {
+ ns2->rumble.hd.hi_amp = effect->u.rumble.weak_magnitude * RUMBLE_MAX >> 16;
+ ns2->rumble.hd.lo_amp = effect->u.rumble.strong_magnitude * RUMBLE_MAX >> 16;
+ }
+
+ schedule_delayed_work(&ns2->rumble_work, 0);
+
+ return 0;
+}
+
+static void switch2_rumble_work(struct work_struct *work)
+{
+ struct switch2_controller *ns2 = container_of(to_delayed_work(work),
+ struct switch2_controller, rumble_work);
+ unsigned long current_ms = jiffies_to_msecs(get_jiffies_64());
+ uint8_t *buffer = kzalloc(64, GFP_KERNEL);
+ unsigned long flags;
+ bool active;
+ int ret;
+
+ spin_lock_irqsave(&ns2->rumble_lock, flags);
+ buffer[0x1] = 0x50 | ns2->rumble_seq;
+ if (ns2->ctlr_type == NS2_CTLR_TYPE_GC) {
+ buffer[0] = 3;
+ if (ns2->rumble.sd.amplitude == 0) {
+ buffer[2] = GC_RUMBLE_STOP;
+ ns2->rumble.sd.error = 0;
+ active = false;
+ } else {
+ if (ns2->rumble.sd.error < ns2->rumble.sd.amplitude) {
+ buffer[2] = GC_RUMBLE_ON;
+ ns2->rumble.sd.error += U16_MAX - ns2->rumble.sd.amplitude;
+ } else {
+ buffer[2] = GC_RUMBLE_OFF;
+ ns2->rumble.sd.error -= ns2->rumble.sd.amplitude;
+ }
+ active = true;
+ }
+ } else {
+ buffer[0] = 1;
+ switch2_encode_rumble(&ns2->rumble.hd, &buffer[0x2]);
+ active = ns2->rumble.hd.hi_amp || ns2->rumble.hd.lo_amp;
+ if (ns2->ctlr_type == NS2_CTLR_TYPE_PRO) {
+ /*
+ * The Pro Controller contains separate LRAs on each
+ * side that can be controlled individually.
+ */
+ buffer[0] = 2;
+ buffer[0x11] = 0x50 | ns2->rumble_seq;
+ switch2_encode_rumble(&ns2->rumble.hd, &buffer[0x12]);
+ }
+ }
+ ns2->rumble_seq = (ns2->rumble_seq + 1) & 0xF;
+
+ if (active) {
+ unsigned long interval = msecs_to_jiffies(4);
+
+ if (!ns2->last_rumble_work)
+ ns2->last_rumble_work = current_ms;
+ else
+ ns2->last_rumble_work += interval;
+ schedule_delayed_work(&ns2->rumble_work,
+ ns2->last_rumble_work + interval - current_ms);
+ } else {
+ ns2->last_rumble_work = 0;
+ }
+ spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+
+ if (!ns2->hdev) {
+ cancel_delayed_work(&ns2->rumble_work);
+ ret = -ENODEV;
+ } else {
+ ret = hid_hw_output_report(ns2->hdev, buffer, 64);
+ }
+
+ kfree(buffer);
+ if (ret < 0)
+ hid_dbg(ns2->hdev, "Failed to send output report ret=%d\n", ret);
+}
+#endif
+
+struct switch2_controller *switch2_get_controller(const char *phys)
+{
+ struct switch2_controller *ns2;
+
+ guard(mutex)(&switch2_controllers_lock);
+ list_for_each_entry(ns2, &switch2_controllers, entry) {
+ if (strncmp(ns2->phys, phys, sizeof(ns2->phys)) == 0)
+ return ns2;
+ }
+ ns2 = kzalloc(sizeof(*ns2), GFP_KERNEL);
+ if (!ns2)
+ return ERR_PTR(-ENOMEM);
+
+ mutex_init(&ns2->lock);
+ INIT_LIST_HEAD(&ns2->entry);
+ list_add(&ns2->entry, &switch2_controllers);
+ strscpy(ns2->phys, phys, sizeof(ns2->phys));
+ return ns2;
+}
+EXPORT_SYMBOL_GPL(switch2_get_controller);
+
+void switch2_controller_put(struct switch2_controller *ns2)
+{
+ mutex_lock(&ns2->lock);
+ if (ns2->input) {
+ input_unregister_device(ns2->input);
+ ns2->input = NULL;
+ }
+ ns2->init_step = 0;
+ if (ns2->hdev || ns2->cfg) {
+ mutex_unlock(&ns2->lock);
+ return;
+ }
+ mutex_unlock(&ns2->lock);
+
+ mutex_lock(&switch2_controllers_lock);
+ list_del_init(&ns2->entry);
+ mutex_unlock(&switch2_controllers_lock);
+ kfree(ns2);
+}
+EXPORT_SYMBOL_GPL(switch2_controller_put);
+
+static bool switch2_parse_stick_calibration(struct switch2_stick_calibration *calib,
+ const uint8_t *data)
+{
+ static const uint8_t UNCALIBRATED[9] = {
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
+ };
+ if (memcmp(UNCALIBRATED, data, sizeof(UNCALIBRATED)) == 0)
+ return false;
+
+ calib->x.neutral = data[0];
+ calib->x.neutral |= (data[1] & 0x0F) << 8;
+
+ calib->y.neutral = data[1] >> 4;
+ calib->y.neutral |= data[2] << 4;
+
+ calib->x.positive = data[3];
+ calib->x.positive |= (data[4] & 0x0F) << 8;
+
+ calib->y.positive = data[4] >> 4;
+ calib->y.positive |= data[5] << 4;
+
+ calib->x.negative = data[6];
+ calib->x.negative |= (data[7] & 0x0F) << 8;
+
+ calib->y.negative = data[7] >> 4;
+ calib->y.negative |= data[8] << 4;
+
+ return true;
+}
+
+static void switch2_handle_flash_read(struct switch2_controller *ns2, uint8_t size,
+ uint32_t address, const uint8_t *data)
+{
+ bool ok;
+
+ switch (address) {
+ case NS2_FLASH_ADDR_SERIAL:
+ if (size != NS2_FLASH_SIZE_SERIAL)
+ return;
+ memcpy(ns2->serial, data, size);
+ break;
+ case NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB:
+ if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB)
+ return;
+ ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], data);
+ if (ok) {
+ hid_dbg(ns2->hdev, "Got factory primary stick calibration:\n");
+ hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+ ns2->stick_calib[0].x.negative,
+ ns2->stick_calib[0].x.neutral,
+ ns2->stick_calib[0].x.positive);
+ hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+ ns2->stick_calib[0].y.negative,
+ ns2->stick_calib[0].y.neutral,
+ ns2->stick_calib[0].y.positive);
+ } else {
+ hid_dbg(ns2->hdev, "Factory primary stick calibration not present\n");
+ }
+ break;
+ case NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB:
+ if (size != NS2_FLASH_SIZE_FACTORY_AXIS_CALIB)
+ return;
+ ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], data);
+ if (ok) {
+ hid_dbg(ns2->hdev, "Got factory secondary stick calibration:\n");
+ hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+ ns2->stick_calib[1].x.negative,
+ ns2->stick_calib[1].x.neutral,
+ ns2->stick_calib[1].x.positive);
+ hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+ ns2->stick_calib[1].y.negative,
+ ns2->stick_calib[1].y.neutral,
+ ns2->stick_calib[1].y.positive);
+ } else {
+ hid_dbg(ns2->hdev, "Factory secondary stick calibration not present\n");
+ }
+ break;
+ case NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB:
+ if (size != NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB)
+ return;
+ if (data[0] != 0xFF && data[1] != 0xFF) {
+ ns2->lt_zero = data[0];
+ ns2->rt_zero = data[1];
+
+ hid_dbg(ns2->hdev, "Got factory trigger calibration:\n");
+ hid_dbg(ns2->hdev, "Left zero point: %i\n", ns2->lt_zero);
+ hid_dbg(ns2->hdev, "Right zero point: %i\n", ns2->rt_zero);
+ } else {
+ hid_dbg(ns2->hdev, "Factory trigger calibration not present\n");
+ }
+ break;
+ case NS2_FLASH_ADDR_USER_PRIMARY_CALIB:
+ if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB)
+ return;
+ if (__le16_to_cpu(*(__le16 *)data) != NS2_USER_CALIB_MAGIC) {
+ hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
+ break;
+ }
+
+ ok = switch2_parse_stick_calibration(&ns2->stick_calib[0], &data[2]);
+ if (ok) {
+ hid_dbg(ns2->hdev, "Got user primary stick calibration:\n");
+ hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+ ns2->stick_calib[0].x.negative,
+ ns2->stick_calib[0].x.neutral,
+ ns2->stick_calib[0].x.positive);
+ hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+ ns2->stick_calib[0].y.negative,
+ ns2->stick_calib[0].y.neutral,
+ ns2->stick_calib[0].y.positive);
+ } else {
+ hid_dbg(ns2->hdev, "No user primary stick calibration present\n");
+ }
+ break;
+ case NS2_FLASH_ADDR_USER_SECONDARY_CALIB:
+ if (size != NS2_FLASH_SIZE_USER_AXIS_CALIB)
+ return;
+ if (__le16_to_cpu(*(__le16 *)data) != NS2_USER_CALIB_MAGIC) {
+ hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
+ break;
+ }
+
+ ok = switch2_parse_stick_calibration(&ns2->stick_calib[1], &data[2]);
+ if (ok) {
+ hid_dbg(ns2->hdev, "Got user secondary stick calibration:\n");
+ hid_dbg(ns2->hdev, "Left max: %i, neutral: %i, right max: %i\n",
+ ns2->stick_calib[1].x.negative,
+ ns2->stick_calib[1].x.neutral,
+ ns2->stick_calib[1].x.positive);
+ hid_dbg(ns2->hdev, "Down max: %i, neutral: %i, up max: %i\n",
+ ns2->stick_calib[1].y.negative,
+ ns2->stick_calib[1].y.neutral,
+ ns2->stick_calib[1].y.positive);
+ } else {
+ hid_dbg(ns2->hdev, "No user secondary stick calibration present\n");
+ }
+ break;
+ }
+}
+
+int switch2_receive_command(struct switch2_controller *ns2,
+ const uint8_t *message, size_t length)
+{
+ const struct switch2_cmd_header *header;
+
+ if (length < 8)
+ return -EINVAL;
+
+ print_hex_dump_debug("got cmd: ", DUMP_PREFIX_OFFSET, 16, 1, message, length, false);
+
+ guard(mutex)(&ns2->lock);
+ if (ns2->init_step < NS2_INIT_DONE)
+ switch2_init_controller(ns2);
+
+ header = (const struct switch2_cmd_header *)message;
+ message = &message[8];
+ switch (header->command) {
+ case NS2_CMD_FLASH:
+ if (header->subcommand == NS2_SUBCMD_FLASH_READ) {
+ uint8_t read_size;
+ uint32_t read_address;
+
+ if (length < 16)
+ return -EINVAL;
+ read_size = message[0];
+ read_address = __le32_to_cpu(*(__le32 *)&message[4]);
+ if (length < read_size + 16)
+ return -EINVAL;
+ switch2_handle_flash_read(ns2, read_size, read_address, &message[8]);
+ }
+ break;
+ case NS2_CMD_FW_INFO:
+ if (header->subcommand == NS2_SUBCMD_FW_INFO_GET) {
+ if (length < sizeof(ns2->version))
+ return -EINVAL;
+ memcpy(&ns2->version, message, sizeof(ns2->version));
+ ns2->ctlr_type = ns2->version.ctlr_type;
+ }
+ break;
+ default:
+ break;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(switch2_receive_command);
+
+static int switch2_features_enable(struct switch2_controller *ns2, int features)
+{
+ __le32 feature_bits = __cpu_to_le32(features);
+
+ if (!ns2->cfg)
+ return -ENOTCONN;
+ return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_ENABLE,
+ &feature_bits, sizeof(feature_bits),
+ ns2->cfg);
+}
+
+static int switch2_read_flash(struct switch2_controller *ns2, uint32_t address,
+ uint8_t size)
+{
+ uint8_t message[8] = { size, 0x7e };
+
+ if (!ns2->cfg)
+ return -ENOTCONN;
+ *(__le32 *)&message[4] = __cpu_to_le32(address);
+ return ns2->cfg->send_command(NS2_CMD_FLASH, NS2_SUBCMD_FLASH_READ, message,
+ sizeof(message), ns2->cfg);
+}
+
+static int switch2_set_player_id(struct switch2_controller *ns2, uint32_t player_id)
+{
+ player_id %= NS2_MAX_PLAYER_ID;
+ uint8_t message[8] = { switch2_player_pattern[player_id] };
+
+ if (!ns2->cfg)
+ return -ENOTCONN;
+ return ns2->cfg->send_command(NS2_CMD_LED, NS2_SUBCMD_LED_PATTERN,
+ &message, sizeof(message),
+ ns2->cfg);
+}
+
+static void switch2_config_buttons(struct input_dev *idev,
+ const struct switch2_ctlr_button_mapping button_mappings[])
+{
+ const struct switch2_ctlr_button_mapping *button;
+
+ for (button = button_mappings; button->code; button++)
+ input_set_capability(idev, EV_KEY, button->code);
+}
+
+static int switch2_init_input(struct switch2_controller *ns2)
+{
+ struct input_dev *input;
+ struct hid_device *hdev = ns2->hdev;
+#ifdef CONFIG_SWITCH2_FF
+ int ret;
+#endif
+
+ rcu_read_lock();
+ input = rcu_dereference(ns2->input);
+ rcu_read_unlock();
+
+ if (input)
+ return 0;
+
+ input = devm_input_allocate_device(&hdev->dev);
+ if (!input)
+ return -ENOMEM;
+
+ input_set_drvdata(input, ns2);
+ input->dev.parent = &hdev->dev;
+ input->id.bustype = hdev->bus;
+ input->id.vendor = hdev->vendor;
+ input->id.product = hdev->product;
+ input->id.version = hdev->version;
+ input->uniq = ns2->serial;
+ input->name = hdev->name;
+ input->phys = hdev->phys;
+
+ switch (ns2->ctlr_type) {
+ case NS2_CTLR_TYPE_JCL:
+ input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+ input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+ switch2_config_buttons(input, left_joycon_button_mappings);
+ break;
+ case NS2_CTLR_TYPE_JCR:
+ input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ switch2_config_buttons(input, right_joycon_button_mappings);
+ break;
+ case NS2_CTLR_TYPE_GC:
+ input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_Z, 0, NS2_TRIGGER_RANGE, 32, 128);
+ input_set_abs_params(input, ABS_RZ, 0, NS2_TRIGGER_RANGE, 32, 128);
+ input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+ input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+ switch2_config_buttons(input, gccon_mappings);
+ break;
+ case NS2_CTLR_TYPE_PRO:
+ input_set_abs_params(input, ABS_X, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_Y, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_RX, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_RY, NS2_AXIS_MIN, NS2_AXIS_MAX, 32, 128);
+ input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+ input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+ switch2_config_buttons(input, procon_mappings);
+ break;
+ default:
+ input_free_device(input);
+ return -EINVAL;
+ }
+
+#ifdef CONFIG_SWITCH2_FF
+ input_set_capability(input, EV_FF, FF_RUMBLE);
+ ret = input_ff_create_memless(input, NULL, switch2_play_effect);
+ if (ret) {
+ input_free_device(input);
+ return ret;
+ }
+#endif
+
+ rcu_assign_pointer(ns2->input, input);
+
+ hid_info(ns2->hdev, "Firmware version %u.%u.%u (%i)\n", ns2->version.major,
+ ns2->version.minor, ns2->version.patch, ns2->version.ctlr_type);
+ if (ns2->version.dsp_type >= 0)
+ hid_info(ns2->hdev, "DSP version %u.%u.%u\n", ns2->version.dsp_major,
+ ns2->version.dsp_minor, ns2->version.dsp_patch);
+ return input_register_device(input);
+}
+
+int switch2_init_controller(struct switch2_controller *ns2)
+{
+ if (ns2->init_step == NS2_INIT_DONE)
+ return 0;
+
+ if (!ns2->cfg)
+ return -ENOTCONN;
+
+ while (ns2->init_step < NS2_INIT_DONE) {
+ ns2->init_step++;
+ /* TODO verify that the step completed successfully */
+ switch (ns2->init_step) {
+ case NS2_INIT_READ_SERIAL:
+ return switch2_read_flash(ns2, NS2_FLASH_ADDR_SERIAL,
+ NS2_FLASH_SIZE_SERIAL);
+ case NS2_INIT_GET_FIRMWARE_INFO:
+ return ns2->cfg->send_command(NS2_CMD_FW_INFO, NS2_SUBCMD_FW_INFO_GET,
+ NULL, 0, ns2->cfg);
+ break;
+ case NS2_INIT_READ_FACTORY_PRIMARY_CALIB:
+ return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB,
+ NS2_FLASH_SIZE_FACTORY_AXIS_CALIB);
+ case NS2_INIT_READ_FACTORY_SECONDARY_CALIB:
+ if (switch2_ctlr_is_joycon(ns2->ctlr_type))
+ break;
+ return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB,
+ NS2_FLASH_SIZE_FACTORY_AXIS_CALIB);
+ case NS2_INIT_READ_FACTORY_TRIGGER_CALIB:
+ if (ns2->ctlr_type != NS2_CTLR_TYPE_GC)
+ break;
+ return switch2_read_flash(ns2, NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB,
+ NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB);
+ case NS2_INIT_READ_USER_PRIMARY_CALIB:
+ return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_PRIMARY_CALIB,
+ NS2_FLASH_SIZE_USER_AXIS_CALIB);
+ case NS2_INIT_READ_USER_SECONDARY_CALIB:
+ if (switch2_ctlr_is_joycon(ns2->ctlr_type))
+ break;
+ return switch2_read_flash(ns2, NS2_FLASH_ADDR_USER_SECONDARY_CALIB,
+ NS2_FLASH_SIZE_USER_AXIS_CALIB);
+ case NS2_INIT_SET_FEATURE_MASK:
+ return ns2->cfg->send_command(NS2_CMD_FEATSEL, NS2_SUBCMD_FEATSEL_SET_MASK,
+ switch2_feature_mask, sizeof(switch2_feature_mask), ns2->cfg);
+ break;
+ case NS2_INIT_ENABLE_FEATURES:
+ return switch2_features_enable(ns2, NS2_FEATURE_BUTTONS |
+ NS2_FEATURE_ANALOG | NS2_FEATURE_RUMBLE);
+ break;
+#ifdef CONFIG_SWITCH2_FF
+ case NS2_INIT_ENABLE_RUMBLE:
+ return ns2->cfg->send_command(NS2_CMD_NFC, 1,
+ switch2_zero_data, sizeof(switch2_zero_data),
+ ns2->cfg);
+ break;
+#endif
+ case NS2_INIT_GRIP_BUTTONS:
+ if (!switch2_ctlr_is_joycon(ns2->ctlr_type))
+ break;
+ return ns2->cfg->send_command(NS2_CMD_GRIP, NS2_SUBCMD_GRIP_ENABLE_BUTTONS,
+ switch2_one_data, sizeof(switch2_one_data),
+ ns2->cfg);
+ break;
+ case NS2_INIT_SET_PLAYER_LEDS:
+ return switch2_set_player_id(ns2, ns2->player_id);
+ case NS2_INIT_INPUT:
+ return ns2->cfg->send_command(NS2_CMD_INIT, NS2_SUBCMD_INIT_USB,
+ switch2_init_cmd_data, sizeof(switch2_init_cmd_data), ns2->cfg);
+ case NS2_INIT_DONE:
+ if (ns2->hdev)
+ return switch2_init_input(ns2);
+ break;
+ default:
+ WARN_ON_ONCE(1);
+ }
+ }
+ return 0;
+}
+EXPORT_SYMBOL_GPL(switch2_init_controller);
+
+static void switch2_report_buttons(struct input_dev *input, const uint8_t *bytes,
+ const struct switch2_ctlr_button_mapping button_mappings[])
+{
+ const struct switch2_ctlr_button_mapping *button;
+
+ for (button = button_mappings; button->code; button++)
+ input_report_key(input, button->code, bytes[button->byte] & button->bit);
+}
+
+static void switch2_report_axis(struct input_dev *input, struct switch2_axis_calibration *calib,
+ int axis, int value, bool negate)
+{
+ if (calib && calib->neutral && calib->negative && calib->positive) {
+ value -= calib->neutral;
+ value *= NS2_AXIS_MAX + 1;
+ if (value < 0)
+ value /= calib->negative;
+ else
+ value /= calib->positive;
+ } else {
+ value = (value - 2048) * 16;
+ }
+
+ if (negate)
+ value = -value;
+ input_report_abs(input, axis,
+ clamp(value, NS2_AXIS_MIN, NS2_AXIS_MAX));
+}
+
+static void switch2_report_stick(struct input_dev *input, struct switch2_stick_calibration *calib,
+ int x, int y, const uint8_t *data)
+{
+ switch2_report_axis(input, &calib->x, x, data[0] | ((data[1] & 0x0F) << 8), false);
+ switch2_report_axis(input, &calib->y, y, (data[1] >> 4) | (data[2] << 4), true);
+}
+
+static void switch2_report_trigger(struct input_dev *input, uint8_t zero, int abs, uint8_t data)
+{
+ int value = (NS2_TRIGGER_RANGE + 1) * (data - zero) / (232 - zero);
+
+ input_report_abs(input, abs, clamp(value, 0, NS2_TRIGGER_RANGE));
+}
+
+static int switch2_event(struct hid_device *hdev, struct hid_report *report, uint8_t *raw_data,
+ int size)
+{
+ struct switch2_controller *ns2 = hid_get_drvdata(hdev);
+ struct input_dev *input;
+
+ if (report->type != HID_INPUT_REPORT)
+ return 0;
+
+ if (size < 15)
+ return -EINVAL;
+
+ rcu_read_lock();
+ input = rcu_dereference(ns2->input);
+ rcu_read_unlock();
+
+ if (!input)
+ return 0;
+
+ switch (report->id) {
+ case NS2_REPORT_UNIFIED:
+ /*
+ * TODO
+ * This won't be sent unless the report type gets changed via command
+ * 03-0A, but we should support it at some point regardless.
+ */
+ break;
+ case NS2_REPORT_JCL:
+ input_report_abs(input, ABS_HAT0X,
+ !!(raw_data[3] & NS2_BTNL_RIGHT) -
+ !!(raw_data[3] & NS2_BTNL_LEFT));
+ input_report_abs(input, ABS_HAT0Y,
+ !!(raw_data[3] & NS2_BTNL_DOWN) -
+ !!(raw_data[3] & NS2_BTNL_UP));
+ switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, ABS_Y, &raw_data[6]);
+ switch2_report_buttons(input, &raw_data[3], left_joycon_button_mappings);
+ break;
+ case NS2_REPORT_JCR:
+ switch2_report_stick(input, &ns2->stick_calib[0], ABS_RX, ABS_RY, &raw_data[6]);
+ switch2_report_buttons(input, &raw_data[3], right_joycon_button_mappings);
+ break;
+ case NS2_REPORT_GC:
+ input_report_abs(input, ABS_HAT0X,
+ !!(raw_data[4] & NS2_BTNL_RIGHT) -
+ !!(raw_data[4] & NS2_BTNL_LEFT));
+ input_report_abs(input, ABS_HAT0Y,
+ !!(raw_data[4] & NS2_BTNL_DOWN) -
+ !!(raw_data[4] & NS2_BTNL_UP));
+ switch2_report_buttons(input, &raw_data[3], gccon_mappings);
+ switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, ABS_Y, &raw_data[6]);
+ switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, ABS_RY, &raw_data[9]);
+ switch2_report_trigger(input, ns2->lt_zero, ABS_Z, raw_data[13]);
+ switch2_report_trigger(input, ns2->rt_zero, ABS_RZ, raw_data[14]);
+ break;
+ case NS2_REPORT_PRO:
+ input_report_abs(input, ABS_HAT0X,
+ !!(raw_data[4] & NS2_BTNL_RIGHT) -
+ !!(raw_data[4] & NS2_BTNL_LEFT));
+ input_report_abs(input, ABS_HAT0Y,
+ !!(raw_data[4] & NS2_BTNL_DOWN) -
+ !!(raw_data[4] & NS2_BTNL_UP));
+ switch2_report_buttons(input, &raw_data[3], procon_mappings);
+ switch2_report_stick(input, &ns2->stick_calib[0], ABS_X, ABS_Y, &raw_data[6]);
+ switch2_report_stick(input, &ns2->stick_calib[1], ABS_RX, ABS_RY, &raw_data[9]);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ input_sync(input);
+ return 0;
+}
+
+static int switch2_probe(struct hid_device *hdev, const struct hid_device_id *id)
+{
+ struct switch2_controller *ns2;
+ struct usb_device *udev;
+ char phys[64];
+ int ret;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ udev = hid_to_usb_dev(hdev);
+ if (usb_make_path(udev, phys, sizeof(phys)) < 0)
+ return -EINVAL;
+
+ ret = hid_parse(hdev);
+ if (ret) {
+ hid_err(hdev, "parse failed %d\n", ret);
+ return ret;
+ }
+
+ ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
+ if (ret) {
+ hid_err(hdev, "hw_start failed %d\n", ret);
+ return ret;
+ }
+
+ ret = hid_hw_open(hdev);
+ if (ret) {
+ hid_err(hdev, "hw_open failed %d\n", ret);
+ goto err_stop;
+ }
+
+ ns2 = switch2_get_controller(phys);
+ if (!ns2) {
+ ret = -ENOMEM;
+ goto err_close;
+ }
+
+ guard(mutex)(&ns2->lock);
+ ns2->hdev = hdev;
+
+ ns2->player_id = U32_MAX;
+ ret = ida_alloc(&switch2_player_id_allocator, GFP_KERNEL);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to allocate player ID, skipping; ret=%d\n", ret);
+ else
+ ns2->player_id = ret;
+#ifdef CONFIG_SWITCH2_FF
+ if (ns2->ctlr_type != NS2_CTLR_TYPE_GC) {
+ ns2->rumble.hd.hi_freq = RUMBLE_HI_FREQ;
+ ns2->rumble.hd.lo_freq = RUMBLE_LO_FREQ;
+ }
+ spin_lock_init(&ns2->rumble_lock);
+ INIT_DELAYED_WORK(&ns2->rumble_work, switch2_rumble_work);
+#endif
+ hid_set_drvdata(hdev, ns2);
+
+ if (ns2->cfg)
+ return switch2_init_controller(ns2);
+
+ return 0;
+
+err_close:
+ hid_hw_close(hdev);
+err_stop:
+ hid_hw_stop(hdev);
+
+ return ret;
+}
+
+static void switch2_remove(struct hid_device *hdev)
+{
+ struct switch2_controller *ns2 = hid_get_drvdata(hdev);
+#ifdef CONFIG_SWITCH2_FF
+ unsigned long flags;
+#endif
+
+#ifdef CONFIG_SWITCH2_FF
+ spin_lock_irqsave(&ns2->rumble_lock, flags);
+ cancel_delayed_work_sync(&ns2->rumble_work);
+ spin_unlock_irqrestore(&ns2->rumble_lock, flags);
+#endif
+ mutex_lock(&ns2->lock);
+ ns2->hdev = NULL;
+ mutex_unlock(&ns2->lock);
+ hid_hw_close(hdev);
+ ida_free(&switch2_player_id_allocator, ns2->player_id);
+ switch2_controller_put(ns2);
+ hid_hw_stop(hdev);
+}
+
+static const struct hid_device_id switch2_devices[] = {
+ { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_PROCON) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_GCCON) },
+ {}
+};
+MODULE_DEVICE_TABLE(hid, switch2_devices);
+
+static struct hid_driver switch2_hid_driver = {
+ .name = "switch2",
+ .id_table = switch2_devices,
+ .probe = switch2_probe,
+ .remove = switch2_remove,
+ .raw_event = switch2_event,
+};
+
+static int __init switch2_init(void)
+{
+ return hid_register_driver(&switch2_hid_driver);
+}
+
+static void __exit switch2_exit(void)
+{
+ hid_unregister_driver(&switch2_hid_driver);
+ ida_destroy(&switch2_player_id_allocator);
+}
+
+module_init(switch2_init);
+module_exit(switch2_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
+MODULE_DESCRIPTION("Driver for Nintendo Switch 2 Controllers");
diff --git a/drivers/hid/hid-switch2.h b/drivers/hid/hid-switch2.h
new file mode 100644
index 0000000000000..4b7b82da0c37f
--- /dev/null
+++ b/drivers/hid/hid-switch2.h
@@ -0,0 +1,242 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * HID driver for Nintendo Switch controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the following work:
+ * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
+ * https://github.com/ndeadly/switch2_controller_research
+ */
+
+#include <linux/bits.h>
+#include <linux/input.h>
+#include <linux/mutex.h>
+#include <linux/rcupdate.h>
+#include <linux/spinlock.h>
+#include "hid-ids.h"
+
+#define NS2_FLAG_OK BIT(0)
+#define NS2_FLAG_NACK BIT(2)
+
+#define NS2_FLASH_ADDR_SERIAL 0x13002
+#define NS2_FLASH_ADDR_FACTORY_PRIMARY_CALIB 0x130a8
+#define NS2_FLASH_ADDR_FACTORY_SECONDARY_CALIB 0x130e8
+#define NS2_FLASH_ADDR_FACTORY_TRIGGER_CALIB 0x13140
+#define NS2_FLASH_ADDR_USER_PRIMARY_CALIB 0x1fc040
+#define NS2_FLASH_ADDR_USER_SECONDARY_CALIB 0x1fc080
+
+#define NS2_FLASH_SIZE_SERIAL 0x10
+#define NS2_FLASH_SIZE_FACTORY_AXIS_CALIB 9
+#define NS2_FLASH_SIZE_FACTORY_TRIGGER_CALIB 2
+#define NS2_FLASH_SIZE_USER_AXIS_CALIB 11
+
+#define NS2_USER_CALIB_MAGIC 0xa1b2
+
+#define NS2_FEATURE_BUTTONS BIT(0)
+#define NS2_FEATURE_ANALOG BIT(1)
+#define NS2_FEATURE_IMU BIT(2)
+#define NS2_FEATURE_MOUSE BIT(4)
+#define NS2_FEATURE_RUMBLE BIT(5)
+#define NS2_FEATURE_MAGNETO BIT(7)
+
+enum switch2_cmd {
+ NS2_CMD_NFC = 0x01,
+ NS2_CMD_FLASH = 0x02,
+ NS2_CMD_INIT = 0x03,
+ NS2_CMD_GRIP = 0x08,
+ NS2_CMD_LED = 0x09,
+ NS2_CMD_VIBRATE = 0x0a,
+ NS2_CMD_BATTERY = 0x0b,
+ NS2_CMD_FEATSEL = 0x0c,
+ NS2_CMD_FW_UPD = 0x0d,
+ NS2_CMD_FW_INFO = 0x10,
+ NS2_CMD_BT_PAIR = 0x15,
+};
+
+enum switch2_direction {
+ NS2_DIR_IN = 0x00,
+ NS2_DIR_OUT = 0x90,
+};
+
+enum switch2_transport {
+ NS2_TRANS_USB = 0x00,
+ NS2_TRANS_BT = 0x01,
+};
+
+enum switch2_subcmd_flash {
+ NS2_SUBCMD_FLASH_READ_BLOCK = 0x01,
+ NS2_SUBCMD_FLASH_WRITE_BLOCK = 0x02,
+ NS2_SUBCMD_FLASH_ERASE_BLOCK = 0x03,
+ NS2_SUBCMD_FLASH_READ = 0x04,
+ NS2_SUBCMD_FLASH_WRITE = 0x05,
+};
+
+enum switch2_subcmd_init {
+ NS2_SUBCMD_INIT_SELECT_REPORT = 0xa,
+ NS2_SUBCMD_INIT_USB = 0xd,
+};
+
+enum switch2_subcmd_feature_select {
+ NS2_SUBCMD_FEATSEL_GET_INFO = 0x1,
+ NS2_SUBCMD_FEATSEL_SET_MASK = 0x2,
+ NS2_SUBCMD_FEATSEL_CLEAR_MASK = 0x3,
+ NS2_SUBCMD_FEATSEL_ENABLE = 0x4,
+ NS2_SUBCMD_FEATSEL_DISABLE = 0x5,
+};
+
+enum switch2_subcmd_grip {
+ NS2_SUBCMD_GRIP_GET_INFO = 0x1,
+ NS2_SUBCMD_GRIP_ENABLE_BUTTONS = 0x2,
+ NS2_SUBCMD_GRIP_GET_INFO_EXT = 0x3,
+};
+
+enum switch2_subcmd_led {
+ NS2_SUBCMD_LED_P1 = 0x1,
+ NS2_SUBCMD_LED_P2 = 0x2,
+ NS2_SUBCMD_LED_P3 = 0x3,
+ NS2_SUBCMD_LED_P4 = 0x4,
+ NS2_SUBCMD_LED_ALL_ON = 0x5,
+ NS2_SUBCMD_LED_ALL_OFF = 0x6,
+ NS2_SUBCMD_LED_PATTERN = 0x7,
+ NS2_SUBCMD_LED_BLINK = 0x8,
+};
+
+enum switch2_subcmd_fw_info {
+ NS2_SUBCMD_FW_INFO_GET = 0x1,
+};
+
+enum switch2_ctlr_type {
+ NS2_CTLR_TYPE_JCL = 0x00,
+ NS2_CTLR_TYPE_JCR = 0x01,
+ NS2_CTLR_TYPE_PRO = 0x02,
+ NS2_CTLR_TYPE_GC = 0x03,
+};
+
+enum switch2_report_id {
+ NS2_REPORT_UNIFIED = 0x05,
+ NS2_REPORT_JCL = 0x07,
+ NS2_REPORT_JCR = 0x08,
+ NS2_REPORT_PRO = 0x09,
+ NS2_REPORT_GC = 0x0a,
+};
+
+enum switch2_init_step {
+ NS2_INIT_STARTING,
+ NS2_INIT_READ_SERIAL,
+ NS2_INIT_READ_FACTORY_PRIMARY_CALIB,
+ NS2_INIT_READ_FACTORY_SECONDARY_CALIB,
+ NS2_INIT_READ_FACTORY_TRIGGER_CALIB,
+ NS2_INIT_READ_USER_PRIMARY_CALIB,
+ NS2_INIT_READ_USER_SECONDARY_CALIB,
+ NS2_INIT_SET_FEATURE_MASK,
+ NS2_INIT_ENABLE_FEATURES,
+ NS2_INIT_GET_FIRMWARE_INFO,
+#ifdef CONFIG_SWITCH2_FF
+ NS2_INIT_ENABLE_RUMBLE,
+#endif
+ NS2_INIT_GRIP_BUTTONS,
+ NS2_INIT_SET_PLAYER_LEDS,
+ NS2_INIT_INPUT,
+ NS2_INIT_DONE,
+};
+
+struct switch2_cmd_header {
+ uint8_t command;
+ uint8_t direciton;
+ uint8_t transport;
+ uint8_t subcommand;
+ uint8_t unk1;
+ uint8_t length;
+ uint16_t unk2;
+};
+static_assert(sizeof(struct switch2_cmd_header) == 8);
+
+struct switch2_controller;
+struct switch2_cfg_intf {
+ struct switch2_controller *parent;
+
+ int (*send_command)(enum switch2_cmd command, uint8_t subcommand,
+ const void *message, size_t length,
+ struct switch2_cfg_intf *intf);
+};
+
+struct switch2_version_info {
+ uint8_t major;
+ uint8_t minor;
+ uint8_t patch;
+ uint8_t ctlr_type;
+ __le32 unk;
+ int8_t dsp_major;
+ int8_t dsp_minor;
+ int8_t dsp_patch;
+ int8_t dsp_type;
+};
+
+struct switch2_axis_calibration {
+ uint16_t neutral;
+ uint16_t negative;
+ uint16_t positive;
+};
+
+struct switch2_stick_calibration {
+ struct switch2_axis_calibration x;
+ struct switch2_axis_calibration y;
+};
+
+struct switch2_hd_rumble {
+ uint16_t hi_freq : 10;
+ uint16_t hi_amp : 10;
+ uint16_t lo_freq : 10;
+ uint16_t lo_amp : 10;
+};
+
+struct switch2_erm_rumble {
+ uint16_t error;
+ uint16_t amplitude;
+};
+
+struct switch2_controller {
+ struct hid_device *hdev;
+ struct switch2_cfg_intf *cfg;
+
+ char phys[64];
+ struct list_head entry;
+ struct mutex lock;
+
+ enum switch2_ctlr_type ctlr_type;
+ enum switch2_init_step init_step;
+ struct input_dev __rcu *input;
+ char serial[NS2_FLASH_SIZE_SERIAL + 1];
+ struct switch2_version_info version;
+
+ struct switch2_stick_calibration stick_calib[2];
+ uint8_t lt_zero;
+ uint8_t rt_zero;
+
+ uint32_t player_id;
+
+#ifdef CONFIG_SWITCH2_FF
+ spinlock_t rumble_lock;
+ uint8_t rumble_seq;
+ union {
+ struct switch2_hd_rumble hd;
+ struct switch2_erm_rumble sd;
+ } rumble;
+ unsigned long last_rumble_work;
+ struct delayed_work rumble_work;
+#endif
+};
+
+struct switch2_controller *switch2_get_controller(const char *phys);
+void switch2_controller_put(struct switch2_controller *controller);
+
+int switch2_receive_command(struct switch2_controller *controller,
+ const uint8_t *message, size_t length);
+
+int switch2_init_controller(struct switch2_controller *controller);
+
+static inline bool switch2_ctlr_is_joycon(enum switch2_ctlr_type type)
+{
+ return type == NS2_CTLR_TYPE_JCL || type == NS2_CTLR_TYPE_JCR;
+}
diff --git a/drivers/input/joystick/Kconfig b/drivers/input/joystick/Kconfig
index 7755e5b454d2c..73b0e15205aee 100644
--- a/drivers/input/joystick/Kconfig
+++ b/drivers/input/joystick/Kconfig
@@ -422,4 +422,12 @@ config JOYSTICK_SEESAW
To compile this driver as a module, choose M here: the module will be
called adafruit-seesaw.
+config JOYSTICK_SWITCH2_USB
+ tristate "Wired Nintendo Switch 2 controller support"
+ depends on HID_SWITCH2
+ select USB
+ help
+ To compile this driver as a module, choose M here: the
+ module will be called switch2-usb.
+
endif
diff --git a/drivers/input/joystick/Makefile b/drivers/input/joystick/Makefile
index 9976f596a9208..46feb7ef60ba0 100644
--- a/drivers/input/joystick/Makefile
+++ b/drivers/input/joystick/Makefile
@@ -34,6 +34,7 @@ obj-$(CONFIG_JOYSTICK_SIDEWINDER) += sidewinder.o
obj-$(CONFIG_JOYSTICK_SPACEBALL) += spaceball.o
obj-$(CONFIG_JOYSTICK_SPACEORB) += spaceorb.o
obj-$(CONFIG_JOYSTICK_STINGER) += stinger.o
+obj-$(CONFIG_JOYSTICK_SWITCH2_USB) += switch2-usb.o
obj-$(CONFIG_JOYSTICK_TMDC) += tmdc.o
obj-$(CONFIG_JOYSTICK_TURBOGRAFX) += turbografx.o
obj-$(CONFIG_JOYSTICK_TWIDJOY) += twidjoy.o
diff --git a/drivers/input/joystick/switch2-usb.c b/drivers/input/joystick/switch2-usb.c
new file mode 100644
index 0000000000000..2c8b09f4b98e9
--- /dev/null
+++ b/drivers/input/joystick/switch2-usb.c
@@ -0,0 +1,355 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * HID driver for Nintendo Switch controllers
+ *
+ * Copyright (c) 2025 Valve Software
+ *
+ * This driver is based on the following work:
+ * https://gist.github.com/shinyquagsire23/66f006b46c56216acbaac6c1e2279b64
+ * https://github.com/ndeadly/switch2_controller_research
+ */
+
+#include "../../hid/hid-switch2.h"
+#include <linux/module.h>
+#include <linux/usb/input.h>
+
+#define NS2_BULK_SIZE 64
+#define NS2_IN_URBS 2
+#define NS2_OUT_URBS 4
+
+static struct usb_driver switch2_usb;
+
+struct switch2_urb {
+ struct urb *urb;
+ uint8_t *data;
+ bool active;
+};
+
+struct switch2_usb {
+ struct switch2_cfg_intf cfg;
+ struct usb_device *udev;
+
+ struct switch2_urb bulk_in[NS2_IN_URBS];
+ struct usb_anchor bulk_in_anchor;
+ spinlock_t bulk_in_lock;
+
+ struct switch2_urb bulk_out[NS2_OUT_URBS];
+ struct usb_anchor bulk_out_anchor;
+ spinlock_t bulk_out_lock;
+
+ int message_in;
+ struct work_struct message_in_work;
+};
+
+static void switch2_bulk_in(struct urb *urb)
+{
+ struct switch2_usb *ns2_usb = urb->context;
+ int i;
+ bool schedule = false;
+ unsigned long flags;
+
+ switch (urb->status) {
+ case 0:
+ schedule = true;
+ break;
+ case -ECONNRESET:
+ case -ENOENT:
+ case -ESHUTDOWN:
+ return;
+ default:
+ dev_dbg(&ns2_usb->udev->dev, "unknown urb status: %d\n",
+ urb->status);
+ break;
+ }
+
+ spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+ for (i = 0; i < NS2_IN_URBS; i++) {
+ int err;
+ struct switch2_urb *ns2_urb;
+
+ if (ns2_usb->bulk_in[i].urb == urb) {
+ ns2_usb->message_in = i;
+ continue;
+ }
+
+ if (ns2_usb->bulk_in[i].active)
+ continue;
+
+ ns2_urb = &ns2_usb->bulk_in[i];
+ usb_anchor_urb(ns2_urb->urb, &ns2_usb->bulk_out_anchor);
+ err = usb_submit_urb(ns2_urb->urb, GFP_ATOMIC);
+ if (err) {
+ usb_unanchor_urb(ns2_urb->urb);
+ dev_dbg(&ns2_usb->udev->dev, "failed to queue input urb: %d\n", err);
+ } else {
+ ns2_urb->active = true;
+ }
+ }
+ spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+ if (schedule)
+ schedule_work(&ns2_usb->message_in_work);
+}
+
+static void switch2_bulk_out(struct urb *urb)
+{
+ struct switch2_usb *ns2_usb = urb->context;
+ int i;
+
+ guard(spinlock_irqsave)(&ns2_usb->bulk_out_lock);
+
+ switch (urb->status) {
+ case 0:
+ break;
+ case -ECONNRESET:
+ case -ENOENT:
+ case -ESHUTDOWN:
+ return;
+ default:
+ dev_dbg(&ns2_usb->udev->dev, "unknown urb status: %d\n", urb->status);
+ return;
+ }
+
+ for (i = 0; i < NS2_OUT_URBS; i++) {
+ if (ns2_usb->bulk_out[i].urb != urb)
+ continue;
+
+ ns2_usb->bulk_out[i].active = false;
+ break;
+ }
+}
+
+static int switch2_usb_send_cmd(enum switch2_cmd command, uint8_t subcommand,
+ const void *message, size_t size, struct switch2_cfg_intf *cfg)
+{
+ struct switch2_usb *ns2_usb = (struct switch2_usb *)cfg;
+ struct switch2_urb *urb = NULL;
+ int i;
+ int ret;
+
+ struct switch2_cmd_header header = {
+ command, NS2_DIR_OUT | NS2_FLAG_OK, NS2_TRANS_USB, subcommand, 0, size
+ };
+
+ if (size > 56) {
+ WARN_ON(1);
+ return -EINVAL;
+ }
+
+ for (i = 0; i < NS2_OUT_URBS; i++) {
+ if (ns2_usb->bulk_out[i].active)
+ continue;
+
+ urb = &ns2_usb->bulk_out[i];
+ urb->active = true;
+ break;
+ }
+ if (!urb) {
+ dev_warn(&ns2_usb->udev->dev, "output queue full, dropping message\n");
+ return -ENOBUFS;
+ }
+
+ memcpy(urb->data, &header, sizeof(header));
+ if (message && size)
+ memcpy(&urb->data[8], message, size);
+ urb->urb->transfer_buffer_length = size + sizeof(header);
+
+ print_hex_dump_debug("sending cmd: ", DUMP_PREFIX_OFFSET, 16, 1, urb->data,
+ size + sizeof(header), false);
+
+ usb_anchor_urb(urb->urb, &ns2_usb->bulk_out_anchor);
+ ret = usb_submit_urb(urb->urb, GFP_ATOMIC);
+ if (ret) {
+ dev_warn(&ns2_usb->udev->dev, "failed to submit urb: %i", ret);
+ urb->active = false;
+ usb_unanchor_urb(urb->urb);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void switch2_usb_message_in_work(struct work_struct *work)
+{
+ struct switch2_usb *ns2_usb = container_of(work, struct switch2_usb, message_in_work);
+ struct switch2_urb *urb;
+ int err;
+ unsigned long flags;
+
+ spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+ urb = &ns2_usb->bulk_in[ns2_usb->message_in];
+ spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+
+ err = switch2_receive_command(ns2_usb->cfg.parent, urb->urb->transfer_buffer,
+ urb->urb->actual_length);
+ if (err)
+ dev_dbg(&ns2_usb->udev->dev, "receive command failed: %d\n", err);
+
+ spin_lock_irqsave(&ns2_usb->bulk_in_lock, flags);
+ urb->active = false;
+ spin_unlock_irqrestore(&ns2_usb->bulk_in_lock, flags);
+}
+
+static int switch2_usb_probe(struct usb_interface *intf, const struct usb_device_id *id)
+{
+ struct switch2_controller *ns2;
+ struct switch2_usb *ns2_usb;
+ struct usb_device *udev;
+ struct usb_endpoint_descriptor *bulk_in, *bulk_out;
+ char phys[64];
+ int ret;
+ int i;
+
+ udev = interface_to_usbdev(intf);
+ if (usb_make_path(udev, phys, sizeof(phys)) < 0)
+ return -EINVAL;
+
+ ret = usb_find_common_endpoints(intf->cur_altsetting, &bulk_in, &bulk_out, NULL, NULL);
+ if (ret) {
+ dev_err(&intf->dev, "failed to find bulk EPs\n");
+ return ret;
+ }
+
+ ns2_usb = devm_kzalloc(&intf->dev, sizeof(*ns2_usb), GFP_KERNEL);
+ if (!ns2_usb)
+ return -ENOMEM;
+
+ ns2_usb->udev = udev;
+ for (i = 0; i < NS2_IN_URBS; i++) {
+ ns2_usb->bulk_in[i].urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!ns2_usb->bulk_in[i].urb) {
+ ret = -ENOMEM;
+ goto err_free_in;
+ }
+
+ ns2_usb->bulk_in[i].data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
+ &ns2_usb->bulk_in[i].urb->transfer_dma);
+ if (!ns2_usb->bulk_in[i].data) {
+ ret = -ENOMEM;
+ goto err_free_in;
+ }
+
+ usb_fill_bulk_urb(ns2_usb->bulk_in[i].urb, udev,
+ usb_rcvbulkpipe(udev, bulk_in->bEndpointAddress),
+ ns2_usb->bulk_in[i].data, NS2_BULK_SIZE, switch2_bulk_in, ns2_usb);
+ ns2_usb->bulk_in[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+ }
+
+ for (i = 0; i < NS2_OUT_URBS; i++) {
+ ns2_usb->bulk_out[i].urb = usb_alloc_urb(0, GFP_KERNEL);
+ if (!ns2_usb->bulk_out[i].urb) {
+ ret = -ENOMEM;
+ goto err_free_out;
+ }
+
+ ns2_usb->bulk_out[i].data = usb_alloc_coherent(udev, NS2_BULK_SIZE, GFP_KERNEL,
+ &ns2_usb->bulk_out[i].urb->transfer_dma);
+ if (!ns2_usb->bulk_out[i].data) {
+ ret = -ENOMEM;
+ goto err_free_out;
+ }
+
+ usb_fill_bulk_urb(ns2_usb->bulk_out[i].urb, udev,
+ usb_sndbulkpipe(udev, bulk_out->bEndpointAddress),
+ ns2_usb->bulk_out[i].data, NS2_BULK_SIZE, switch2_bulk_out, ns2_usb);
+ ns2_usb->bulk_out[i].urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
+ }
+
+ ns2_usb->bulk_in[0].active = true;
+ ret = usb_submit_urb(ns2_usb->bulk_in[0].urb, GFP_ATOMIC);
+ if (ret < 0)
+ goto err_free_out;
+
+ ns2 = switch2_get_controller(phys);
+ if (IS_ERR(ns2)) {
+ ret = PTR_ERR(ns2);
+ goto err_kill_urb;
+ }
+
+ ns2_usb->cfg.parent = ns2;
+ ns2_usb->cfg.send_command = switch2_usb_send_cmd;
+
+ init_usb_anchor(&ns2_usb->bulk_out_anchor);
+ spin_lock_init(&ns2_usb->bulk_out_lock);
+ init_usb_anchor(&ns2_usb->bulk_in_anchor);
+ spin_lock_init(&ns2_usb->bulk_in_lock);
+ INIT_WORK(&ns2_usb->message_in_work, switch2_usb_message_in_work);
+
+ usb_set_intfdata(intf, ns2_usb);
+
+ guard(mutex)(&ns2->lock);
+ ns2->cfg = (struct switch2_cfg_intf *) ns2_usb;
+
+ if (ns2->hdev)
+ return switch2_init_controller(ns2);
+
+ return 0;
+
+err_kill_urb:
+ usb_kill_urb(ns2_usb->bulk_in[0].urb);
+err_free_out:
+ for (i = 0; i < NS2_OUT_URBS; i++) {
+ usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_out[i].data,
+ ns2_usb->bulk_out[i].urb->transfer_dma);
+ usb_free_urb(ns2_usb->bulk_out[i].urb);
+ }
+err_free_in:
+ for (i = 0; i < NS2_IN_URBS; i++) {
+ usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_in[i].data,
+ ns2_usb->bulk_in[i].urb->transfer_dma);
+ usb_free_urb(ns2_usb->bulk_in[i].urb);
+ }
+ devm_kfree(&intf->dev, ns2_usb);
+
+ return ret;
+}
+
+static void switch2_usb_disconnect(struct usb_interface *intf)
+{
+ struct switch2_usb *ns2_usb = usb_get_intfdata(intf);
+ struct switch2_controller *ns2 = ns2_usb->cfg.parent;
+ int i;
+
+ usb_kill_anchored_urbs(&ns2_usb->bulk_out_anchor);
+ for (i = 0; i < NS2_OUT_URBS; i++) {
+ usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_out[i].data,
+ ns2_usb->bulk_out[i].urb->transfer_dma);
+ usb_free_urb(ns2_usb->bulk_out[i].urb);
+ }
+
+ usb_kill_anchored_urbs(&ns2_usb->bulk_in_anchor);
+ for (i = 0; i < NS2_IN_URBS; i++) {
+ usb_free_coherent(ns2_usb->udev, NS2_BULK_SIZE, ns2_usb->bulk_in[i].data,
+ ns2_usb->bulk_in[i].urb->transfer_dma);
+ usb_free_urb(ns2_usb->bulk_in[i].urb);
+ }
+
+ mutex_lock(&ns2->lock);
+ ns2->cfg = NULL;
+ mutex_unlock(&ns2->lock);
+ switch2_controller_put(ns2);
+}
+
+#define SWITCH2_CONTROLLER(vend, prod) \
+ USB_DEVICE_AND_INTERFACE_INFO(vend, prod, USB_CLASS_VENDOR_SPEC, 0, 0)
+
+static const struct usb_device_id switch2_usb_devices[] = {
+ { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONL) },
+ { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_JOYCONR) },
+ { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_PROCON) },
+ { SWITCH2_CONTROLLER(USB_VENDOR_ID_NINTENDO, USB_DEVICE_ID_NINTENDO_NS2_GCCON) },
+ { }
+};
+MODULE_DEVICE_TABLE(usb, switch2_usb_devices);
+
+static struct usb_driver switch2_usb = {
+ .name = "switch2",
+ .id_table = switch2_usb_devices,
+ .probe = switch2_usb_probe,
+ .disconnect = switch2_usb_disconnect,
+};
+module_usb_driver(switch2_usb);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Vicki Pfau <vi@endrift.com>");
+MODULE_DESCRIPTION("Driver for Nintendo Switch 2 Controllers");
--
2.51.0
^ permalink raw reply related
* [PATCH v1] HID: Fix Report Descriptor for Evision Wireless Receiver 320f:226f
From: Terry Junge @ 2025-11-20 1:49 UTC (permalink / raw)
To: Jiri Kosina, Benjamin Tissoires, Artem; +Cc: linux-input, linux-kernel
In-Reply-To: <b627cfc9-0dda-4b59-ae5f-83f40cf6088b@cosmicgizmosystems.com>
Update help message for Evision driver
Add #define for Evision device ID 0x226f
Add report_fixup hook to Evision driver
The mouse portion of the device's Report Descriptor declares 5 buttons
but only declares 3 usages (Button 1 through Button 3).
As a result events for the 2 side buttons are not generated.
Detect and repair the Report Descriptor if necessary by changing the
Usage Maximum value from Button 3 to Button 5.
Reported-by: Artem <temabiill@gmail.com>
Closes: https://lore.kernel.org/all/CADYkRmrfhRf6VmQjc+su+mepyv=TsHc+aMcL6ryRZ5HTZ8pyFg@mail.gmail.com/
Signed-off-by: Terry Junge <linuxhid@cosmicgizmosystems.com>
---
drivers/hid/Kconfig | 1 +
drivers/hid/hid-evision.c | 21 +++++++++++++++++++++
drivers/hid/hid-ids.h | 1 +
3 files changed, 23 insertions(+)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 04420a713be0..30c4f79535fe 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -383,6 +383,7 @@ config HID_EVISION
help
Support for some EVision keyboards. Note that this is needed only when
applying customization using userspace programs.
+ Support for some EVision devices requiring report descriptor fixups.
config HID_EZKEY
tristate "Ezkey BTC 8193 keyboard"
diff --git a/drivers/hid/hid-evision.c b/drivers/hid/hid-evision.c
index bb5997078491..3e7f43ab80bb 100644
--- a/drivers/hid/hid-evision.c
+++ b/drivers/hid/hid-evision.c
@@ -18,6 +18,10 @@ static int evision_input_mapping(struct hid_device *hdev, struct hid_input *hi,
struct hid_field *field, struct hid_usage *usage,
unsigned long **bit, int *max)
{
+ /* mapping only applies to USB_DEVICE_ID_EVISION_ICL01 */
+ if (hdev->product != USB_DEVICE_ID_EVISION_ICL01)
+ return 0;
+
if ((usage->hid & HID_USAGE_PAGE) != HID_UP_CONSUMER)
return 0;
@@ -37,8 +41,24 @@ static int evision_input_mapping(struct hid_device *hdev, struct hid_input *hi,
return 0;
}
+#define REP_DSC_SIZE 236
+#define USAGE_MAX_INDEX 59
+
+static const __u8 *evision_report_fixup(struct hid_device *hdev, __u8 *rdesc,
+ unsigned int *rsize)
+{
+ if (hdev->product == USB_DEVICE_ID_EV_TELINK_RECEIVER &&
+ *rsize == REP_DSC_SIZE && rdesc[USAGE_MAX_INDEX] == 0x29 &&
+ rdesc[USAGE_MAX_INDEX + 1] == 3) {
+ hid_info(hdev, "fixing EVision:TeLink Receiver report descriptor\n");
+ rdesc[USAGE_MAX_INDEX + 1] = 5; // change usage max from 3 to 5
+ }
+ return rdesc;
+}
+
static const struct hid_device_id evision_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_EVISION, USB_DEVICE_ID_EVISION_ICL01) },
+ { HID_USB_DEVICE(USB_VENDOR_ID_EVISION, USB_DEVICE_ID_EV_TELINK_RECEIVER) },
{ }
};
MODULE_DEVICE_TABLE(hid, evision_devices);
@@ -47,6 +67,7 @@ static struct hid_driver evision_driver = {
.name = "evision",
.id_table = evision_devices,
.input_mapping = evision_input_mapping,
+ .report_fixup = evision_report_fixup,
};
module_hid_driver(evision_driver);
diff --git a/drivers/hid/hid-ids.h b/drivers/hid/hid-ids.h
index 0723b4b1c9ec..c9e67b768bc7 100644
--- a/drivers/hid/hid-ids.h
+++ b/drivers/hid/hid-ids.h
@@ -476,6 +476,7 @@
#define USB_DEVICE_ID_EMS_TRIO_LINKER_PLUS_II 0x0118
#define USB_VENDOR_ID_EVISION 0x320f
+#define USB_DEVICE_ID_EV_TELINK_RECEIVER 0x226f
#define USB_DEVICE_ID_EVISION_ICL01 0x5041
#define USB_VENDOR_ID_FFBEAST 0x045b
base-commit: 2953fb65481b262514ac13f24ffbc70eeace68c6
--
2.43.0
^ permalink raw reply related
* Re: [PATCH 1/2 v2] HID: input: map HID_GD_Z to ABS_DISTANCE for stylus/pen
From: Ping Cheng @ 2025-11-20 0:13 UTC (permalink / raw)
To: linux-input, Benjamin Tissoires
Cc: benjamin.tissoires, jikos, Dmitry.Torokhov, stable, Ping Cheng
In-Reply-To: <20251027203742.23740-1-ping.cheng@wacom.com>
Hi Benjamin and Jiri,
Can one of you review and merge the patchset?
This patch was from me. The second patch for selftest followed
Benjamin's suggestion
Cheers,
Ping.
On Mon, Oct 27, 2025 at 1:38 PM Ping Cheng <pinglinux@gmail.com> wrote:
>
> HID_GD_Z is mapped to ABS_Z for stylus and pen in hid-input.c. But HID_GD_Z
> should be used to report ABS_DISTANCE for stylus and pen as described at:
> Documentation/input/event-codes.rst#n226
>
> * ABS_DISTANCE:
>
> - Used to describe the distance of a tool from an interaction surface. This
> event should only be emitted while the tool is hovering, meaning in close
> proximity of the device and while the value of the BTN_TOUCH code is 0. If
> the input device may be used freely in three dimensions, consider ABS_Z
> instead.
> - BTN_TOOL_<name> should be set to 1 when the tool comes into detectable
> proximity and set to 0 when the tool leaves detectable proximity.
> BTN_TOOL_<name> signals the type of tool that is currently detected by the
> hardware and is otherwise independent of ABS_DISTANCE and/or BTN_TOUCH.
>
> This patch makes the correct mapping. The ABS_DISTANCE is currently not mapped
> by any HID usage in hid-generic driver.
>
> Signed-off-by: Ping Cheng <ping.cheng@wacom.com>
> Cc: stable@kernel.org
> ---
> v2: updated pen/stylus checks
> ---
> drivers/hid/hid-input.c | 18 +++++++++++++++++-
> 1 file changed, 17 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/hid/hid-input.c b/drivers/hid/hid-input.c
> index f45f856a127f..141359de5ce5 100644
> --- a/drivers/hid/hid-input.c
> +++ b/drivers/hid/hid-input.c
> @@ -860,7 +860,7 @@ static void hidinput_configure_usage(struct hid_input *hidinput, struct hid_fiel
>
> switch (usage->hid) {
> /* These usage IDs map directly to the usage codes. */
> - case HID_GD_X: case HID_GD_Y: case HID_GD_Z:
> + case HID_GD_X: case HID_GD_Y:
> case HID_GD_RX: case HID_GD_RY: case HID_GD_RZ:
> if (field->flags & HID_MAIN_ITEM_RELATIVE)
> map_rel(usage->hid & 0xf);
> @@ -868,6 +868,22 @@ static void hidinput_configure_usage(struct hid_input *hidinput, struct hid_fiel
> map_abs_clear(usage->hid & 0xf);
> break;
>
> + case HID_GD_Z:
> + /* HID_GD_Z is mapped to ABS_DISTANCE for stylus/pen */
> + if (field->flags & HID_MAIN_ITEM_RELATIVE) {
> + map_rel(usage->hid & 0xf);
> + } else {
> + if (field->application == HID_DG_PEN ||
> + field->physical == HID_DG_PEN ||
> + field->logical == HID_DG_STYLUS ||
> + field->physical == HID_DG_STYLUS ||
> + field->application == HID_DG_DIGITIZER)
> + map_abs_clear(ABS_DISTANCE);
> + else
> + map_abs_clear(usage->hid & 0xf);
> + }
> + break;
> +
> case HID_GD_WHEEL:
> if (field->flags & HID_MAIN_ITEM_RELATIVE) {
> set_bit(REL_WHEEL, input->relbit);
> --
> 2.51.0
>
^ permalink raw reply
* Re: [PATCH 1/2] hid: hid-pl: handle probe errors
From: Jiri Kosina @ 2025-11-19 20:51 UTC (permalink / raw)
To: Oliver Neukum; +Cc: bentiss, linux-input, stable
In-Reply-To: <20251119090957.1936249-1-oneukum@suse.com>
On Wed, 19 Nov 2025, Oliver Neukum wrote:
> Errors in init must be reported back or we'll
> follow a NULL pointer the first time FF is used.
>
> Fixes: 20eb127906709 ("hid: force feedback driver for PantherLord USB/PS2 2in1 Adapter")
> Cc: <stable@vger.kernel.org>
> Signed-off-by: Oliver Neukum <oneukum@suse.com>
> ---
> drivers/hid/hid-pl.c | 7 ++++++-
> 1 file changed, 6 insertions(+), 1 deletion(-)
>
> diff --git a/drivers/hid/hid-pl.c b/drivers/hid/hid-pl.c
> index 3c8827081dea..dc11d5322fc0 100644
> --- a/drivers/hid/hid-pl.c
> +++ b/drivers/hid/hid-pl.c
> @@ -194,9 +194,14 @@ static int pl_probe(struct hid_device *hdev, const struct hid_device_id *id)
> goto err;
> }
>
> - plff_init(hdev);
> + ret = plff_init(hdev);
> + if (ret)
> + goto stop;
>
> return 0;
> +
> +stop:
> + hid_hw_stop(hdev);
> err:
Thanks for the patch, makes sense and I'm going to apply it shortly.
Where is 2/2 though? I don't see it anywhere.
--
Jiri Kosina
SUSE Labs
^ permalink raw reply
* Re: [PATCH] input: touchscreen: Add ilitek touchscreen driver support
From: kernel test robot @ 2025-11-19 18:48 UTC (permalink / raw)
To: 2724853925, Dmitry Torokhov, Henrik Rydberg
Cc: oe-kbuild-all, linux-input, linux-kernel, linux-gpio, 2724853925
In-Reply-To: <tencent_995E6FC62EDBC1EED14E6052847F270F6406@qq.com>
Hi,
kernel test robot noticed the following build errors:
[auto build test ERROR on dtor-input/next]
[also build test ERROR on dtor-input/for-linus linus/master v6.18-rc6 next-20251119]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]
url: https://github.com/intel-lab-lkp/linux/commits/2724853925-qq-com/input-touchscreen-Add-ilitek-touchscreen-driver-support/20251116-215220
base: https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git next
patch link: https://lore.kernel.org/r/tencent_995E6FC62EDBC1EED14E6052847F270F6406%40qq.com
patch subject: [PATCH] input: touchscreen: Add ilitek touchscreen driver support
config: um-randconfig-r053-20251119 (https://download.01.org/0day-ci/archive/20251120/202511200123.RGbEPLxi-lkp@intel.com/config)
compiler: clang version 22.0.0git (https://github.com/llvm/llvm-project 0bba1e76581bad04e7d7f09f5115ae5e2989e0d9)
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20251120/202511200123.RGbEPLxi-lkp@intel.com/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202511200123.RGbEPLxi-lkp@intel.com/
All errors (new ones prefixed by >>):
/usr/bin/ld: warning: .tmp_vmlinux1 has a LOAD segment with RWX permissions
/usr/bin/ld: drivers/input/touchscreen/ilitek/ilitek_main.o: in function `ilitek_udp_reply':
>> include/linux/skbuff.h:1336:(.text+0x1643): undefined reference to `__alloc_skb'
/usr/bin/ld: drivers/input/touchscreen/ilitek/ilitek_main.o: in function `ilitek_udp_reply':
>> include/net/netlink.h:1001:(.text+0x167f): undefined reference to `__nlmsg_put'
/usr/bin/ld: drivers/input/touchscreen/ilitek/ilitek_main.o: in function `ilitek_udp_reply':
>> drivers/input/touchscreen/ilitek/ilitek_main.c:83:(.text+0x16b9): undefined reference to `netlink_unicast'
/usr/bin/ld: drivers/input/touchscreen/ilitek/ilitek_main.o: in function `ilitek_udp_reply':
>> include/linux/skbuff.h:1275:(.text+0x1900): undefined reference to `sk_skb_reason_drop'
/usr/bin/ld: drivers/input/touchscreen/ilitek/ilitek_main.o: in function `ilitek_netlink_init':
>> drivers/input/touchscreen/ilitek/ilitek_main.c:1833:(.text+0x3458): undefined reference to `netlink_kernel_release'
/usr/bin/ld: drivers/input/touchscreen/ilitek/ilitek_main.o: in function `ilitek_netlink_init':
>> include/linux/netlink.h:62:(.text+0x3475): undefined reference to `init_net'
>> /usr/bin/ld: include/linux/netlink.h:62:(.text+0x347a): undefined reference to `__netlink_kernel_create'
/usr/bin/ld: drivers/input/touchscreen/ilitek/ilitek_main.o: in function `ilitek_netlink_exit':
drivers/input/touchscreen/ilitek/ilitek_main.c:1833:(.text+0x4d15): undefined reference to `netlink_kernel_release'
/usr/bin/ld: drivers/input/touchscreen/ilitek/ilitek_main.o: in function `ilitek_main_remove':
drivers/input/touchscreen/ilitek/ilitek_main.c:1833:(.text+0x71e3): undefined reference to `netlink_kernel_release'
clang: error: linker command failed with exit code 1 (use -v to see invocation)
vim +83 drivers/input/touchscreen/ilitek/ilitek_main.c
53
54 static void __maybe_unused ilitek_udp_reply(void *payload, int size)
55 {
56 #ifdef ILITEK_TUNING_MESSAGE
57 struct sk_buff *skb;
58 struct nlmsghdr *nlh;
59 int len = NLMSG_SPACE(size);
60 int ret;
61 int pid = ilitek_pid, seq = ilitek_seq;
62
63 TP_DBG(NULL, "[%s] ilitek_debug_flag: %d\n", __func__, ilitek_debug_flag);
64 if (!ilitek_debug_flag)
65 return;
66
67 skb = alloc_skb(len, GFP_ATOMIC);
68 if (!skb) {
69 TP_ERR(NULL, "alloc skb error\n");
70 return;
71 }
72
73 nlh = nlmsg_put(skb, pid, seq, 0, size, 0);
74 if (!nlh)
75 goto nlmsg_failure;
76
77 nlh->nlmsg_flags = 0;
78 memcpy(NLMSG_DATA(nlh), payload, size);
79
80 NETLINK_CB(skb).portid = 0; /* from kernel */
81 NETLINK_CB(skb).dst_group = 0; /* unicast */
82
> 83 ret = netlink_unicast(ilitek_netlink_sock, skb, pid, MSG_DONTWAIT);
84 if (ret < 0)
85 TP_ERR(NULL, "ilitek send failed, ret: %d\n", ret);
86 return;
87
88 nlmsg_failure:
89 kfree_skb(skb);
90
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* [PATCH v2] Input: pixcir_i2c_ts - add support for one-time total calibration
From: Michal Vokáč @ 2025-11-19 17:51 UTC (permalink / raw)
To: Dmitry Torokhov
Cc: linux-input, linux-kernel, Fabio Estevam, Michal Vokáč
The Pixcir Tango controller has support for a one-time total calibration
(manual calibration) procedure. Its purpose is to measure the capacitance
offsets of the electrode system and to store these values into EEPROM.
During normal operation this calibration data is subtracted from the values
measured. This calibration should be necessary only once in the product
lifetime. It should be performed as part of the final adjustment after
the panel is mounted in the product.
Add support for the calibration with sysfs interface.
Signed-off-by: Michal Vokáč <michal.vokac@ysoft.com>
---
changes in v2:
- Removed redundant lock from calibrate_store().
drivers/input/touchscreen/pixcir_i2c_ts.c | 26 +++++++++++++++++++++++
1 file changed, 26 insertions(+)
diff --git a/drivers/input/touchscreen/pixcir_i2c_ts.c b/drivers/input/touchscreen/pixcir_i2c_ts.c
index dad5786e82a4..e52ec8d8e392 100644
--- a/drivers/input/touchscreen/pixcir_i2c_ts.c
+++ b/drivers/input/touchscreen/pixcir_i2c_ts.c
@@ -24,6 +24,7 @@
*/
#define PIXCIR_REG_POWER_MODE 51
#define PIXCIR_REG_INT_MODE 52
+#define PIXCIR_REG_SPECOP 58
/*
* Power modes:
@@ -462,6 +463,30 @@ static int pixcir_i2c_ts_resume(struct device *dev)
static DEFINE_SIMPLE_DEV_PM_OPS(pixcir_dev_pm_ops,
pixcir_i2c_ts_suspend, pixcir_i2c_ts_resume);
+static ssize_t calibrate_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct pixcir_i2c_ts_data *ts = i2c_get_clientdata(client);
+ static const u8 cmd = 0x03;
+ int error;
+
+ error = i2c_smbus_write_byte_data(ts->client, PIXCIR_REG_SPECOP, cmd);
+ if (error)
+ dev_err(dev, "calibrate command failed: %d\n", error);
+
+ return error ?: count;
+}
+
+static DEVICE_ATTR_WO(calibrate);
+
+static struct attribute *pixcir_i2c_ts_attrs[] = {
+ &dev_attr_calibrate.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(pixcir_i2c_ts);
+
static int pixcir_i2c_ts_probe(struct i2c_client *client)
{
const struct i2c_device_id *id = i2c_client_get_device_id(client);
@@ -600,6 +625,7 @@ MODULE_DEVICE_TABLE(of, pixcir_of_match);
static struct i2c_driver pixcir_i2c_ts_driver = {
.driver = {
.name = "pixcir_ts",
+ .dev_groups = pixcir_i2c_ts_groups,
.pm = pm_sleep_ptr(&pixcir_dev_pm_ops),
.of_match_table = of_match_ptr(pixcir_of_match),
},
--
2.43.0
^ permalink raw reply related
* [PATCH] input: Add marine keycodes for radar control.
From: Hunter Moore @ 2025-11-19 16:38 UTC (permalink / raw)
To: dmitry.torokhov; +Cc: Hunter.Moore, linux-input, linux-kernel
In-Reply-To: <2rtgkpu7dzfxrnmepwxmxmemiqmmbx4fl6ow336f6spjdgqj7k@fxgfc26vzzkk>
Hi Dmitry,
It may help if I provide some context of what we are intending on doing.
We are currently updating some of our radar capabilities. One of the goals
necessitates that our radar devices can be controlled by dedicated hardware
controls. You can see this requirement documented in this publicly available
IMO standards document, section 6.1.3[1].
We would like to create as open of a platform as possible to allow 3rd party
manufacturers to create radar control panels. The additional keys are industry
standard functions amoung other commonly available radar control panels, and
not specific to proprietary features. Since radars are important for user
awareness while navigating, we also want to ensure that 3rd party input is
clearly defined.
> No, we will not be adding these new keys since I do not see any users of
> the previously defined ones anywhere, not in kernel sources and not in
> the HID specification.
> You seem to be creating a purpose-built devices where you control your
> userspace, and I do not think the new keycodes will be of any use to
> anyone but your specific application.
We currently allow 3rd party input devices to use the previously defined keys
in our marine ecosystem. These inputs can come from physical user input
devices, networked input devices, or from other applications that run
alongside our application.
> You are also unlikely to be
> running anything else besides the software that you are developing on
> these devices, so I'd recommend simply reuse parts of the existing key
> code space for your purposes (KEY_MACRO*, BTN_TRIGGER_HAPPY*, etc).
The documentation currently states “The KEY_MACRO# codes MUST also NOT be used
as fallback for when no existing KEY_FOO define matches the marking / purpose.
In this case a new KEY_FOO define MUST be added“.
As such, we are apprehensive to repurpose the existing generic keys, for fear
of their purpose changing or unintended user behavior with 3rd party input
devices. If we are unable to get dedicated key codes added, do you have a
suggestion for how to correctly deal with this problem?
[1]: https://wwwcdn.imo.org/localresources/en/KnowledgeCentre/IndexofIMOResolutions/MSCResolutions/MSC.192(79).pdf
________________________________
CONFIDENTIALITY NOTICE: This email and any attachments are for the sole use of the intended recipient(s) and contain information that may be Garmin confidential and/or Garmin legally privileged. If you have received this email in error, please notify the sender by reply email and delete the message. Any disclosure, copying, distribution or use of this communication (including attachments) by someone other than the intended recipient is prohibited. Thank you.
________________________________
CONFIDENTIALITY NOTICE: This email and any attachments are for the sole use of the intended recipient(s) and contain information that may be Garmin confidential and/or Garmin legally privileged. If you have received this email in error, please notify the sender by reply email and delete the message. Any disclosure, copying, distribution or use of this communication (including attachments) by someone other than the intended recipient is prohibited. Thank you.
^ permalink raw reply
* Re: (subset) [PATCH v3 00/11] arm64: dts: add description for solidrun imx8mp hummingboard-iiot
From: Neil Armstrong @ 2025-11-19 16:49 UTC (permalink / raw)
To: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Shawn Guo,
Jessica Zhang, David Airlie, Simona Vetter, Maarten Lankhorst,
Maxime Ripard, Thomas Zimmermann, Dmitry Torokhov, Sascha Hauer,
Pengutronix Kernel Team, Fabio Estevam, Laurent Pinchart,
Lad Prabhakar, Thierry Reding, Josua Mayer
Cc: Jon Nettleton, Mikhail Anikin, Yazan Shhady, devicetree,
linux-kernel, dri-devel, linux-input, imx, linux-arm-kernel,
Krzysztof Kozlowski, Conor Dooley
In-Reply-To: <20251117-imx8mp-hb-iiot-v3-0-bf1a4cf5fa8e@solid-run.com>
Hi,
On Mon, 17 Nov 2025 13:28:42 +0100, Josua Mayer wrote:
> This patchset mainl adds description for 3 SolidRun boards:
> - i.MX8MP Hummingboard IIoT
> - SolidSense N8 Compact
> - i.MX8MM Hummingboard Ripple
>
> This includes dt bindings and a range of bug-fixes:
>
> [...]
Thanks, Applied to https://gitlab.freedesktop.org/drm/misc/kernel.git (drm-misc-next)
[02/11] dt-bindings: display: panel: ronbo,rb070d30: panel-common ref
https://gitlab.freedesktop.org/drm/misc/kernel/-/commit/779640415ea28abb60bb7641d50f1d9cccaa4d73
[03/11] dt-bindings: panel: lvds: add Winstar WF70A8SYJHLNGA
https://gitlab.freedesktop.org/drm/misc/kernel/-/commit/72539c4b9e482f397cc9340bcd97bb7643c0692d
[05/11] drm/panel: ronbo-rb070d30: fix warning with gpio controllers that sleep
https://gitlab.freedesktop.org/drm/misc/kernel/-/commit/93f5a0dc2b7c2df71a93e0fc59edae474bb84c6f
--
Neil
^ permalink raw reply
* [PATCH v3 1/1] HID: input: Add support for multiple batteries per device
From: Lucas Zampieri @ 2025-11-19 14:30 UTC (permalink / raw)
To: linux-input
Cc: Lucas Zampieri, linux-kernel, Jiri Kosina, Benjamin Tissoires,
Sebastian Reichel, Bastien Nocera, linux-pm
In-Reply-To: <20251119143005.1513531-1-lzampier@redhat.com>
Introduce struct hid_battery to encapsulate individual battery state and
enable HID devices to register multiple batteries, each identified by
its report ID.
This change adds struct hid_battery with all battery-related fields and
replaces the legacy dev->battery_* fields with a batteries list. All
memory management is converted to use devm_* for simpler cleanup.
Batteries are named using their report ID with the pattern
hid-{uniq}-battery-{report_id}. External drivers hid-apple and
hid-magicmouse are updated to use the new battery API via the
hid_get_first_battery() helper, and hid-input-test is updated for the
new battery structure.
This enables proper battery reporting for devices with multiple
batteries such as split keyboards, gaming headsets with charging docks,
and wireless earbuds with per-earbud batteries.
Signed-off-by: Lucas Zampieri <lzampier@redhat.com>
---
drivers/hid/hid-apple.c | 10 +-
drivers/hid/hid-core.c | 4 +
drivers/hid/hid-input-test.c | 39 +++----
drivers/hid/hid-input.c | 191 +++++++++++++++++++----------------
drivers/hid/hid-magicmouse.c | 10 +-
include/linux/hid.h | 54 +++++++---
6 files changed, 182 insertions(+), 126 deletions(-)
diff --git a/drivers/hid/hid-apple.c b/drivers/hid/hid-apple.c
index 61404d7a43ee..fb09b616f8cc 100644
--- a/drivers/hid/hid-apple.c
+++ b/drivers/hid/hid-apple.c
@@ -618,17 +618,19 @@ static int apple_fetch_battery(struct hid_device *hdev)
struct apple_sc *asc = hid_get_drvdata(hdev);
struct hid_report_enum *report_enum;
struct hid_report *report;
+ struct hid_battery *bat;
- if (!(asc->quirks & APPLE_RDESC_BATTERY) || !hdev->battery)
+ bat = hid_get_first_battery(hdev);
+ if (!(asc->quirks & APPLE_RDESC_BATTERY) || !bat)
return -1;
- report_enum = &hdev->report_enum[hdev->battery_report_type];
- report = report_enum->report_id_hash[hdev->battery_report_id];
+ report_enum = &hdev->report_enum[bat->report_type];
+ report = report_enum->report_id_hash[bat->report_id];
if (!report || report->maxfield < 1)
return -1;
- if (hdev->battery_capacity == hdev->battery_max)
+ if (bat->capacity == bat->max)
return -1;
hid_hw_request(hdev, report, HID_REQ_GET_REPORT);
diff --git a/drivers/hid/hid-core.c b/drivers/hid/hid-core.c
index a5b3a8ca2fcb..76d628547e9a 100644
--- a/drivers/hid/hid-core.c
+++ b/drivers/hid/hid-core.c
@@ -2990,6 +2990,10 @@ struct hid_device *hid_allocate_device(void)
mutex_init(&hdev->ll_open_lock);
kref_init(&hdev->ref);
+#ifdef CONFIG_HID_BATTERY_STRENGTH
+ INIT_LIST_HEAD(&hdev->batteries);
+#endif
+
ret = hid_bpf_device_init(hdev);
if (ret)
goto out_err;
diff --git a/drivers/hid/hid-input-test.c b/drivers/hid/hid-input-test.c
index 6f5c71660d82..c92008dafddf 100644
--- a/drivers/hid/hid-input-test.c
+++ b/drivers/hid/hid-input-test.c
@@ -9,54 +9,59 @@
static void hid_test_input_update_battery_charge_status(struct kunit *test)
{
- struct hid_device *dev;
+ struct hid_battery *bat;
bool handled;
- dev = kunit_kzalloc(test, sizeof(*dev), GFP_KERNEL);
- KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
+ bat = kunit_kzalloc(test, sizeof(*bat), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bat);
- handled = hidinput_update_battery_charge_status(dev, HID_DG_HEIGHT, 0);
+ handled = hidinput_update_battery_charge_status(bat, HID_DG_HEIGHT, 0);
KUNIT_EXPECT_FALSE(test, handled);
- KUNIT_EXPECT_EQ(test, dev->battery_charge_status, POWER_SUPPLY_STATUS_UNKNOWN);
+ KUNIT_EXPECT_EQ(test, bat->charge_status, POWER_SUPPLY_STATUS_UNKNOWN);
- handled = hidinput_update_battery_charge_status(dev, HID_BAT_CHARGING, 0);
+ handled = hidinput_update_battery_charge_status(bat, HID_BAT_CHARGING, 0);
KUNIT_EXPECT_TRUE(test, handled);
- KUNIT_EXPECT_EQ(test, dev->battery_charge_status, POWER_SUPPLY_STATUS_DISCHARGING);
+ KUNIT_EXPECT_EQ(test, bat->charge_status, POWER_SUPPLY_STATUS_DISCHARGING);
- handled = hidinput_update_battery_charge_status(dev, HID_BAT_CHARGING, 1);
+ handled = hidinput_update_battery_charge_status(bat, HID_BAT_CHARGING, 1);
KUNIT_EXPECT_TRUE(test, handled);
- KUNIT_EXPECT_EQ(test, dev->battery_charge_status, POWER_SUPPLY_STATUS_CHARGING);
+ KUNIT_EXPECT_EQ(test, bat->charge_status, POWER_SUPPLY_STATUS_CHARGING);
}
static void hid_test_input_get_battery_property(struct kunit *test)
{
struct power_supply *psy;
+ struct hid_battery *bat;
struct hid_device *dev;
union power_supply_propval val;
int ret;
dev = kunit_kzalloc(test, sizeof(*dev), GFP_KERNEL);
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
- dev->battery_avoid_query = true;
+
+ bat = kunit_kzalloc(test, sizeof(*bat), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bat);
+ bat->dev = dev;
+ bat->avoid_query = true;
psy = kunit_kzalloc(test, sizeof(*psy), GFP_KERNEL);
KUNIT_ASSERT_NOT_ERR_OR_NULL(test, psy);
- psy->drv_data = dev;
+ psy->drv_data = bat;
- dev->battery_status = HID_BATTERY_UNKNOWN;
- dev->battery_charge_status = POWER_SUPPLY_STATUS_CHARGING;
+ bat->status = HID_BATTERY_UNKNOWN;
+ bat->charge_status = POWER_SUPPLY_STATUS_CHARGING;
ret = hidinput_get_battery_property(psy, POWER_SUPPLY_PROP_STATUS, &val);
KUNIT_EXPECT_EQ(test, ret, 0);
KUNIT_EXPECT_EQ(test, val.intval, POWER_SUPPLY_STATUS_UNKNOWN);
- dev->battery_status = HID_BATTERY_REPORTED;
- dev->battery_charge_status = POWER_SUPPLY_STATUS_CHARGING;
+ bat->status = HID_BATTERY_REPORTED;
+ bat->charge_status = POWER_SUPPLY_STATUS_CHARGING;
ret = hidinput_get_battery_property(psy, POWER_SUPPLY_PROP_STATUS, &val);
KUNIT_EXPECT_EQ(test, ret, 0);
KUNIT_EXPECT_EQ(test, val.intval, POWER_SUPPLY_STATUS_CHARGING);
- dev->battery_status = HID_BATTERY_REPORTED;
- dev->battery_charge_status = POWER_SUPPLY_STATUS_DISCHARGING;
+ bat->status = HID_BATTERY_REPORTED;
+ bat->charge_status = POWER_SUPPLY_STATUS_DISCHARGING;
ret = hidinput_get_battery_property(psy, POWER_SUPPLY_PROP_STATUS, &val);
KUNIT_EXPECT_EQ(test, ret, 0);
KUNIT_EXPECT_EQ(test, val.intval, POWER_SUPPLY_STATUS_DISCHARGING);
diff --git a/drivers/hid/hid-input.c b/drivers/hid/hid-input.c
index e56e7de53279..08b889ecaf7f 100644
--- a/drivers/hid/hid-input.c
+++ b/drivers/hid/hid-input.c
@@ -418,18 +418,18 @@ static unsigned find_battery_quirk(struct hid_device *hdev)
return quirks;
}
-static int hidinput_scale_battery_capacity(struct hid_device *dev,
+static int hidinput_scale_battery_capacity(struct hid_battery *bat,
int value)
{
- if (dev->battery_min < dev->battery_max &&
- value >= dev->battery_min && value <= dev->battery_max)
- value = ((value - dev->battery_min) * 100) /
- (dev->battery_max - dev->battery_min);
+ if (bat->min < bat->max &&
+ value >= bat->min && value <= bat->max)
+ value = ((value - bat->min) * 100) /
+ (bat->max - bat->min);
return value;
}
-static int hidinput_query_battery_capacity(struct hid_device *dev)
+static int hidinput_query_battery_capacity(struct hid_battery *bat)
{
u8 *buf;
int ret;
@@ -438,14 +438,14 @@ static int hidinput_query_battery_capacity(struct hid_device *dev)
if (!buf)
return -ENOMEM;
- ret = hid_hw_raw_request(dev, dev->battery_report_id, buf, 4,
- dev->battery_report_type, HID_REQ_GET_REPORT);
+ ret = hid_hw_raw_request(bat->dev, bat->report_id, buf, 4,
+ bat->report_type, HID_REQ_GET_REPORT);
if (ret < 2) {
kfree(buf);
return -ENODATA;
}
- ret = hidinput_scale_battery_capacity(dev, buf[1]);
+ ret = hidinput_scale_battery_capacity(bat, buf[1]);
kfree(buf);
return ret;
}
@@ -454,7 +454,8 @@ static int hidinput_get_battery_property(struct power_supply *psy,
enum power_supply_property prop,
union power_supply_propval *val)
{
- struct hid_device *dev = power_supply_get_drvdata(psy);
+ struct hid_battery *bat = power_supply_get_drvdata(psy);
+ struct hid_device *dev = bat->dev;
int value;
int ret = 0;
@@ -465,13 +466,13 @@ static int hidinput_get_battery_property(struct power_supply *psy,
break;
case POWER_SUPPLY_PROP_CAPACITY:
- if (dev->battery_status != HID_BATTERY_REPORTED &&
- !dev->battery_avoid_query) {
- value = hidinput_query_battery_capacity(dev);
+ if (bat->status != HID_BATTERY_REPORTED &&
+ !bat->avoid_query) {
+ value = hidinput_query_battery_capacity(bat);
if (value < 0)
return value;
} else {
- value = dev->battery_capacity;
+ value = bat->capacity;
}
val->intval = value;
@@ -482,20 +483,20 @@ static int hidinput_get_battery_property(struct power_supply *psy,
break;
case POWER_SUPPLY_PROP_STATUS:
- if (dev->battery_status != HID_BATTERY_REPORTED &&
- !dev->battery_avoid_query) {
- value = hidinput_query_battery_capacity(dev);
+ if (bat->status != HID_BATTERY_REPORTED &&
+ !bat->avoid_query) {
+ value = hidinput_query_battery_capacity(bat);
if (value < 0)
return value;
- dev->battery_capacity = value;
- dev->battery_status = HID_BATTERY_QUERIED;
+ bat->capacity = value;
+ bat->status = HID_BATTERY_QUERIED;
}
- if (dev->battery_status == HID_BATTERY_UNKNOWN)
+ if (bat->status == HID_BATTERY_UNKNOWN)
val->intval = POWER_SUPPLY_STATUS_UNKNOWN;
else
- val->intval = dev->battery_charge_status;
+ val->intval = bat->charge_status;
break;
case POWER_SUPPLY_PROP_SCOPE:
@@ -510,37 +511,54 @@ static int hidinput_get_battery_property(struct power_supply *psy,
return ret;
}
+static struct hid_battery *hidinput_find_battery(struct hid_device *dev,
+ int report_id)
+{
+ struct hid_battery *bat;
+
+ list_for_each_entry(bat, &dev->batteries, list) {
+ if (bat->report_id == report_id)
+ return bat;
+ }
+ return NULL;
+}
+
static int hidinput_setup_battery(struct hid_device *dev, unsigned report_type,
struct hid_field *field, bool is_percentage)
{
+ struct hid_battery *bat;
struct power_supply_desc *psy_desc;
- struct power_supply_config psy_cfg = { .drv_data = dev, };
+ struct power_supply_config psy_cfg = { 0 };
unsigned quirks;
s32 min, max;
- int error;
- if (dev->battery)
- return 0; /* already initialized? */
+ if (hidinput_find_battery(dev, field->report->id))
+ return 0; /* already initialized */
quirks = find_battery_quirk(dev);
- hid_dbg(dev, "device %x:%x:%x %d quirks %d\n",
- dev->bus, dev->vendor, dev->product, dev->version, quirks);
+ hid_dbg(dev, "device %x:%x:%x %d quirks %d report_id %d\n",
+ dev->bus, dev->vendor, dev->product, dev->version, quirks,
+ field->report->id);
if (quirks & HID_BATTERY_QUIRK_IGNORE)
return 0;
- psy_desc = kzalloc(sizeof(*psy_desc), GFP_KERNEL);
+ bat = devm_kzalloc(&dev->dev, sizeof(*bat), GFP_KERNEL);
+ if (!bat)
+ return -ENOMEM;
+
+ psy_desc = devm_kzalloc(&dev->dev, sizeof(*psy_desc), GFP_KERNEL);
if (!psy_desc)
return -ENOMEM;
- psy_desc->name = kasprintf(GFP_KERNEL, "hid-%s-battery",
- strlen(dev->uniq) ?
- dev->uniq : dev_name(&dev->dev));
- if (!psy_desc->name) {
- error = -ENOMEM;
- goto err_free_mem;
- }
+ psy_desc->name = devm_kasprintf(&dev->dev, GFP_KERNEL,
+ "hid-%s-battery-%d",
+ strlen(dev->uniq) ?
+ dev->uniq : dev_name(&dev->dev),
+ field->report->id);
+ if (!psy_desc->name)
+ return -ENOMEM;
psy_desc->type = POWER_SUPPLY_TYPE_BATTERY;
psy_desc->properties = hidinput_battery_props;
@@ -559,98 +577,89 @@ static int hidinput_setup_battery(struct hid_device *dev, unsigned report_type,
if (quirks & HID_BATTERY_QUIRK_FEATURE)
report_type = HID_FEATURE_REPORT;
- dev->battery_min = min;
- dev->battery_max = max;
- dev->battery_report_type = report_type;
- dev->battery_report_id = field->report->id;
- dev->battery_charge_status = POWER_SUPPLY_STATUS_DISCHARGING;
+ bat->dev = dev;
+ bat->min = min;
+ bat->max = max;
+ bat->report_type = report_type;
+ bat->report_id = field->report->id;
+ bat->charge_status = POWER_SUPPLY_STATUS_DISCHARGING;
+ bat->status = HID_BATTERY_UNKNOWN;
/*
* Stylus is normally not connected to the device and thus we
* can't query the device and get meaningful battery strength.
* We have to wait for the device to report it on its own.
*/
- dev->battery_avoid_query = report_type == HID_INPUT_REPORT &&
- field->physical == HID_DG_STYLUS;
+ bat->avoid_query = report_type == HID_INPUT_REPORT &&
+ field->physical == HID_DG_STYLUS;
if (quirks & HID_BATTERY_QUIRK_AVOID_QUERY)
- dev->battery_avoid_query = true;
-
- dev->battery = power_supply_register(&dev->dev, psy_desc, &psy_cfg);
- if (IS_ERR(dev->battery)) {
- error = PTR_ERR(dev->battery);
- hid_warn(dev, "can't register power supply: %d\n", error);
- goto err_free_name;
+ bat->avoid_query = true;
+
+ psy_cfg.drv_data = bat;
+ bat->ps = devm_power_supply_register(&dev->dev, psy_desc, &psy_cfg);
+ if (IS_ERR(bat->ps)) {
+ hid_warn(dev, "can't register power supply: %ld\n",
+ PTR_ERR(bat->ps));
+ return PTR_ERR(bat->ps);
}
- power_supply_powers(dev->battery, &dev->dev);
- return 0;
+ power_supply_powers(bat->ps, &dev->dev);
+
+ list_add_tail(&bat->list, &dev->batteries);
-err_free_name:
- kfree(psy_desc->name);
-err_free_mem:
- kfree(psy_desc);
- dev->battery = NULL;
- return error;
+ return 0;
}
static void hidinput_cleanup_battery(struct hid_device *dev)
{
- const struct power_supply_desc *psy_desc;
-
- if (!dev->battery)
- return;
+ struct hid_battery *bat, *next;
- psy_desc = dev->battery->desc;
- power_supply_unregister(dev->battery);
- kfree(psy_desc->name);
- kfree(psy_desc);
- dev->battery = NULL;
+ list_for_each_entry_safe(bat, next, &dev->batteries, list) {
+ list_del(&bat->list);
+ }
}
-static bool hidinput_update_battery_charge_status(struct hid_device *dev,
+static bool hidinput_update_battery_charge_status(struct hid_battery *bat,
unsigned int usage, int value)
{
switch (usage) {
case HID_BAT_CHARGING:
- dev->battery_charge_status = value ?
- POWER_SUPPLY_STATUS_CHARGING :
- POWER_SUPPLY_STATUS_DISCHARGING;
+ bat->charge_status = value ?
+ POWER_SUPPLY_STATUS_CHARGING :
+ POWER_SUPPLY_STATUS_DISCHARGING;
return true;
}
return false;
}
-static void hidinput_update_battery(struct hid_device *dev, unsigned int usage,
- int value)
+static void hidinput_update_battery(struct hid_battery *bat,
+ unsigned int usage, int value)
{
int capacity;
- if (!dev->battery)
- return;
-
- if (hidinput_update_battery_charge_status(dev, usage, value)) {
- power_supply_changed(dev->battery);
+ if (hidinput_update_battery_charge_status(bat, usage, value)) {
+ power_supply_changed(bat->ps);
return;
}
if ((usage & HID_USAGE_PAGE) == HID_UP_DIGITIZER && value == 0)
return;
- if (value < dev->battery_min || value > dev->battery_max)
+ if (value < bat->min || value > bat->max)
return;
- capacity = hidinput_scale_battery_capacity(dev, value);
+ capacity = hidinput_scale_battery_capacity(bat, value);
- if (dev->battery_status != HID_BATTERY_REPORTED ||
- capacity != dev->battery_capacity ||
- ktime_after(ktime_get_coarse(), dev->battery_ratelimit_time)) {
- dev->battery_capacity = capacity;
- dev->battery_status = HID_BATTERY_REPORTED;
- dev->battery_ratelimit_time =
+ if (bat->status != HID_BATTERY_REPORTED ||
+ capacity != bat->capacity ||
+ ktime_after(ktime_get_coarse(), bat->ratelimit_time)) {
+ bat->capacity = capacity;
+ bat->status = HID_BATTERY_REPORTED;
+ bat->ratelimit_time =
ktime_add_ms(ktime_get_coarse(), 30 * 1000);
- power_supply_changed(dev->battery);
+ power_supply_changed(bat->ps);
}
}
#else /* !CONFIG_HID_BATTERY_STRENGTH */
@@ -664,9 +673,10 @@ static void hidinput_cleanup_battery(struct hid_device *dev)
{
}
-static void hidinput_update_battery(struct hid_device *dev, unsigned int usage,
- int value)
+static struct hid_battery *hidinput_find_battery(struct hid_device *dev,
+ int report_id)
{
+ return NULL;
}
#endif /* CONFIG_HID_BATTERY_STRENGTH */
@@ -1533,7 +1543,10 @@ void hidinput_hid_event(struct hid_device *hid, struct hid_field *field, struct
return;
if (usage->type == EV_PWR) {
- hidinput_update_battery(hid, usage->hid, value);
+ struct hid_battery *bat = hidinput_find_battery(hid, report->id);
+
+ if (bat)
+ hidinput_update_battery(bat, usage->hid, value);
return;
}
diff --git a/drivers/hid/hid-magicmouse.c b/drivers/hid/hid-magicmouse.c
index 7d4a25c6de0e..b495f7a4bc6c 100644
--- a/drivers/hid/hid-magicmouse.c
+++ b/drivers/hid/hid-magicmouse.c
@@ -812,19 +812,21 @@ static int magicmouse_fetch_battery(struct hid_device *hdev)
#ifdef CONFIG_HID_BATTERY_STRENGTH
struct hid_report_enum *report_enum;
struct hid_report *report;
+ struct hid_battery *bat;
- if (!hdev->battery ||
+ bat = hid_get_first_battery(hdev);
+ if (!bat ||
(!is_usb_magicmouse2(hdev->vendor, hdev->product) &&
!is_usb_magictrackpad2(hdev->vendor, hdev->product)))
return -1;
- report_enum = &hdev->report_enum[hdev->battery_report_type];
- report = report_enum->report_id_hash[hdev->battery_report_id];
+ report_enum = &hdev->report_enum[bat->report_type];
+ report = report_enum->report_id_hash[bat->report_id];
if (!report || report->maxfield < 1)
return -1;
- if (hdev->battery_capacity == hdev->battery_max)
+ if (bat->capacity == bat->max)
return -1;
hid_hw_request(hdev, report, HID_REQ_GET_REPORT);
diff --git a/include/linux/hid.h b/include/linux/hid.h
index a4ddb94e3ee5..3e33ef74c3c1 100644
--- a/include/linux/hid.h
+++ b/include/linux/hid.h
@@ -634,6 +634,36 @@ enum hid_battery_status {
HID_BATTERY_REPORTED, /* Device sent unsolicited battery strength report */
};
+/**
+ * struct hid_battery - represents a single battery power supply
+ * @dev: pointer to the parent hid_device
+ * @ps: the power supply instance
+ * @min: minimum battery value from HID descriptor
+ * @max: maximum battery value from HID descriptor
+ * @report_type: HID report type (input/feature)
+ * @report_id: HID report ID for this battery
+ * @charge_status: current charging status
+ * @status: battery reporting status
+ * @capacity: current battery capacity (0-100)
+ * @avoid_query: if true, avoid querying battery (e.g., for stylus)
+ * @ratelimit_time: rate limiting for battery reports
+ * @list: list node for linking into hid_device's battery list
+ */
+struct hid_battery {
+ struct hid_device *dev;
+ struct power_supply *ps;
+ __s32 min;
+ __s32 max;
+ __s32 report_type;
+ __s32 report_id;
+ __s32 charge_status;
+ enum hid_battery_status status;
+ __s32 capacity;
+ bool avoid_query;
+ ktime_t ratelimit_time;
+ struct list_head list;
+};
+
struct hid_driver;
struct hid_ll_driver;
@@ -670,19 +700,10 @@ struct hid_device {
#ifdef CONFIG_HID_BATTERY_STRENGTH
/*
* Power supply information for HID devices which report
- * battery strength. power_supply was successfully registered if
- * battery is non-NULL.
+ * battery strength. Each battery is tracked separately in the
+ * batteries list.
*/
- struct power_supply *battery;
- __s32 battery_capacity;
- __s32 battery_min;
- __s32 battery_max;
- __s32 battery_report_type;
- __s32 battery_report_id;
- __s32 battery_charge_status;
- enum hid_battery_status battery_status;
- bool battery_avoid_query;
- ktime_t battery_ratelimit_time;
+ struct list_head batteries;
#endif
unsigned long status; /* see STAT flags above */
@@ -743,6 +764,15 @@ static inline void hid_set_drvdata(struct hid_device *hdev, void *data)
dev_set_drvdata(&hdev->dev, data);
}
+#ifdef CONFIG_HID_BATTERY_STRENGTH
+static inline struct hid_battery *hid_get_first_battery(struct hid_device *hdev)
+{
+ if (list_empty(&hdev->batteries))
+ return NULL;
+ return list_first_entry(&hdev->batteries, struct hid_battery, list);
+}
+#endif
+
#define HID_GLOBAL_STACK_SIZE 4
#define HID_COLLECTION_STACK_SIZE 4
--
2.51.1
^ permalink raw reply related
* [PATCH v3 0/1] HID: Add support for multiple batteries per device
From: Lucas Zampieri @ 2025-11-19 14:30 UTC (permalink / raw)
To: linux-input
Cc: Lucas Zampieri, linux-kernel, Jiri Kosina, Benjamin Tissoires,
Sebastian Reichel, Bastien Nocera, linux-pm
This series adds support for HID devices with multiple batteries.
Currently, the HID battery reporting subsystem only supports one battery per
device. There are several devices with multiple batteries that would benefit
from this support:
- Gaming headsets with batteries in both the headset and charging dock
- Wireless earbuds with per-earbud batteries plus charging case
- Split keyboards with per-side batteries
## Proposed Solution
This series introduces struct hid_battery to encapsulate individual battery
state, replaces the old battery fields with a list-based approach, and adds
support for multiple batteries tracked within struct hid_device. Batteries
are identified by report ID. The implementation is fully backwards compatible
with single-battery devices through a helper function.
## Testing
Tested with split keyboard hardware (Dactyl 5x6) using custom ZMK firmware
that implements per-side HID battery reporting. Each battery (left and right
keyboard halves) reports independently through the power supply interface with
distinct report IDs (0x05 and 0x06).
Test firmware available on my personal fork at:
https://github.com/zampierilucas/zmk/tree/feat/individual-hid-battery-reporting
If this series gets merged, these changes will be proposed to upstream ZMK.
HID descriptor and recording captured with hid-recorder:
D: 0
R: 162 05 01 09 06 a1 01 85 01 05 07 19 e0 29 e7 15 00 25 01 75 01 95 08 81 02 05 07 75 08 95 01 81 03 05 07 15 00 25 01 19 00 29 67 75 01 95 68 81 02 c0 05 0c 09 01 a1 01 85 02 05 0c 15 00 26 ff 0f 19 00 2a ff 0f 75 10 95 06 81 00 c0 05 84 09 05 a1 01 05 85 85 05 09 44 15 00 25 01 35 00 45 01 75 08 95 01 81 02 09 65 15 00 25 64 35 00 45 64 75 08 95 01 81 02 c0 05 84 09 05 a1 01 05 85 85 06 09 44 15 00 25 01 35 00 45 01 75 08 95 01 81 02 09 65 15 00 25 64 35 00 45 64 75 08 95 01 81 02 c0
N: ZMK Project Dactyl 5x6
P: usb-0000:2d:00.3-4.2/input2
I: 3 1d50 615e
D: 0
E: 0.000000 3 05 00 56
E: 0.000977 3 05 00 56
E: 1.490974 3 06 00 52
E: 1.491958 3 06 00 52
E: 6.492979 3 06 00 53
E: 6.493962 3 06 00 53
The recording shows both batteries reporting with different charge levels
(Report ID 05: 86%, Report ID 06: 82%-83%), demonstrating the multi-battery
functionality. This can be used to verify UPower compatibility.
## Future Work: Userspace Integration
As suggested by Bastien, semantic battery differentiation (e.g., "left
earbud" vs "right earbud") requires userspace coordination, as HID
reports typically lack role metadata.
This will require:
1. systemd/hwdb entries for device-specific battery role mappings
2. UPower updates to enumerate and group multi-battery devices
3. Desktop environment changes to display batteries with meaningful labels
This kernel infrastructure is a prerequisite for that userspace work.
Lucas Zampieri (1):
HID: input: Add support for multiple batteries per device
Signed-off-by: Lucas Zampieri <lzampier@redhat.com>
Changes in v3:
- Squashed the three v2 patches into a single patch as suggested by
Benjamin
- Removed all legacy dev->battery_* fields, using list-based storage only
- Changed battery naming to include report ID: hid-{uniq}-battery-{report_id}
- Converted battery memory management to devm_* for automatic cleanup
- Updated hidinput_update_battery() to take struct hid_battery directly
- Added hid_get_first_battery() helper for external driver compatibility
- Updated hid-apple.c and hid-magicmouse.c to use new battery API
- Simplified cover letter based on feedback
Changes in v2:
- Split the monolithic v1 patch into three logical patches for easier review:
1. Introduce struct hid_battery (pure structure addition)
2. Refactor existing code to use the new structure (internal changes)
3. Add multi-battery support (new functionality)
- Added detailed testing section with hardware specifics
- Added hid-recorder output (dactyl-hid-recording.txt) demonstrating two-battery
HID descriptor for UPower validation
- Added "Future Work: Userspace Integration" section addressing Bastien's feedback
about semantic battery differentiation
- Added hardware examples with product links to commit messages (per Bastien's
suggestion)
- No functional changes from v1, only improved patch organization and documentation
drivers/hid/hid-apple.c | 10 +-
drivers/hid/hid-core.c | 4 +
drivers/hid/hid-input-test.c | 39 +++----
drivers/hid/hid-input.c | 191 +++++++++++++++++++----------------
drivers/hid/hid-magicmouse.c | 10 +-
include/linux/hid.h | 54 +++++++---
6 files changed, 182 insertions(+), 126 deletions(-)
--
2.51.1
^ permalink raw reply
* Re: [PATCH v3 1/4] firmware_loader: expand firmware error codes with up-to-date error
From: Konstantin Ryabitsev @ 2025-11-19 13:58 UTC (permalink / raw)
To: Marco Felsch
Cc: Russ Weight, Rob Herring, Conor Dooley, Henrik Rydberg,
Kamel Bouhara, Rafael J. Wysocki, devicetree, Greg Kroah-Hartman,
Dmitry Torokhov, linux-kernel, Luis Chamberlain, Marco Felsch,
linux-input, Andrew Morton, Krzysztof Kozlowski, Danilo Krummrich
In-Reply-To: <20251119101007.za2373biybt24qfs@pengutronix.de>
On Wed, Nov 19, 2025 at 11:10:07AM +0100, Marco Felsch wrote:
> > Marco - I would recommend adding the Reviewed-by tags that you
> > have received and then resubmitting the patch.
>
> I can do this albeit I thought this will be collected autom. by b4.
You have to run `b4 trailers -u` for it to be collected.
-K
^ permalink raw reply
* Re: [PATCH] Input: pixcir_i2c_ts - add support for one-time total calibration
From: Michal Vokáč @ 2025-11-19 10:23 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: linux-input, linux-kernel, Fabio Estevam
In-Reply-To: <5uyos6zu74jfro7zsfup4zbkrywf5odi4ytfuwuttslgrus2of@fmopwef7fkme>
Hi Dmitry,
On 18. 11. 25 20:26, Dmitry Torokhov wrote:
> Hi Michal,
>
> On Wed, Nov 12, 2025 at 02:00:19PM +0100, Michal Vokáč wrote:
>> The Pixcir Tango controller has support for a one-time total calibration
>> (manual calibration) procedure. Its purpose is to measure the capacitance
>> offsets of the electrode system and to store these values into EEPROM.
>>
>> During normal operation this calibration data is subtracted from the values
>> measured. This calibration should be necessary only once in the product
>> lifetime. It should be performed as part of the final adjustment after
>> the panel is mounted in the product.
>>
>> Add support for the calibration with sysfs interface.
>>
>> Signed-off-by: Michal Vokáč <michal.vokac@ysoft.com>
>> ---
>> drivers/input/touchscreen/pixcir_i2c_ts.c | 34 +++++++++++++++++++++++
>> 1 file changed, 34 insertions(+)
>>
>> diff --git a/drivers/input/touchscreen/pixcir_i2c_ts.c b/drivers/input/touchscreen/pixcir_i2c_ts.c
>> index dad5786e82a4..2215e56b1458 100644
>> --- a/drivers/input/touchscreen/pixcir_i2c_ts.c
>> +++ b/drivers/input/touchscreen/pixcir_i2c_ts.c
>> @@ -24,6 +24,7 @@
>> */
>> #define PIXCIR_REG_POWER_MODE 51
>> #define PIXCIR_REG_INT_MODE 52
>> +#define PIXCIR_REG_SPECOP 58
>>
>> /*
>> * Power modes:
>> @@ -82,6 +83,7 @@ struct pixcir_i2c_ts_data {
>> const struct pixcir_i2c_chip_data *chip;
>> struct touchscreen_properties prop;
>> bool running;
>> + struct mutex sysfs_mutex;
>> };
>>
>> struct pixcir_report_data {
>> @@ -462,6 +464,35 @@ static int pixcir_i2c_ts_resume(struct device *dev)
>> static DEFINE_SIMPLE_DEV_PM_OPS(pixcir_dev_pm_ops,
>> pixcir_i2c_ts_suspend, pixcir_i2c_ts_resume);
>>
>> +static ssize_t calibrate_store(struct device *dev,
>> + struct device_attribute *attr,
>> + const char *buf, size_t count)
>> +{
>> + struct i2c_client *client = to_i2c_client(dev);
>> + struct pixcir_i2c_ts_data *ts = i2c_get_clientdata(client);
>> + static const u8 cmd = 0x03;
>> + int error;
>> +
>> + error = mutex_lock_interruptible(&ts->sysfs_mutex);
>> + if (error)
>> + return error;
>
> Why do we need this mutex? i2c_smbus_write_byte_data() does take adapter
> lock, why do we need this additional locking?
Honestly I was not sure about usefulness of the lock.
I originally have not it there when the patch was in our downstream tree.
When I was rewriting it for mainline I realized there are other touchscreen
drivers that already have this calibration feature implemented and that
they have the lock in place. See raydium_i2c_ts.c or elants_i2c.c.
So I got inspired and used it as well for the case I missed something.
Now after a second look at the mentioned drivers I see that these also
have a sysfs interface for FW update. So it make sense to use the lock
to assure the whole fw transfer is finished before someone else can
access the device.
That is not our case. The mutex can safely be removed. I will send v2.
Thank you,
Michal
^ permalink raw reply
* Re: [PATCH v3 1/4] firmware_loader: expand firmware error codes with up-to-date error
From: Marco Felsch @ 2025-11-19 10:10 UTC (permalink / raw)
To: Russ Weight
Cc: Rob Herring, Conor Dooley, Henrik Rydberg, Kamel Bouhara,
Rafael J. Wysocki, devicetree, Greg Kroah-Hartman,
Dmitry Torokhov, linux-kernel, Luis Chamberlain, Marco Felsch,
linux-input, Andrew Morton, Krzysztof Kozlowski, Danilo Krummrich
In-Reply-To: <juffz52dxb2txvolv7d3kb37urweg3kau67rb3zk42ovn4uze2@uqvbyz6nuecn>
On 25-11-18, Russ Weight wrote:
> On Tue, Nov 11, 2025 at 12:01:10PM +0100, Marco Felsch wrote:
> > On 25-10-16, Marco Felsch wrote:
> > > Hi all,
> > >
> > > On 25-09-20, Dmitry Torokhov wrote:
> > > > On Wed, Aug 27, 2025 at 03:29:33PM -0600, Russ Weight wrote:
> > > > >
> > > > > On Thu, Aug 21, 2025 at 07:26:36PM +0200, Marco Felsch wrote:
> > > > > > Add FW_UPLOAD_ERR_DUPLICATE to allow drivers to inform the firmware_loader
> > > > > > framework that the update is not required. This can be the case if the
> > > > > > user provided firmware matches the current running firmware.
> > > > > >
> > > > > > Sync lib/test_firmware.c accordingly.
> > > > > >
> > > > > > Signed-off-by: Marco Felsch <m.felsch@pengutronix.de>
> > > > >
> > > > > Reviewed-by: Russ Weight <russ.weight@linux.dev>
> > > >
> > > > Does this mean I should merge this through input tree?
> > >
> > > may I ask how this is planned to go further?
> >
> > Gentle ping.
>
> Marco - I would recommend adding the Reviewed-by tags that you
> have received and then resubmitting the patch.
I can do this albeit I thought this will be collected autom. by b4.
Regards,
Marco
> -- Russ
>
> >
> > Regards,
> > Marco
> >
> >
> > >
> > > Regards,
> > > Marco
> > >
> > > >
> > > > Thanks.
> > > >
> > > > --
> > > > Dmitry
> >
> > --
> > #gernperDu
> > #CallMeByMyFirstName
> >
> > Pengutronix e.K. | |
> > Steuerwalder Str. 21 | https://www.pengutronix.de/ |
> > 31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
> > Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-9 |
>
>
--
#gernperDu
#CallMeByMyFirstName
Pengutronix e.K. | |
Steuerwalder Str. 21 | https://www.pengutronix.de/ |
31137 Hildesheim, Germany | Phone: +49-5121-206917-0 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-9 |
^ permalink raw reply
* [dtor-input:for-linus] BUILD SUCCESS ae8966b7b5bd69b86209cc34bcca1ba9f18b68e6
From: kernel test robot @ 2025-11-19 9:50 UTC (permalink / raw)
To: Dmitry Torokhov; +Cc: linux-input
tree/branch: https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git for-linus
branch HEAD: ae8966b7b5bd69b86209cc34bcca1ba9f18b68e6 Input: rename INPUT_PROP_HAPTIC_TOUCHPAD to INPUT_PROP_PRESSUREPAD
elapsed time: 1546m
configs tested: 108
configs skipped: 3
The following configs have been built successfully.
More configs may be tested in the coming days.
tested configs:
alpha alldefconfig gcc-15.1.0
alpha allnoconfig gcc-15.1.0
arc allnoconfig gcc-15.1.0
arc haps_hs_defconfig gcc-15.1.0
arc randconfig-001-20251118 gcc-14.3.0
arc randconfig-002-20251118 gcc-15.1.0
arm allnoconfig clang-22
arm moxart_defconfig gcc-15.1.0
arm randconfig-001-20251118 gcc-8.5.0
arm randconfig-002-20251118 gcc-10.5.0
arm randconfig-003-20251118 clang-22
arm randconfig-004-20251118 clang-22
arm realview_defconfig clang-16
arm64 allnoconfig gcc-15.1.0
arm64 randconfig-001-20251119 clang-22
arm64 randconfig-002-20251119 gcc-8.5.0
arm64 randconfig-003-20251119 clang-22
arm64 randconfig-004-20251119 clang-22
csky allnoconfig gcc-15.1.0
csky randconfig-001-20251119 gcc-15.1.0
csky randconfig-002-20251119 gcc-10.5.0
hexagon allnoconfig clang-22
hexagon randconfig-001-20251119 clang-22
hexagon randconfig-002-20251119 clang-22
i386 allnoconfig gcc-14
i386 buildonly-randconfig-001-20251119 gcc-14
i386 buildonly-randconfig-002-20251119 clang-20
i386 buildonly-randconfig-003-20251119 clang-20
i386 buildonly-randconfig-004-20251119 clang-20
i386 buildonly-randconfig-005-20251119 gcc-14
i386 buildonly-randconfig-006-20251119 gcc-14
i386 randconfig-007-20251119 gcc-14
i386 randconfig-011-20251119 clang-20
i386 randconfig-012-20251119 clang-20
i386 randconfig-013-20251119 clang-20
i386 randconfig-014-20251119 gcc-14
i386 randconfig-015-20251119 gcc-14
i386 randconfig-016-20251119 clang-20
i386 randconfig-017-20251119 clang-20
loongarch allnoconfig clang-22
loongarch defconfig clang-19
loongarch randconfig-001-20251119 clang-22
loongarch randconfig-002-20251119 gcc-14.3.0
m68k allnoconfig gcc-15.1.0
m68k apollo_defconfig gcc-15.1.0
m68k defconfig gcc-15.1.0
microblaze allnoconfig gcc-15.1.0
microblaze defconfig gcc-15.1.0
mips allnoconfig gcc-15.1.0
nios2 allnoconfig gcc-11.5.0
nios2 defconfig gcc-11.5.0
nios2 randconfig-001-20251119 gcc-11.5.0
nios2 randconfig-002-20251119 gcc-10.5.0
openrisc allnoconfig gcc-15.1.0
openrisc defconfig gcc-15.1.0
parisc allnoconfig gcc-15.1.0
parisc defconfig gcc-15.1.0
parisc randconfig-001-20251119 gcc-8.5.0
parisc randconfig-002-20251119 gcc-8.5.0
parisc64 defconfig gcc-15.1.0
powerpc allnoconfig gcc-15.1.0
powerpc amigaone_defconfig gcc-15.1.0
powerpc randconfig-001-20251119 clang-16
powerpc randconfig-002-20251119 clang-22
powerpc64 randconfig-001-20251119 clang-19
riscv allnoconfig gcc-15.1.0
riscv defconfig clang-22
riscv randconfig-001-20251119 gcc-15.1.0
riscv randconfig-002-20251119 gcc-10.5.0
s390 allnoconfig clang-22
s390 debug_defconfig gcc-15.1.0
s390 defconfig clang-22
s390 randconfig-001-20251119 gcc-8.5.0
s390 randconfig-002-20251119 clang-22
sh allnoconfig gcc-15.1.0
sh defconfig gcc-15.1.0
sh randconfig-001-20251119 gcc-11.5.0
sh randconfig-002-20251119 gcc-9.5.0
sparc allnoconfig gcc-15.1.0
sparc defconfig gcc-15.1.0
sparc randconfig-001-20251119 gcc-15.1.0
sparc randconfig-002-20251119 gcc-15.1.0
sparc64 defconfig clang-20
sparc64 randconfig-001-20251119 gcc-15.1.0
sparc64 randconfig-002-20251119 clang-22
um allnoconfig clang-22
um defconfig clang-22
um i386_defconfig gcc-14
um randconfig-001-20251119 clang-16
um randconfig-002-20251119 gcc-14
um x86_64_defconfig clang-22
x86_64 allnoconfig clang-20
x86_64 buildonly-randconfig-001-20251119 gcc-14
x86_64 buildonly-randconfig-002-20251119 gcc-14
x86_64 buildonly-randconfig-003-20251119 clang-20
x86_64 buildonly-randconfig-004-20251119 clang-20
x86_64 buildonly-randconfig-005-20251119 gcc-14
x86_64 buildonly-randconfig-006-20251119 gcc-12
x86_64 defconfig gcc-14
x86_64 randconfig-071-20251119 gcc-14
x86_64 randconfig-072-20251119 clang-20
x86_64 randconfig-073-20251119 gcc-12
x86_64 randconfig-074-20251119 clang-20
x86_64 randconfig-075-20251119 clang-20
x86_64 randconfig-076-20251119 gcc-14
xtensa allnoconfig gcc-15.1.0
xtensa randconfig-001-20251119 gcc-8.5.0
xtensa randconfig-002-20251119 gcc-11.5.0
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* [PATCH 1/2] hid: hid-pl: handle probe errors
From: Oliver Neukum @ 2025-11-19 9:09 UTC (permalink / raw)
To: jikos, bentiss, linux-input; +Cc: Oliver Neukum, stable
Errors in init must be reported back or we'll
follow a NULL pointer the first time FF is used.
Fixes: 20eb127906709 ("hid: force feedback driver for PantherLord USB/PS2 2in1 Adapter")
Cc: <stable@vger.kernel.org>
Signed-off-by: Oliver Neukum <oneukum@suse.com>
---
drivers/hid/hid-pl.c | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-pl.c b/drivers/hid/hid-pl.c
index 3c8827081dea..dc11d5322fc0 100644
--- a/drivers/hid/hid-pl.c
+++ b/drivers/hid/hid-pl.c
@@ -194,9 +194,14 @@ static int pl_probe(struct hid_device *hdev, const struct hid_device_id *id)
goto err;
}
- plff_init(hdev);
+ ret = plff_init(hdev);
+ if (ret)
+ goto stop;
return 0;
+
+stop:
+ hid_hw_stop(hdev);
err:
return ret;
}
--
2.51.1
^ permalink raw reply related
* Re: [PATCH v8 04/10] HID: asus: prevent binding to all HID devices on ROG
From: Ilpo Järvinen @ 2025-11-19 8:00 UTC (permalink / raw)
To: Antheas Kapenekakis
Cc: platform-driver-x86, linux-input, LKML, Jiri Kosina,
Benjamin Tissoires, Corentin Chary, Luke D . Jones, Hans de Goede,
Denis Benato
In-Reply-To: <20251101104712.8011-5-lkml@antheas.dev>
On Sat, 1 Nov 2025, Antheas Kapenekakis wrote:
> Currently, when hid-asus is not loaded, NKEY keyboards load as ~6
> event devices with a pretty ASUSTEK name. When it loads, it concatenates
> all applications per HID endpoint, renames them, and prints errors
> when some of them do not have an input device.
>
> Therefore, change probe so that this is no longer the case. Stop
Using a standalone "this" should be generally avoided in changelog text,
it's very often too ambiguous what it refers to (including in this case).
--
i.
> renaming the devices, omit the check for .input which causes errors
> on e.g., the Z13 for some hiddev only devices, and move RGB checks
> into probe.
>
> Reviewed-by: Luke D. Jones <luke@ljones.dev>
> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> ---
> drivers/hid/hid-asus.c | 52 ++++++++++++++++++++++++++++--------------
> 1 file changed, 35 insertions(+), 17 deletions(-)
>
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 03f0d86936fc..726f5d8e22d1 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
> @@ -47,6 +47,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
> #define T100CHI_MOUSE_REPORT_ID 0x06
> #define FEATURE_REPORT_ID 0x0d
> #define INPUT_REPORT_ID 0x5d
> +#define HID_USAGE_PAGE_VENDOR 0xff310000
> #define FEATURE_KBD_REPORT_ID 0x5a
> #define FEATURE_KBD_REPORT_SIZE 64
> #define FEATURE_KBD_LED_REPORT_ID1 0x5d
> @@ -89,6 +90,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
> #define QUIRK_ROG_NKEY_KEYBOARD BIT(11)
> #define QUIRK_ROG_CLAYMORE_II_KEYBOARD BIT(12)
> #define QUIRK_ROG_ALLY_XPAD BIT(13)
> +#define QUIRK_SKIP_REPORT_FIXUP BIT(14)
>
> #define I2C_KEYBOARD_QUIRKS (QUIRK_FIX_NOTEBOOK_REPORT | \
> QUIRK_NO_INIT_REPORTS | \
> @@ -125,7 +127,6 @@ struct asus_drvdata {
> struct input_dev *tp_kbd_input;
> struct asus_kbd_leds *kbd_backlight;
> const struct asus_touchpad_info *tp;
> - bool enable_backlight;
> struct power_supply *battery;
> struct power_supply_desc battery_desc;
> int battery_capacity;
> @@ -316,7 +317,7 @@ static int asus_e1239t_event(struct asus_drvdata *drvdat, u8 *data, int size)
> static int asus_event(struct hid_device *hdev, struct hid_field *field,
> struct hid_usage *usage, __s32 value)
> {
> - if ((usage->hid & HID_USAGE_PAGE) == 0xff310000 &&
> + if ((usage->hid & HID_USAGE_PAGE) == HID_USAGE_PAGE_VENDOR &&
> (usage->hid & HID_USAGE) != 0x00 &&
> (usage->hid & HID_USAGE) != 0xff && !usage->type) {
> hid_warn(hdev, "Unmapped Asus vendor usagepage code 0x%02x\n",
> @@ -931,11 +932,6 @@ static int asus_input_configured(struct hid_device *hdev, struct hid_input *hi)
>
> drvdata->input = input;
>
> - if (drvdata->enable_backlight &&
> - !asus_kbd_wmi_led_control_present(hdev) &&
> - asus_kbd_register_leds(hdev))
> - hid_warn(hdev, "Failed to initialize backlight.\n");
> -
> return 0;
> }
>
> @@ -1008,15 +1004,6 @@ static int asus_input_mapping(struct hid_device *hdev,
> return -1;
> }
>
> - /*
> - * Check and enable backlight only on devices with UsagePage ==
> - * 0xff31 to avoid initializing the keyboard firmware multiple
> - * times on devices with multiple HID descriptors but same
> - * PID/VID.
> - */
> - if (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT)
> - drvdata->enable_backlight = true;
> -
> set_bit(EV_REP, hi->input->evbit);
> return 1;
> }
> @@ -1133,8 +1120,10 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev)
>
> static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
> {
> - int ret;
> + struct hid_report_enum *rep_enum;
> struct asus_drvdata *drvdata;
> + struct hid_report *rep;
> + int ret, is_vendor = 0;
>
> drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
> if (drvdata == NULL) {
> @@ -1218,12 +1207,37 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
> return ret;
> }
>
> + /* Check for vendor for RGB init and handle generic devices properly. */
> + rep_enum = &hdev->report_enum[HID_INPUT_REPORT];
> + list_for_each_entry(rep, &rep_enum->report_list, list) {
> + if ((rep->application & HID_USAGE_PAGE) == HID_USAGE_PAGE_VENDOR)
> + is_vendor = true;
> + }
> +
> + /*
> + * For ROG keyboards, disable fixups except vendor devices.
> + */
> + if (drvdata->quirks & QUIRK_ROG_NKEY_KEYBOARD && !is_vendor)
> + drvdata->quirks |= QUIRK_SKIP_REPORT_FIXUP;
> +
> ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
> if (ret) {
> hid_err(hdev, "Asus hw start failed: %d\n", ret);
> return ret;
> }
>
> + if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) &&
> + !asus_kbd_wmi_led_control_present(hdev) &&
> + asus_kbd_register_leds(hdev))
> + hid_warn(hdev, "Failed to initialize backlight.\n");
> +
> + /*
> + * For ROG keyboards, skip rename for consistency and ->input check as
> + * some devices do not have inputs.
> + */
> + if (drvdata->quirks & QUIRK_ROG_NKEY_KEYBOARD)
> + return 0;
> +
> /*
> * Check that input registration succeeded. Checking that
> * HID_CLAIMED_INPUT is set prevents a UAF when all input devices
> @@ -1352,6 +1366,10 @@ static const __u8 *asus_report_fixup(struct hid_device *hdev, __u8 *rdesc,
> rdesc = new_rdesc;
> }
>
> + /* Vendor fixups should only apply to NKEY vendor devices. */
> + if (drvdata->quirks & QUIRK_SKIP_REPORT_FIXUP)
> + return rdesc;
> +
> if (drvdata->quirks & QUIRK_ROG_NKEY_KEYBOARD &&
> *rsize == 331 && rdesc[190] == 0x85 && rdesc[191] == 0x5a &&
> rdesc[204] == 0x95 && rdesc[205] == 0x05) {
>
^ permalink raw reply
* Re: [PATCH v8 05/10] HID: asus: initialize LED endpoint early for old NKEY keyboards
From: Ilpo Järvinen @ 2025-11-19 7:55 UTC (permalink / raw)
To: Denis Benato
Cc: Antheas Kapenekakis, platform-driver-x86, linux-input, LKML,
Jiri Kosina, Benjamin Tissoires, Corentin Chary, Luke D . Jones,
Hans de Goede
In-Reply-To: <b505ffca-63a1-4c52-b940-cdfc507813fc@gmail.com>
[-- Attachment #1: Type: text/plain, Size: 4733 bytes --]
On Wed, 19 Nov 2025, Denis Benato wrote:
> On 11/18/25 13:10, Ilpo Järvinen wrote:
> > On Sat, 1 Nov 2025, Antheas Kapenekakis wrote:
> >
> >> These keyboards have always had initialization in the kernel for 0x5d.
> >> At this point, it is hard to verify again and we risk regressions by
> >> removing this. Therefore, initialize with 0x5d as well.
> See patch 1: unless I missed something you can retain the two
> FEATURE_KBD_LED_REPORT_IDx behind the same exact quirk:
> why are we adding a quirk to replace a quirk that was removed
> in patch 1?
>
> You are basically doing the pretty-much-but-not-quite
> equivalent of what the driver was doing before.
> >> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> >> ---
> >> drivers/hid/hid-asus.c | 15 +++++++++++++--
> >> 1 file changed, 13 insertions(+), 2 deletions(-)
> >>
> >> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> >> index 726f5d8e22d1..221c7195e885 100644
> >> --- a/drivers/hid/hid-asus.c
> >> +++ b/drivers/hid/hid-asus.c
> >> @@ -91,6 +91,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
> >> #define QUIRK_ROG_CLAYMORE_II_KEYBOARD BIT(12)
> >> #define QUIRK_ROG_ALLY_XPAD BIT(13)
> >> #define QUIRK_SKIP_REPORT_FIXUP BIT(14)
> >> +#define QUIRK_ROG_NKEY_LEGACY BIT(15)
> >>
> >> #define I2C_KEYBOARD_QUIRKS (QUIRK_FIX_NOTEBOOK_REPORT | \
> >> QUIRK_NO_INIT_REPORTS | \
> >> @@ -669,6 +670,16 @@ static int asus_kbd_register_leds(struct hid_device *hdev)
> >> if (ret < 0)
> >> return ret;
> >>
> >> + if (drvdata->quirks & QUIRK_ROG_NKEY_LEGACY) {
> >> + /*
> >> + * These keyboards might need 0x5d for shortcuts to work.
> >> + * As it has been more than 5 years, it is hard to verify.
> >> + */
> >> + ret = asus_kbd_init(hdev, FEATURE_KBD_LED_REPORT_ID1);
> >> + if (ret < 0)
> >> + return ret;
> >> + }
> >> +
> >> /* Get keyboard functions */
> >> ret = asus_kbd_get_functions(hdev, &kbd_func, FEATURE_KBD_REPORT_ID);
> >> if (ret < 0)
> >> @@ -1409,10 +1420,10 @@ static const struct hid_device_id asus_devices[] = {
> >> QUIRK_USE_KBD_BACKLIGHT },
> >> { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
> >> USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD),
> >> - QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD },
> >> + QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD | QUIRK_ROG_NKEY_LEGACY },
> >> { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
> >> USB_DEVICE_ID_ASUSTEK_ROG_NKEY_KEYBOARD2),
> >> - QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD },
> >> + QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD | QUIRK_ROG_NKEY_LEGACY },
> >> { HID_USB_DEVICE(USB_VENDOR_ID_ASUSTEK,
> >> USB_DEVICE_ID_ASUSTEK_ROG_Z13_LIGHTBAR),
> >> QUIRK_USE_KBD_BACKLIGHT | QUIRK_ROG_NKEY_KEYBOARD },
> > You should do FEATURE_KBD_LED_REPORT_ID1 refactoring together, not remove
> > + add back in different patches.
> Granted I still have no idea why that was removed in the first place?
> Then re-added but losing FEATURE_KBD_LED_REPORT_ID1 ?
Did you mean losing ID2 not ID1 as I don't understand what you meant
otherwise?
And my suggestion was to not "remove [it] in the first place". In a reply
to patch 1, Antheas seemed to be agreeable to not remove it first and also
to not remove ID2 but instead introduce the quirk earlier in the series.
> What's the problem with FEATURE_KBD_LED_REPORT_ID1?
>
> > I suppose the cleanest would be to add a new patch as first which moves
> > asus_kbd_init() outside of if/else so you can make this refactoring of
> > FEATURE_KBD_LED_REPORT_ID1 in the 2nd patch.
>
> Again I am missing the point in moving these...
Antheas wants to consolidate the asus_kbd_register_leds() if/else
branches. That consolidation requires "moving" code one level up
indentation-wise. The series is just misordered currently (in v8 and
before) so that code is first removed and then reintroduced later,
whereas correct approach to order the series would ensure there are
no intermediate step within series that (can) result in lacking something.
My understanding is that this ordering problem is going to be corrected in
v9.
> > I note there's still contention with this series overall.
> >
> There are a few things that have pretty much the potential of making
> some laptops act funny due to tinkering with initializations commands.
>
> The rename will break some tools, but other than that, granted I have yet
> to check the rest of the patchset, looks reasonable to me.
>
> Perhaps I am not entirely happy with how things are worded in
> a few instances, but it's a minor issue.
I'd prefer we address nits as well.
--
i.
^ permalink raw reply
* Re: [PATCH v8 03/10] HID: asus: fortify keyboard handshake
From: Ilpo Järvinen @ 2025-11-19 7:22 UTC (permalink / raw)
To: Denis Benato
Cc: Antheas Kapenekakis, platform-driver-x86, linux-input, LKML,
Jiri Kosina, Benjamin Tissoires, Corentin Chary, Luke D . Jones,
Hans de Goede
In-Reply-To: <75fe4c0f-3303-4f3d-adc5-45487df3c80a@gmail.com>
On Wed, 19 Nov 2025, Denis Benato wrote:
> On 11/1/25 11:47, Antheas Kapenekakis wrote:
> > Handshaking with an Asus device involves sending it a feature report
> > with the string "ASUS Tech.Inc." and then reading it back to verify the
> > handshake was successful, under the feature ID the interaction will
> > take place.
> >
> > Currently, the driver only does the first part. Add the readback to
> > verify the handshake was successful. As this could cause breakages,
> > allow the verification to fail with a dmesg error until we verify
> > all devices work with it (they seem to).
> >
> > Since the response is more than 16 bytes, increase the buffer size
> > to 64 as well to avoid overflow errors.
> >
> > Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
> > ---
> > drivers/hid/hid-asus.c | 32 +++++++++++++++++++++++++++++---
> > 1 file changed, 29 insertions(+), 3 deletions(-)
> >
> > diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> > index 4676b7f20caf..03f0d86936fc 100644
> > --- a/drivers/hid/hid-asus.c
> > +++ b/drivers/hid/hid-asus.c
> > @@ -48,7 +48,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
> > #define FEATURE_REPORT_ID 0x0d
> > #define INPUT_REPORT_ID 0x5d
> > #define FEATURE_KBD_REPORT_ID 0x5a
> > -#define FEATURE_KBD_REPORT_SIZE 16
> > +#define FEATURE_KBD_REPORT_SIZE 64
> > #define FEATURE_KBD_LED_REPORT_ID1 0x5d
> > #define FEATURE_KBD_LED_REPORT_ID2 0x5e
> >
> > @@ -393,14 +393,40 @@ static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t bu
> >
> > static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
> > {
> > + /*
> > + * The handshake is first sent as a set_report, then retrieved
> > + * from a get_report. They should be equal.
> > + */
> > const u8 buf[] = { report_id, 0x41, 0x53, 0x55, 0x53, 0x20, 0x54,
> > 0x65, 0x63, 0x68, 0x2e, 0x49, 0x6e, 0x63, 0x2e, 0x00 };
> > + u8 *readbuf;
>
> __free(kfree) = NULL here? Would simplify the code.
Yes, but see below.
> > int ret;
> >
> > ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
> > - if (ret < 0)
> > - hid_err(hdev, "Asus failed to send init command: %d\n", ret);
> > + if (ret < 0) {
> > + hid_err(hdev, "Asus failed to send handshake: %d\n", ret);
> > + return ret;
> > + }
> > +
> > + readbuf = kzalloc(FEATURE_KBD_REPORT_SIZE, GFP_KERNEL);
The preferred approach for using __free() is to have the whole variable
declaration here as it ensures tear down order of the auto-variables
is the reverse of the init order.
It would be safe to do in this case also in the top block but if something
gets added in between, the ordering could become wrong. Also, each
correctly done example helps to form a pattern in the mind of those
reading this code so they'd be more likely to get the placement right in
some other place.
--
i.
> > + if (!readbuf)
> > + return -ENOMEM;
> > +
> > + ret = hid_hw_raw_request(hdev, report_id, readbuf,
> > + FEATURE_KBD_REPORT_SIZE, HID_FEATURE_REPORT,
> > + HID_REQ_GET_REPORT);
> See comments on patch 1 (also reported below): not sure if others
> report_id are going to answer, my guess is that we will have to try
> if you choose to go that route.
> > + if (ret < 0) {
> > + hid_err(hdev, "Asus failed to receive handshake ack: %d\n", ret);
> > + } else if (memcmp(readbuf, buf, sizeof(buf)) != 0) {
> > + hid_warn(hdev, "Asus handshake returned invalid response: %*ph\n",
> > + FEATURE_KBD_REPORT_SIZE, readbuf);
> > + /*
> > + * Do not return error if handshake is wrong until this is
> > + * verified to work for all devices.
> > + */
> In review of patch 1 I requested this function to be called with more report_id
> than just 0x5a so this will have to be checked against those values too.
>
> In alternative you can fork based on the report_id, but having confirmation that
> this is valid with those ids too would be of great help. Perhaps I can help you
> with this asking to asus-linux users.
> > + }
> >
> > + kfree(readbuf);
> > return ret;
> > }
> >
>
^ permalink raw reply
* Re: [PATCH v8 04/10] HID: asus: prevent binding to all HID devices on ROG
From: Denis Benato @ 2025-11-19 2:33 UTC (permalink / raw)
To: Antheas Kapenekakis
Cc: platform-driver-x86, linux-input, linux-kernel, Jiri Kosina,
Benjamin Tissoires, Corentin Chary, Luke D . Jones,
Ilpo Järvinen, Hans de Goede
In-Reply-To: <CAGwozwEj94txMhgXPigbJxVxw4c=9vSTHNEjpmCXs_fKeSQcXQ@mail.gmail.com>
On 11/19/25 02:45, Antheas Kapenekakis wrote:
> On Wed, 19 Nov 2025 at 01:38, Denis Benato <benato.denis96@gmail.com> wrote:
>>
>> On 11/1/25 11:47, Antheas Kapenekakis wrote:
>>> Currently, when hid-asus is not loaded, NKEY keyboards load as ~6
>>> event devices with a pretty ASUSTEK name. When it loads, it concatenates
>>> all applications per HID endpoint, renames them, and prints errors
>>> when some of them do not have an input device.
>>>
>>> Therefore, change probe so that this is no longer the case. Stop
>>> renaming the devices, omit the check for .input which causes errors
>>> on e.g., the Z13 for some hiddev only devices, and move RGB checks
>>> into probe.
>> I have an issue with this "therefore" related to the renaming of device:
>> you are basically doing here:
>>
>> state a matter of fact.
>> Therefore, change that.
>>
>> Why? the check for .input is clear why, the rename not so much.
>>
>> I have a few more comments below about the rename.
>>> Reviewed-by: Luke D. Jones <luke@ljones.dev>
>>> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
>>> ---
>>> drivers/hid/hid-asus.c | 52 ++++++++++++++++++++++++++++--------------
>>> 1 file changed, 35 insertions(+), 17 deletions(-)
>>>
>>> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
>>> index 03f0d86936fc..726f5d8e22d1 100644
>>> --- a/drivers/hid/hid-asus.c
>>> +++ b/drivers/hid/hid-asus.c
>>> @@ -47,6 +47,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
>>> #define T100CHI_MOUSE_REPORT_ID 0x06
>>> #define FEATURE_REPORT_ID 0x0d
>>> #define INPUT_REPORT_ID 0x5d
>>> +#define HID_USAGE_PAGE_VENDOR 0xff310000
>>> #define FEATURE_KBD_REPORT_ID 0x5a
>>> #define FEATURE_KBD_REPORT_SIZE 64
>>> #define FEATURE_KBD_LED_REPORT_ID1 0x5d
>>> @@ -89,6 +90,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
>>> #define QUIRK_ROG_NKEY_KEYBOARD BIT(11)
>>> #define QUIRK_ROG_CLAYMORE_II_KEYBOARD BIT(12)
>>> #define QUIRK_ROG_ALLY_XPAD BIT(13)
>>> +#define QUIRK_SKIP_REPORT_FIXUP BIT(14)
>>>
>>> #define I2C_KEYBOARD_QUIRKS (QUIRK_FIX_NOTEBOOK_REPORT | \
>>> QUIRK_NO_INIT_REPORTS | \
>>> @@ -125,7 +127,6 @@ struct asus_drvdata {
>>> struct input_dev *tp_kbd_input;
>>> struct asus_kbd_leds *kbd_backlight;
>>> const struct asus_touchpad_info *tp;
>>> - bool enable_backlight;
>>> struct power_supply *battery;
>>> struct power_supply_desc battery_desc;
>>> int battery_capacity;
>>> @@ -316,7 +317,7 @@ static int asus_e1239t_event(struct asus_drvdata *drvdat, u8 *data, int size)
>>> static int asus_event(struct hid_device *hdev, struct hid_field *field,
>>> struct hid_usage *usage, __s32 value)
>>> {
>>> - if ((usage->hid & HID_USAGE_PAGE) == 0xff310000 &&
>>> + if ((usage->hid & HID_USAGE_PAGE) == HID_USAGE_PAGE_VENDOR &&
>>> (usage->hid & HID_USAGE) != 0x00 &&
>>> (usage->hid & HID_USAGE) != 0xff && !usage->type) {
>>> hid_warn(hdev, "Unmapped Asus vendor usagepage code 0x%02x\n",
>>> @@ -931,11 +932,6 @@ static int asus_input_configured(struct hid_device *hdev, struct hid_input *hi)
>>>
>>> drvdata->input = input;
>>>
>>> - if (drvdata->enable_backlight &&
>>> - !asus_kbd_wmi_led_control_present(hdev) &&
>>> - asus_kbd_register_leds(hdev))
>>> - hid_warn(hdev, "Failed to initialize backlight.\n");
>>> -
>>> return 0;
>>> }
>>>
>>> @@ -1008,15 +1004,6 @@ static int asus_input_mapping(struct hid_device *hdev,
>>> return -1;
>>> }
>>>
>>> - /*
>>> - * Check and enable backlight only on devices with UsagePage ==
>>> - * 0xff31 to avoid initializing the keyboard firmware multiple
>>> - * times on devices with multiple HID descriptors but same
>>> - * PID/VID.
>>> - */
>>> - if (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT)
>>> - drvdata->enable_backlight = true;
>>> -
>>> set_bit(EV_REP, hi->input->evbit);
>>> return 1;
>>> }
>>> @@ -1133,8 +1120,10 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev)
>>>
>>> static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
>>> {
>>> - int ret;
>>> + struct hid_report_enum *rep_enum;
>>> struct asus_drvdata *drvdata;
>>> + struct hid_report *rep;
>>> + int ret, is_vendor = 0;
>>>
>> Why is is_vendor an int? Don't we have bools?
>>> drvdata = devm_kzalloc(&hdev->dev, sizeof(*drvdata), GFP_KERNEL);
>>> if (drvdata == NULL) {
>>> @@ -1218,12 +1207,37 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
>>> return ret;
>>> }
>>>
>>> + /* Check for vendor for RGB init and handle generic devices properly. */
>>> + rep_enum = &hdev->report_enum[HID_INPUT_REPORT];
>>> + list_for_each_entry(rep, &rep_enum->report_list, list) {
>>> + if ((rep->application & HID_USAGE_PAGE) == HID_USAGE_PAGE_VENDOR)
>>> + is_vendor = true;
>>> + }
>>> +
>>> + /*
>>> + * For ROG keyboards, disable fixups except vendor devices.
>>> + */
>> multiline comment for no reason. Comma doesn't provide any value here.
>>> + if (drvdata->quirks & QUIRK_ROG_NKEY_KEYBOARD && !is_vendor)
>>> + drvdata->quirks |= QUIRK_SKIP_REPORT_FIXUP;
>>> +
>> Doing this will skip the report fixup entirely while before
>> it was called in every case: are we really sure we want this?
>> Or do we want it only for specific devices?
>>
>> It's my understanding that function is only useful on
>> keyboard devices, so before keyboard devices (all)
>> while now is_vendor keyboard devices, right?
> ROG Keyboard devices have multiple HID endpoints. This driver only
> hooks to the 0xff31 endpoint. So the rest of the endpoints should not
> be modified. Except for minor fixups, see below.
>
>> What about keyboard devices that are not is_vendor
>> for which function isn't called anymore?
>>> ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT);
>>> if (ret) {
>>> hid_err(hdev, "Asus hw start failed: %d\n", ret);
>>> return ret;
>>> }
>>>
>>> + if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) &&
>>> + !asus_kbd_wmi_led_control_present(hdev) &&
>>> + asus_kbd_register_leds(hdev))
>>> + hid_warn(hdev, "Failed to initialize backlight.\n");
>>> +
>>> + /*
>>> + * For ROG keyboards, skip rename for consistency and ->input check as
>>> + * some devices do not have inputs.
>>> + */
>>> + if (drvdata->quirks & QUIRK_ROG_NKEY_KEYBOARD)
>>> + return 0;
>>> +
>> This is a moot point: I have yet to see the real benefit of doing this,
>> but one thing is sure... having for the same driver multiple name
>> to basically the same interface across different lineups of
>> hardware is not something I would call "consistency".
> The reason the rename exists here is because the devices this driver
> applied to originally did not have proper names (e.g. I2C). The new
> ROG devices do have proper names, so there is no reason to deviate
> from those. It also eliminates a point of failure in which the
> hid-asus driver is not loaded, if your point is consistency. Ie, as we
> add more keyboard IDs, those would then not be renamed when hid-asus
> starts to load them.
I understand you don't see a reason to rename them,
but I am also not seeing any reason not to rename them.
Removing a theoretically possible failure in the load path
that nobody has ever reported once IMHO is not a strong
enough motivation to change the name of a device.
Just for clarity I am telling you that I am not particularly
happy but I won't oppose if the userspace won't suffer
from this change.
> As for affected software (per your other email), yes it is only
> Inputplumber when running on its fallback mode without access to its
> OOT modules that disable this driver. Because it had an architectural
> decision to rely on hardcoded evdev/phys names for most devices
> instead of more canonical vid/pid/capability matches. The phys part is
> especially problematic as it also hardcodes the bus node of a lot of
> devices. There is a PR open to remove the matches for the Ally units
> though.
I asked what's the path forward, and for you two to decide how to not
break the "we don't break userspace" rule causing troubles to
users/distro maintainers/valve/whoever.
Have you two reached an agreement yet?
> If you want to know about other software that relies on names, SDL is
> the main one. And the reason for that is so that it matches the kernel
> driver. E.g., when a playstation 5 controller loads using
> hid_playstation, it has a different mapping and name than when it
> loads through hid core. My software also relies on it for WMI
> keyboards, as those have a vid:pid of 0:0 so it is unavoidable.
>
> SDL does not map keyboards so it is not affected. Moreover, as this
> series makes it so the device has the same name as with the driver not
> loaded, software such as SDL would have a fallback mapping for that
> name already.
>
>> As I said already I want you to either drop this or to present
>> a list of pros of doing this and to hear from Derek the plan
>> going forward to avoid breaking anything.
>>> /*
>>> * Check that input registration succeeded. Checking that
>>> * HID_CLAIMED_INPUT is set prevents a UAF when all input devices
>>> @@ -1352,6 +1366,10 @@ static const __u8 *asus_report_fixup(struct hid_device *hdev, __u8 *rdesc,
>>> rdesc = new_rdesc;
>>> }
>>>
>>> + /* Vendor fixups should only apply to NKEY vendor devices. */
>>> + if (drvdata->quirks & QUIRK_SKIP_REPORT_FIXUP)
>>> + return rdesc;
>>> +
>> Uhm... no? Or at least it's not obvious why.
>>
>> If this is the case why is the check not at the top of the function?
> Because the checks above apply to e.g., touchpad devices. This was
> actually a bug with this series in previous versions. Only the report
> fixups should be skipped. Specifically, with previous versions a HID
> application mute was skipped, which caused certain keyboards to show
> up as range finders if I recall (there is a comment for it in this
> driver).
I see... honestly I would insert a summary of this in a comment
somewhere. Probably before that if.
> The report fixups that are below this check are only applicable to the
> 0xff31 devices that emit 0x5a events. Specifically, this driver
> "abuses" the HID subsystem a bit to make the vendor report, which is
> not a HID input device, appear as an input device, by mutating its
> descriptor. But refactoring that would be too painful. At least with
> this we make the fixup apply more precisely to only those devices.
You can put a TL;DR of this as a comment too in the code.
> I kindly ask you finish your comments until tomorrow evening, so I can
> resend this series.
I will try to do what I can, but until I hear an approval from Derek
I can't give my approval knowing this can break a tool in use
and we know it.
> Thanks,
> Antheas
>
>> Beside please refrain from using "should" in this context unless
>> backed up by evidence or it's otherwise obvious as "should"
>> can have many different interpretations.
>>> if (drvdata->quirks & QUIRK_ROG_NKEY_KEYBOARD &&
>>> *rsize == 331 && rdesc[190] == 0x85 && rdesc[191] == 0x5a &&
>>> rdesc[204] == 0x95 && rdesc[205] == 0x05) {
^ permalink raw reply
* Re: [PATCH] input: touchscreen: Add ilitek touchscreen driver support
From: kernel test robot @ 2025-11-19 1:47 UTC (permalink / raw)
To: 2724853925, Dmitry Torokhov, Henrik Rydberg
Cc: oe-kbuild-all, linux-input, linux-kernel, linux-gpio, 2724853925
In-Reply-To: <tencent_995E6FC62EDBC1EED14E6052847F270F6406@qq.com>
Hi,
kernel test robot noticed the following build warnings:
[auto build test WARNING on dtor-input/next]
[also build test WARNING on dtor-input/for-linus linus/master v6.18-rc6 next-20251118]
[If your patch is applied to the wrong git tree, kindly drop us a note.
And when submitting patch, we suggest to use '--base' as documented in
https://git-scm.com/docs/git-format-patch#_base_tree_information]
url: https://github.com/intel-lab-lkp/linux/commits/2724853925-qq-com/input-touchscreen-Add-ilitek-touchscreen-driver-support/20251116-215220
base: https://git.kernel.org/pub/scm/linux/kernel/git/dtor/input.git next
patch link: https://lore.kernel.org/r/tencent_995E6FC62EDBC1EED14E6052847F270F6406%40qq.com
patch subject: [PATCH] input: touchscreen: Add ilitek touchscreen driver support
config: sh-randconfig-r112-20251119 (https://download.01.org/0day-ci/archive/20251119/202511190932.OFk1oMBB-lkp@intel.com/config)
compiler: sh4-linux-gcc (GCC) 11.5.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20251119/202511190932.OFk1oMBB-lkp@intel.com/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202511190932.OFk1oMBB-lkp@intel.com/
sparse warnings: (new ones prefixed by >>)
drivers/input/touchscreen/ilitek/ilitek_tool.c:93:38: sparse: sparse: incorrect type in argument 2 (different address spaces) @@ expected void const [noderef] __user *from @@ got char const *buf @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:93:38: sparse: expected void const [noderef] __user *from
drivers/input/touchscreen/ilitek/ilitek_tool.c:93:38: sparse: got char const *buf
drivers/input/touchscreen/ilitek/ilitek_tool.c:239:45: sparse: sparse: incorrect type in argument 2 (different address spaces) @@ expected void const [noderef] __user *from @@ got unsigned char [usertype] * @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:239:45: sparse: expected void const [noderef] __user *from
drivers/input/touchscreen/ilitek/ilitek_tool.c:239:45: sparse: got unsigned char [usertype] *
drivers/input/touchscreen/ilitek/ilitek_tool.c:268:35: sparse: sparse: incorrect type in argument 1 (different address spaces) @@ expected void [noderef] __user *to @@ got unsigned char [usertype] * @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:268:35: sparse: expected void [noderef] __user *to
drivers/input/touchscreen/ilitek/ilitek_tool.c:268:35: sparse: got unsigned char [usertype] *
drivers/input/touchscreen/ilitek/ilitek_tool.c:279:35: sparse: sparse: incorrect type in argument 1 (different address spaces) @@ expected void [noderef] __user *to @@ got unsigned char [usertype] * @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:279:35: sparse: expected void [noderef] __user *to
drivers/input/touchscreen/ilitek/ilitek_tool.c:279:35: sparse: got unsigned char [usertype] *
drivers/input/touchscreen/ilitek/ilitek_tool.c:286:35: sparse: sparse: incorrect type in argument 1 (different address spaces) @@ expected void [noderef] __user *to @@ got unsigned char [usertype] * @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:286:35: sparse: expected void [noderef] __user *to
drivers/input/touchscreen/ilitek/ilitek_tool.c:286:35: sparse: got unsigned char [usertype] *
>> drivers/input/touchscreen/ilitek/ilitek_tool.c:307:21: sparse: sparse: incorrect type in initializer (different address spaces) @@ expected signed int [noderef] __user *__pu_addr @@ got signed int [usertype] * @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:307:21: sparse: expected signed int [noderef] __user *__pu_addr
drivers/input/touchscreen/ilitek/ilitek_tool.c:307:21: sparse: got signed int [usertype] *
drivers/input/touchscreen/ilitek/ilitek_tool.c:312:45: sparse: sparse: incorrect type in argument 2 (different address spaces) @@ expected void const [noderef] __user *from @@ got unsigned char [usertype] * @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:312:45: sparse: expected void const [noderef] __user *from
drivers/input/touchscreen/ilitek/ilitek_tool.c:312:45: sparse: got unsigned char [usertype] *
drivers/input/touchscreen/ilitek/ilitek_tool.c:324:45: sparse: sparse: incorrect type in argument 2 (different address spaces) @@ expected void const [noderef] __user *from @@ got unsigned char [usertype] * @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:324:45: sparse: expected void const [noderef] __user *from
drivers/input/touchscreen/ilitek/ilitek_tool.c:324:45: sparse: got unsigned char [usertype] *
drivers/input/touchscreen/ilitek/ilitek_tool.c:340:45: sparse: sparse: incorrect type in argument 2 (different address spaces) @@ expected void const [noderef] __user *from @@ got unsigned char [usertype] * @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:340:45: sparse: expected void const [noderef] __user *from
drivers/input/touchscreen/ilitek/ilitek_tool.c:340:45: sparse: got unsigned char [usertype] *
drivers/input/touchscreen/ilitek/ilitek_tool.c:365:35: sparse: sparse: incorrect type in argument 1 (different address spaces) @@ expected void [noderef] __user *to @@ got unsigned char [usertype] * @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:365:35: sparse: expected void [noderef] __user *to
drivers/input/touchscreen/ilitek/ilitek_tool.c:365:35: sparse: got unsigned char [usertype] *
drivers/input/touchscreen/ilitek/ilitek_tool.c:416:26: sparse: sparse: incorrect type in argument 1 (different address spaces) @@ expected void [noderef] __user *to @@ got char *buf @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:416:26: sparse: expected void [noderef] __user *to
drivers/input/touchscreen/ilitek/ilitek_tool.c:416:26: sparse: got char *buf
drivers/input/touchscreen/ilitek/ilitek_tool.c:444:17: sparse: sparse: incorrect type in initializer (incompatible argument 2 (different address spaces)) @@ expected int ( *read )( ... ) @@ got int ( * )( ... ) @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:444:17: sparse: expected int ( *read )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:444:17: sparse: got int ( * )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:445:18: sparse: sparse: incorrect type in initializer (incompatible argument 2 (different address spaces)) @@ expected int ( *write )( ... ) @@ got int ( * )( ... ) @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:445:18: sparse: expected int ( *write )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:445:18: sparse: got int ( * )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:457:22: sparse: sparse: incorrect type in initializer (incompatible argument 2 (different address spaces)) @@ expected int ( *proc_read )( ... ) @@ got int ( * )( ... ) @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:457:22: sparse: expected int ( *proc_read )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:457:22: sparse: got int ( * )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:458:23: sparse: sparse: incorrect type in initializer (incompatible argument 2 (different address spaces)) @@ expected int ( *proc_write )( ... ) @@ got int ( * )( ... ) @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:458:23: sparse: expected int ( *proc_write )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:458:23: sparse: got int ( * )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:571:37: sparse: sparse: incorrect type in argument 2 (different address spaces) @@ expected void const [noderef] __user *from @@ got char const *buf @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:571:37: sparse: expected void const [noderef] __user *from
drivers/input/touchscreen/ilitek/ilitek_tool.c:571:37: sparse: got char const *buf
drivers/input/touchscreen/ilitek/ilitek_tool.c:610:23: sparse: sparse: incorrect type in initializer (incompatible argument 2 (different address spaces)) @@ expected int ( *proc_write )( ... ) @@ got int ( * )( ... ) @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:610:23: sparse: expected int ( *proc_write )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:610:23: sparse: got int ( * )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:622:33: sparse: sparse: incorrect type in argument 2 (different address spaces) @@ expected void const [noderef] __user *from @@ got char const *buf @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:622:33: sparse: expected void const [noderef] __user *from
drivers/input/touchscreen/ilitek/ilitek_tool.c:622:33: sparse: got char const *buf
drivers/input/touchscreen/ilitek/ilitek_tool.c:684:23: sparse: sparse: incorrect type in initializer (incompatible argument 2 (different address spaces)) @@ expected int ( *proc_write )( ... ) @@ got int ( * )( ... ) @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:684:23: sparse: expected int ( *proc_write )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:684:23: sparse: got int ( * )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:693:36: sparse: sparse: incorrect type in argument 2 (different address spaces) @@ expected void const [noderef] __user *from @@ got char const *buf @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:693:36: sparse: expected void const [noderef] __user *from
drivers/input/touchscreen/ilitek/ilitek_tool.c:693:36: sparse: got char const *buf
drivers/input/touchscreen/ilitek/ilitek_tool.c:731:23: sparse: sparse: incorrect type in initializer (incompatible argument 2 (different address spaces)) @@ expected int ( *proc_write )( ... ) @@ got int ( * )( ... ) @@
drivers/input/touchscreen/ilitek/ilitek_tool.c:731:23: sparse: expected int ( *proc_write )( ... )
drivers/input/touchscreen/ilitek/ilitek_tool.c:731:23: sparse: got int ( * )( ... )
vim +307 drivers/input/touchscreen/ilitek/ilitek_tool.c
218
219 static FOPS_IOCTL_FUNC(ilitek_file_ioctl, uint32_t cmd, unsigned long arg)
220 {
221 static u8 *buffer;
222 static unsigned long len;
223 s32 ret = 0;
224 int tmp;
225
226 buffer = kmalloc(ILITEK_IOCTL_MAX_TRANSFER, GFP_KERNEL);
227 memset(buffer, 0, ILITEK_IOCTL_MAX_TRANSFER);
228
229 switch (cmd) {
230 case ILITEK_IOCTL_I2C_WRITE_DATA:
231 case ILITEK_IOCTL_I2C_WRITE_DATA_COMPAT:
232 if (len > ILITEK_IOCTL_MAX_TRANSFER) {
233 TP_ERR(NULL, "invalid write len: %lu > %lu too large\n",
234 len, ILITEK_IOCTL_MAX_TRANSFER);
235 ret = -EINVAL;
236 break;
237 }
238
239 if (copy_from_user(buffer, (u8 *)arg, len)) {
240 TP_ERR(NULL, "copy data from user space, failed\n");
241 ret = -EFAULT;
242 break;
243 }
244
245 mutex_lock(&ts->ilitek_mutex);
246 ret = ilitek_write_and_read(buffer, len, 0, NULL, 0);
247 mutex_unlock(&ts->ilitek_mutex);
248 if (ret < 0)
249 TP_ERR(NULL, "i2c write failed, cmd: %x\n", buffer[0]);
250 break;
251 case ILITEK_IOCTL_I2C_READ_DATA:
252 case ILITEK_IOCTL_I2C_READ_DATA_COMPAT:
253 if (len > ILITEK_IOCTL_MAX_TRANSFER) {
254 TP_ERR(NULL, "invalid read len: %lu > %lu too large\n",
255 len, ILITEK_IOCTL_MAX_TRANSFER);
256 ret = -EINVAL;
257 break;
258 }
259
260 mutex_lock(&ts->ilitek_mutex);
261 ret = ilitek_write_and_read(NULL, 0, 0, buffer, len);
262 mutex_unlock(&ts->ilitek_mutex);
263 if (ret < 0) {
264 TP_ERR(NULL, "i2c read failed, buf: %x\n", buffer[0]);
265 break;
266 }
267
268 if (copy_to_user((u8 *)arg, buffer, len)) {
269 ret = -EFAULT;
270 TP_ERR(NULL, "copy data to user space, failed\n");
271 }
272 break;
273 case ILITEK_IOCTL_I2C_WRITE_LENGTH:
274 case ILITEK_IOCTL_I2C_READ_LENGTH:
275 len = arg;
276 break;
277 case ILITEK_IOCTL_DRIVER_INFORMATION:
278 memcpy(buffer, driver_ver, 7);
279 if (copy_to_user((u8 *)arg, buffer, 7))
280 ret = -EFAULT;
281 break;
282 case ILITEK_IOCTL_I2C_UPDATE:
283 break;
284 case ILITEK_IOCTL_I2C_INT_FLAG:
285 buffer[0] = !(gpio_get_value(ts->irq_gpio));
286 if (copy_to_user((u8 *)arg, buffer, 1)) {
287 TP_ERR(NULL, "copy data to user space, failed\n");
288 ret = -EFAULT;
289 break;
290 }
291 TP_DBG(NULL, "ILITEK_IOCTL_I2C_INT_FLAG = %d.\n", buffer[0]);
292 break;
293 case ILITEK_IOCTL_START_READ_DATA:
294 ilitek_irq_enable();
295 ts->unhandle_irq = false;
296 TP_MSG(NULL, "enable_irq and ts->unhandle_irq = false.\n");
297 break;
298 case ILITEK_IOCTL_STOP_READ_DATA:
299 ilitek_irq_disable();
300 ts->unhandle_irq = true;
301 TP_MSG(NULL, "disable_irq and ts->unhandle_irq = true.\n");
302 break;
303 case ILITEK_IOCTL_RESET:
304 ilitek_reset(ts->dev->reset_time);
305 break;
306 case ILITEK_IOCTL_INT_STATUS:
> 307 if (put_user(gpio_get_value(ts->irq_gpio), (s32 *)arg))
308 ret = -EFAULT;
309 break;
310 #ifdef ILITEK_TUNING_MESSAGE
311 case ILITEK_IOCTL_DEBUG_SWITCH:
312 if (copy_from_user(buffer, (u8 *)arg, 1)) {
313 ret = -EFAULT;
314 break;
315 }
316 TP_MSG(NULL, "ilitek The debug_flag = %d.\n", buffer[0]);
317 if (buffer[0] == 0)
318 ilitek_debug_flag = false;
319 else if (buffer[0] == 1)
320 ilitek_debug_flag = true;
321 break;
322 #endif
323 case ILITEK_IOCTL_I2C_SWITCH_IRQ:
324 if (copy_from_user(buffer, (u8 *)arg, 1)) {
325 ret = -EFAULT;
326 break;
327 }
328
329 if (buffer[0] == 0)
330 ilitek_irq_disable();
331 else
332 ilitek_irq_enable();
333
334 break;
335 case ILITEK_IOCTL_UPDATE_FLAG:
336 ts->operation_protection = arg;
337 TP_MSG(NULL, "operation_protection = %d\n", ts->operation_protection);
338 break;
339 case ILITEK_IOCTL_I2C_UPDATE_FW:
340 if (copy_from_user(buffer, (u8 *)arg, 35)) {
341 TP_ERR(NULL, "copy data from user space, failed\n");
342 ret = -EFAULT;
343 break;
344 }
345
346 ilitek_irq_disable();
347 mutex_lock(&ts->ilitek_mutex);
348 ret = ilitek_write_and_read(buffer, buffer[34], 0, NULL, 0);
349 mutex_unlock(&ts->ilitek_mutex);
350 ilitek_irq_enable();
351
352 if (ret < 0)
353 TP_ERR(NULL, "i2c write, failed\n");
354
355 break;
356 case ILITEK_IOCTL_I2C_INT_CLR:
357 TP_DBG(NULL, "ILITEK_IOCTL_I2C_INT_CLR, set get_INT false\n");
358 atomic_set(&ts->get_INT, 0);
359 break;
360 case ILITEK_IOCTL_I2C_INT_POLL:
361 case ILITEK_IOCTL_I2C_INT_POLL_COMPAT:
362 tmp = atomic_read(&ts->get_INT);
363 TP_DBG(NULL, "ILITEK_IOCTL_I2C_INT_POLL, get_INT: %d\n", tmp);
364
365 if (copy_to_user((u8 *)arg, &tmp, 1)) {
366 TP_ERR(NULL, "copy data to user space, failed\n");
367 ret = -EFAULT;
368 }
369 break;
370 case ILITEK_IOCTL_I2C_ISR_TYPE:
371 TP_MSG(NULL, "ILITEK_IOCTL_I2C_ISR_TYPE, set ISR type: %lu\n", arg);
372 ts->irq_handle_type = (arg >> 16);
373 ts->irq_read_len = arg & 0xFFFF;
374 break;
375 case ILITEK_IOCTL_I2C_NETLINK:
376 TP_MSG(NULL, "ILITEK_IOCTL_I2C_NETLINK, set netlink: %s with ETH: %hhu\n",
377 (arg >> 8) ? "ON" : "OFF", (u8)(arg & 0xFF));
378
379 if (arg >> 8)
380 ret = ilitek_netlink_init(arg & 0xFF);
381 else
382 ilitek_netlink_exit();
383
384 break;
385 default:
386 TP_ERR(NULL, "unrecognized ioctl cmd: 0x%04x\n", cmd);
387 ret = -EINVAL;
388 break;
389 }
390
391 kfree(buffer);
392 return (ret < 0) ? ret : 0;
393 }
394
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* Re: [PATCH v8 03/10] HID: asus: fortify keyboard handshake
From: Denis Benato @ 2025-11-19 1:46 UTC (permalink / raw)
To: luke
Cc: Antheas Kapenekakis, platform-driver-x86, linux-input,
linux-kernel, Jiri Kosina, Benjamin Tissoires, Corentin Chary,
Hans de Goede, Ilpo Järvinen
In-Reply-To: <0F5019F3-2654-4C03-B7EF-5B7B83AF7B32@ljones.dev>
On 11/19/25 00:46, luke@ljones.dev wrote:
>
>> On 19 Nov 2025, at 12:43, Denis Benato <benato.denis96@gmail.com> wrote:
>>
>>
>> On 11/1/25 11:47, Antheas Kapenekakis wrote:
>>> Handshaking with an Asus device involves sending it a feature report
>>> with the string "ASUS Tech.Inc." and then reading it back to verify the
>>> handshake was successful, under the feature ID the interaction will
>>> take place.
>>>
>>> Currently, the driver only does the first part. Add the readback to
>>> verify the handshake was successful. As this could cause breakages,
>>> allow the verification to fail with a dmesg error until we verify
>>> all devices work with it (they seem to).
>>>
>>> Since the response is more than 16 bytes, increase the buffer size
>>> to 64 as well to avoid overflow errors.
>>>
>>> Signed-off-by: Antheas Kapenekakis <lkml@antheas.dev>
>>> ---
>>> drivers/hid/hid-asus.c | 32 +++++++++++++++++++++++++++++---
>>> 1 file changed, 29 insertions(+), 3 deletions(-)
>>>
>>> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
>>> index 4676b7f20caf..03f0d86936fc 100644
>>> --- a/drivers/hid/hid-asus.c
>>> +++ b/drivers/hid/hid-asus.c
>>> @@ -48,7 +48,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
>>> #define FEATURE_REPORT_ID 0x0d
>>> #define INPUT_REPORT_ID 0x5d
>>> #define FEATURE_KBD_REPORT_ID 0x5a
>>> -#define FEATURE_KBD_REPORT_SIZE 16
>>> +#define FEATURE_KBD_REPORT_SIZE 64
>>> #define FEATURE_KBD_LED_REPORT_ID1 0x5d
>>> #define FEATURE_KBD_LED_REPORT_ID2 0x5e
>>>
>>> @@ -393,14 +393,40 @@ static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t bu
>>>
>>> static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
>>> {
>>> + /*
>>> + * The handshake is first sent as a set_report, then retrieved
>>> + * from a get_report. They should be equal.
>>> + */
>>> const u8 buf[] = { report_id, 0x41, 0x53, 0x55, 0x53, 0x20, 0x54,
>>> 0x65, 0x63, 0x68, 0x2e, 0x49, 0x6e, 0x63, 0x2e, 0x00 };
>>> + u8 *readbuf;
>> __free(kfree) = NULL here? Would simplify the code.
>>
>>> int ret;
>>>
>>> ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
>>> - if (ret < 0)
>>> - hid_err(hdev, "Asus failed to send init command: %d\n", ret);
>>> + if (ret < 0) {
>>> + hid_err(hdev, "Asus failed to send handshake: %d\n", ret);
>>> + return ret;
>>> + }
>>> +
>>> + readbuf = kzalloc(FEATURE_KBD_REPORT_SIZE, GFP_KERNEL);
>>> + if (!readbuf)
>>> + return -ENOMEM;
>>> +
>>> + ret = hid_hw_raw_request(hdev, report_id, readbuf,
>>> + FEATURE_KBD_REPORT_SIZE, HID_FEATURE_REPORT,
>>> + HID_REQ_GET_REPORT);
>> See comments on patch 1 (also reported below): not sure if others
>> report_id are going to answer, my guess is that we will have to try
>> if you choose to go that route.
>>> + if (ret < 0) {
>>> + hid_err(hdev, "Asus failed to receive handshake ack: %d\n", ret);
>>> + } else if (memcmp(readbuf, buf, sizeof(buf)) != 0) {
>>> + hid_warn(hdev, "Asus handshake returned invalid response: %*ph\n",
>>> + FEATURE_KBD_REPORT_SIZE, readbuf);
>>> + /*
>>> + * Do not return error if handshake is wrong until this is
>>> + * verified to work for all devices.
>>> + */
>> In review of patch 1 I requested this function to be called with more report_id
>> than just 0x5a so this will have to be checked against those values too.
>>
>> In alternative you can fork based on the report_id, but having confirmation that
>> this is valid with those ids too would be of great help. Perhaps I can help you
>> with this asking to asus-linux users.
> The handshake works for 0x5D and 0x5E also.
Then this, like patch 2, should have been sent regardless of
this patchset, this whole discussion stalled two good patches
that we all agree are good and are totally independent from
the main issue?
>>> + }
>>>
>>> + kfree(readbuf);
>>> return ret;
>>> }
>>>
^ permalink raw reply
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