* [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds
@ 2026-10-31 17:41 Denis Benato
2026-10-31 17:41 ` [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor Denis Benato
` (16 more replies)
0 siblings, 17 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
Hi all,
This patch series adds support for ROG Ally handhelds in the HID driver for ASUS devices.
The changes include reinitialization of the device after exiting a sleep state,
gamepad configuration, vibration strength configuration, joysticks and triggers
range configuration, anti-deadzone configuration, response curve support,
force feedback support, gamepad mode support, turbo buttons support,
and button remapping support.
This driver is based on the original Luke's work, which was a great starting point
and I have reworked it to be upstreamable, while keeping the original functionality
intact and extending hid-asus to avoid regressions in functionality such as the
backlight control.
The ally support has been a surprisingly long task, spanning multiple years and
being used in Steam OS and well as other distributions for quite a while,
and I am happy to finally be submitting it.
At this time LEDs are not part of the patch series as I have work in progress to
unify LEDs handling across all ASUS devices, and I will submit that in a separate
patch series when the userspace interface will be ready to express capabilities
of these devices, but that will take some more time.
This driver is the culmination of a lot of work, from many different people,
and I want to thank Luke for his original work, as well as the many people that have
helped me test and provide feedback on the driver: Khamunetri, Jonathan, Derek,
Matthew and others that have helped during these years.
Best regards,
Denis Benato
Link v1: https://lore.kernel.org/all/20260813144736.2477941-1-denis.benato@linux.dev/
Link v2: https://lore.kernel.org/all/20260819195835.1649657-1-denis.benato@linux.dev/
Link v3: https://lore.kernel.org/all/20260823185849.2478673-2-denis.benato@linux.dev/
Link v4: https://lore.kernel.org/all/20260830234320.114356-1-denis.benato@linux.dev/
Link v5: https://lore.kernel.org/all/20260904145845.184887-1-denis.benato@linux.dev/
Link v6: https://lore.kernel.org/all/20260908174433.74260-1-denis.benato@linux.dev/
Link v7: https://lore.kernel.org/all/20260915201314.28279-1-denis.benato@linux.dev/
Link v8: https://lore.kernel.org/all/20260922230205.6847-1-denis.benato@linux.dev/
Link v9: https://lore.kernel.org/all/20261009122820.7603-1-denis.benato@linux.dev/
Link v10: https://lore.kernel.org/all/20261017181520.70206-1-denis.benato@linux.dev/
Link v11: https://lore.kernel.org/all/20261025235232.129945-1-denis.benato@linux.dev/
------------------------------------------------------------------------------------------------
The 12th version is aimed at cleaning up all the weird locking/unlocking machinery
and compressing the code wherever it was natural to do so while also addressing
all comments and suggestions.
I thank all the reviewers for their feedback and suggestions, along with Luytan that
borrowed me an API key for sashiko-bot as I could ensure no medium/high/critical issues
were left unaddressed and low ones are benign.
Given I have made quite a bit of changes I would ask for a second test by at least
Rebecca and Paolo to ensure I hadn't screwed up what was working before:
this is also the reason why I left out Tested-by tags.
------------------------------------------------------------------------------------------------
Version v11 has been more involved and I focused on two main things: extending the
xg mobile (which the ROG ally is one of the few models that supports it) and
properly handling hid packet sizes because it has been a constant source of issues
and fight in the past and I want to put an end to both of those.
I kindly ask Rebecca and Paolo to try using the new asus_report_len() to see if its
also suitable for theirs Zenbook Duo devices and if so to adopt it in theirs patches
as well.
Link of a relevant patchset: https://lore.kernel.org/all/20261020092423.36380-1-p.pisati@gmail.com/
------------------------------------------------------------------------------------------------
Version v10 drops a patch already merged as part of XG mobile and applies the
patchset on top of for-next/7.4.
------------------------------------------------------------------------------------------------
With v8 the driver does sysfs_notify after successfully changing configuration
attributes so that userspace can be notified of the changes immediately.
------------------------------------------------------------------------------------------------
With v7 the series is rebased on top of the asus tree: the first patch of v6
has been applied upstream as 8099f9da59f1 ("HID: asus: do not send keyboard
init reports to touchpads"), so this series is now 12 patches.
The probe paths of the ally interfaces now reserve the interface role while
their initialization is running, the configuration attributes work from the
moment their files exist and a failed initialization rolls the configuration
back instead of leaving the created sysfs groups behind. The response curve
is no longer configured entry by entry: a single attribute per joystick now
takes the whole curve in one validated write, and a companion index file
documents what each value is.
------------------------------------------------------------------------------------------------
With v5 I returned to the original manual management of memory and sysfs groups
as going devm did not play well with the general "plugin" architecture of the
ally portion of the driver and has caused more bugs than it solved: sashiko-bot
will have to come to terms with hand-managed memory this time.
------------------------------------------------------------------------------------------------
Changelog:
-v2
- HID: asus: add support for ROG Ally handhelds
- release all vendor buttons on resume
- align arguments of hid_asus_ally_raw_event()
- HID: asus: add gamepad configuration
- clean up comments and kernel-doc
- HID: asus: add vibration strength configuration
- clean up comments and kernel-doc
- HID: asus: add joysticks inner and outer range configuration
- clean up comments and kernel-doc
- HID: asus: add triggers inner and outer range configuration
- clean up comments and kernel-doc
- HID: asus: add joysticks anti-deadzone configuration
- clean up comments and kernel-doc
- HID: asus: add support for response curve
- normalize kernel-doc comment
- HID: asus: add support to force feedback
- remove a stale comment
- HID: asus: add support for gamepad mode
- remove a stale comment, normalize kernel-doc
- HID: asus: add support for turbo buttons
- remove stale comments, normalize kernel-doc
- HID: asus: add support for btn remapping
- convert function comments to kernel-doc
-v3
- HID: asus: add support for ROG Ally handhelds
- take references on input devices to avoid use-after-free on unbind
- reject probes from USB devices other than the recorded controller
- add open/close callbacks to the gamepad input device
- keyboard_input is populated after hid_hw_start(): issue does not apply
- HID: asus: add gamepad configuration
- restore cached Xbox controller mode after an MCU reset
- convert remaining scoped_guard() users to guard()
- avoid printing "(null)" for unnamed sysfs groups
- HID: asus: add vibration strength configuration
- send MCU commands through ally_gamepad_send_packet()
- HID: asus: add joysticks inner and outer range configuration
- send MCU commands through ally_gamepad_send_packet()
- gate sysfs groups and callbacks on capabilities
- HID: asus: add triggers inner and outer range configuration
- send MCU commands through ally_gamepad_send_packet()
- gate sysfs groups and callbacks on capabilities
- HID: asus: add joysticks anti-deadzone configuration
- send MCU commands through ally_gamepad_send_packet()
- gate sysfs groups and callbacks on capabilities
- HID: asus: add support for response curve
- send MCU commands through ally_gamepad_send_packet()
- check resp_curve_support in show callbacks too
- HID: asus: add support to force feedback
- read ally_x_hdev under ally_data_lock and take a reference on it
- publish ally_x_hdev before registering the input device
- HID: asus: add support for gamepad mode
- convert remaining scoped_guard() users to guard()
- HID: asus: add support for turbo buttons
- pass the config pointer directly to ally_config_remove()
- run ally_config_remove() outside ally_data_lock
- restore cached values when the hardware rejects an update
- check turbo_support in show callbacks too
- reported scoped_guard warning verified as false positive
- HID: asus: add support for btn remapping
- look up default mappings by name instead of array index
- resend the old mapping when a sequential update fails midway
- allow macros regardless of the gamepad mode
- rename PAD_* codes to proper input event names
- convert remaining scoped_guard() users to guard()
- v4
- HID: asus: do not send keyboard init reports to touchpads
- Added to avoid regressting touchpads and as guidance for maintainers
- HID: asus: reinitialize the device after exiting a sleep state
- Follow the modified logic of the previous patch
- Addressed issues of sashiko-bot albeit one was a false positive
- Rework patches: I made some disasters rebasing in v3
- v5
- HID: asus: add support for ROG Ally handhelds
- Fix UAF due to inverted input device ownership in hid_asus_ally_remove()
- Reject probes claiming an interface role that is already owned
- HID: asus: add gamepad configuration
- Keep the configuration allocated until module removal instead of
devm-managing it: the sysfs callbacks must be able to outlive probe
- Undo sysfs group creation manually on the error path and in
ally_config_remove(): creation and removal are now symmetric
- Add payloads size check in ally_alloc_cmd()
- HID: asus: add vibration strength configuration
- Restore the cached vibration intensity after an MCU reset
- HID: asus: add joysticks inner and outer range configuration
- Restore the cached joystick thresholds after an MCU reset
- Remove the calibration sysfs groups manually like the other ones
- HID: asus: add triggers inner and outer range configuration
- Restore the cached trigger ranges after an MCU reset
- HID: asus: add joysticks anti-deadzone configuration
- Restore the cached anti-deadzone values after an MCU reset
- HID: asus: add support for response curve
- Restore the cached response curves after an MCU reset
- Keep holding config_mutex until the curve packet has been sent
- HID: asus: add support to force feedback
- Serialize the cross-interface force-feedback sends with the gamepad
interface removal by holding intf_mutex in the callers
- HID: asus: add support for gamepad mode
- Restore the recorded gamepad mode after an MCU reset
- HID: asus: add support for turbo buttons
- Fix UAF allocating the turbo button entries with kcalloc
- Restore the cached turbo parameters after an MCU reset
- HID: asus: add support for btn remapping
- Keep the button mappings outside of the controller lifetime, so
that the recorded configuration survives a re-probe
- Re-apply the recorded mappings after an MCU reset
- Fetch the configuration through ally_get_config()
- v6
- HID: asus: add support for ROG Ally handhelds
- Add a maximum size check in ally_alloc_cmd()
- Drop the dead x_hdev snapshot taken in hid_asus_ally_raw_event()
- Drop the unused hdev parameter of handle_ctrl_alt_del()
- Drop the unused hdev/report parameters of ally_x_raw_event()
- Turn the "// F20;" comment into a proper "/* F20 */"
- HID: asus: add gamepad configuration
- Publish the configuration (ally->config under ally_data_lock)
before the sysfs groups are created and withdraw it on the error
path: attributes could become visible while ally_get_config()
still returned NULL and the restore path silently did nothing
- Record the Xbox controller mode in the first-setup defaults and
apply the recorded value from hid_asus_ally_init()
- properly rebase ally_alloc_cmd(): make it no change from previous patch
- HID: asus: add joysticks inner and outer range configuration
- Drop the unused cfg parameter of ally_set_joystick_thresholds()
- HID: asus: add triggers inner and outer range configuration
- Drop the unused cfg parameter of ally_set_trigger_ranges()
- Fix a commit message typo: "sensitivity"
- HID: asus: add support for response curve
- Add ally_resp_curve_is_valid() and use it in the init/resume
restore path exactly like the apply path does
- Drop the unused struct device * parameter of the four
response-curve helpers
- HID: asus: add support to force feedback
- Drop the unused pointers from ally_x_send_ff_report() and ally_x_send_ff_off()
- HID: asus: add support for gamepad mode
- Apply the mode exactly once from hid_asus_ally_init()
- HID: asus: add support for turbo buttons
- Remove the calibration groups created before a sysfs failure
- HID: asus: add support for btn remapping
- Do not gate remapping by turbo
- Reject NULL remap/macro pointers in ally_set_button_mapping()
- Carry the ally pointer in struct button_remap_attr
- Fix the "hypen" comment typo
- v7
- The "HID: asus: do not send keyboard init reports to touchpads" patch
is not part of this series anymore: it has been applied upstream as
8099f9da59f1 and the series is rebased on top of it
- HID: asus: add support for ROG Ally handhelds
- Reserve the probed interface role for the whole duration of its
sleeping initialization: a concurrent probe of the same role could
pass the singleton checks before the first one published its pointer
- HID: asus: add gamepad configuration
- Publish drvdata->rog_ally before the configuration attributes are
created: the callbacks fetched it before asus_probe() published it
and returned -ENODEV in the meantime
- Roll the configuration back with ally_config_remove() when
hid_asus_ally_init() fails, instead of leaking the created sysfs
groups and the button entries
- HID: asus: add support for response curve
- Replace response_curve_pct_1..4, response_curve_move_1..4 and the
apply attributes with one response_curve attribute per joystick that
takes the whole curve in a single validated all-or-nothing write:
staged partial edits reaching the MCU on resume cannot happen anymore
- Add a read-only response_curve_index attribute documenting the
meaning of each value
- Restore both curves unconditionally on init/resume: only validated
curves are recorded now
- HID: asus: add support for btn remapping
- Drop the stale cfg->turbo_support gate in ally_config_create():
remapping attributes are created for every device with button
directories, not only turbo-capable ones
- v8
- HID: asus: add support for ROG Ally handhelds
- Release the probe reservation when the interface pointer is
published: the flags were only cleared on failure, leaving them
stuck set after the first bind and rejecting every later probe
of that role
- Reserve the keyboard interface role too: its check and its
publication were two separate locked sections, so concurrent
probes of the same role could both publish
- Keep the recorded USB device while any probe is still in flight
- HID: asus: add gamepad configuration
- Report the missing Xbox controller capability with -EOPNOTSUPP
- HID: asus: add vibration strength configuration
- Wake pollers with sysfs_notify() after a successful write
- HID: asus: add joysticks inner and outer range configuration
- Wake pollers with sysfs_notify() after a successful write
- HID: asus: add triggers inner and outer range configuration
- Wake pollers with sysfs_notify() after a successful write
- HID: asus: add joysticks anti-deadzone configuration
- Wake pollers with sysfs_notify() after a successful write
- HID: asus: add support for response curve
- Wake pollers with sysfs_notify() after a successful curve write
- HID: asus: add support to force feedback
- Clear the rumble update latch on setup so a fresh interface
cannot inherit one left over from a teardown race
- HID: asus: add support for gamepad mode
- Return -EIO for invalid gamepad modes stored in the device
- Wake pollers with sysfs_notify() after a successful write
- HID: asus: add support for turbo buttons
- Wake pollers with sysfs_notify() after a successful write
- HID: asus: add support for btn remapping
- Return -EIO if a mapping is ever found unset instead of
dereferencing it
- Wake pollers with sysfs_notify() after remap and macro writes,
and after a mode switch re-defaults the mappings
- v9
- HID: asus: add support to force feedback
- Hold intf_mutex while unpublishing the gamepad interface in the
setup error path, like hid_asus_ally_remove() does
- Improve gamepad interface device lock around the initialization
force-feedback send
- HID: asus: add support for btn remapping
- Unpublish button_entries under config_mutex before removing the
attributes and freeing the array
- v10
- HID: asus: reinitialize the device after exiting a sleep state
- Dropped as already merged in XG Mobile patchset
- v11
- HID: asus: add support for ROG Ally handhelds
- Split the chained data[1] = data[3] = 0x00 assignment into two
statements
- Align the hid_asus_ally_raw_event() continuation line with the
opening parenthesis
- HID: asus: add gamepad configuration
- Add the missing blank line between struct hid_driver and
asus_init()
- HID: asus: add joysticks inner and outer range configuration
- Align the sysfs_remove_group() and hid_warn() continuation lines
with the opening parenthesis
- HID: asus: add triggers inner and outer range configuration
- Align the hid_warn() continuation lines with the opening
parenthesis
- HID: asus: add joysticks anti-deadzone configuration
- Align the hid_warn() continuation lines with the opening
parenthesis
- HID: asus: add support for response curve
- Drop the duplicate blank line before ally_config_attrs[]
- HID: asus: add support to force feedback
- Add the missing blank line before the static_assert
- Align the hid_warn() continuation line with the opening
parenthesis
- HID: asus: add support for btn remapping
- Align the hid_warn() and ally_set_button_mapping() continuation
lines with the opening parenthesis
- HID: asus: size feature reports from the HID report descriptor
- New patch
- HID: asus: identify XG Mobile stations and bind the LED to the
reported model
- New patch
- HID: asus: expose XG Mobile identity and firmware information
- New patch
- HID: asus: size ally gamepad commands from the HID report descriptor
- New patch
- platform/x86: asus-armoury: pick the eGPU activation from the
reported GPU
- New patch
- HID: asus: report the XG Mobile GPU vendor to asus-wmi
- New patch
- MAINTAINERS: add maintainer for the ASUS HID driver
- New patch
- v12
- HID: asus: size feature reports from the HID report descriptor
- First commit in the series. v11 had it after the Ally driver
- Look reports up in report_id_hash. asus_find_report() is that
lookup, and asus_has_report_id() is three calls to it after
rejecting an id of HID_MAX_IDS or more
- asus_kbd_get_functions() returns -EINVAL when the 0x5A report is
shorter than 7 bytes, before it reads the flags byte
- mcu_request_version() sizes the 0x0D response from the descriptor
and names the request FEATURE_KBD_REPORT_ID
- Remove ROG_ALLY_REPORT_SIZE as not needed anymore
- Include linux/container_of.h as asus_xgm_led_set() already calls
container_of()
- HID: asus: add support for ROG Ally handhelds
- One ally_intf per interface. Vendor keys are reported on the
input device of the interface that received the report
- The private gamepad input is registered and unregistered by that
interface.
- Remove BTN_TRIGGER_HAPPY as it was unused
- The rumble-off packet is the array ally_force_feedback_off (lowercase)
- Include linux/compiler_attributes.h for the packed report structs
- hid_asus_ally_probe() frees the interface with __free(kfree) and
returns no_free_ptr()
- Commands are allocated from the FEATURE_KBD_REPORT_ID length.
ally_dev_set_report() calls asus_kbd_set_report(), so a short
payload is padded to the descriptor length. The init rumble-off
packet starts with 0x0D and is padded to the length of report
0x0D. ally_dev_get_report() reads HID_ALLY_GET_REPORT_ID through
asus_kbd_get_report()
- ally_gamepad_check_ready() sleeps after a failed transfer as well
- HID_ALLY_X_INPUT_REPORT_SIZE is not defined.
hid_asus_ally_raw_event() does not take the unused struct
hid_report. The kernel-doc @ally lines are gone
- handle_ctrl_alt_del() names the boot-protocol offsets
(HID_INPUT_REPORT_ID_OFFSET, HID_INPUT_MODIFIER_OFFSET,
HID_INPUT_RESERVED_OFFSET, HID_INPUT_KEY_OFFSET) and the modifier
and key bytes. Its kernel-doc says true means the buffer was
rewritten and ally processing of that report stops
- KEY_PROG1 and KEY_F16-F19 are set in asus_input_mapping(), before
input_register_device(). KEY_F19 is the second event of usage
0x38
- HID: asus: add gamepad configuration
- The cache is the static ally_prefs. A comment on it says one set
is by design: the controller is part of the PC, and a second HID
device only appears when one is emulated with uhid
- Payloads passed to ally_alloc_cmd() are const
- hid_asus_ally_init() holds ally_prefs.lock with one guard().
ally_prefs_setup() does the same from the capability stores
through sysfs_live
- ally_sysfs_remove() is its own call and runs before hid_hw_stop
- The message names xbox_controller and says hid_asus_ally_reset_resume()
can fail with -ENODEV. The comment in that function about
initialization already having been performed is gone
- HID: asus: add vibration strength configuration
- The intensity fields, the attributes, and the restore path are
born on ally_prefs
- ally_set_vibration_intensity() has a kernel-doc comment. The lock
requirement is in the Context section
- HID: asus: add joysticks inner and outer range configuration
- The threshold fields, the attributes, and the restore path are
born on ally_prefs. The calibration groups are created only when
user_cal_support is set
- ally_sysfs_remove() takes ally_prefs.lock with guard() and holds
it until the function returns, so the calibration-group removal
reads the capability flags under the mutex
- HID: asus: add triggers inner and outer range configuration
- The trigger fields, the attributes, and the restore path are born
on ally_prefs, under the same calibration-group gate
- HID: asus: add joysticks anti-deadzone configuration
- The attributes are born on the joystick groups. The calibration
gate includes anti_deadzone_support
- The command buffer is declared with __free(kfree) on the
ally_alloc_cmd() line. The capability test stays above that
declaration
- HID: asus: add support for response curve
- The curves, the attributes, and the restore path are born on
ally_prefs. The calibration gate includes resp_curve_support
- response_curve_store() parses eight values with
ally_parse_u8_token(), which copies each token out of the write,
bounds it by count, and parses it with kstrtou8(). After the
eighth value every remaining byte is checked, and a non-space
byte, including an embedded NUL, is rejected
- ally_set_joystick_resp_curve() has a kernel-doc comment, with the
lock requirement in the Context section
- HID: asus: add support to force feedback
- Rumble state, the work, and rumble-off live on the gamepad
interface. The off packet is sent on that interface's own hdev.
There is no snapshotted ally_x_hdev
- ally_ff_quiesce() runs before hid_hw_stop
- select INPUT_FF_MEMLESS in config HID_ASUS is indented with a tab
- HID: asus: add support for gamepad mode
- Changed gamepad_mode_index to gamepad_mode_available to follow conventions
- The mode byte and the attribute are born on ally_prefs.
ally_prefs_setup() stores ALLY_GAMEPAD_MODE_GAMEPAD as the
initial mode
- The command buffer is declared with __free(kfree) on the
ally_alloc_cmd() line
- gamepad_mode_store() returns the sysfs_match_string() error for
an unknown mode and does not log it
- ally_set_gamepad_mode() has a kernel-doc comment, with the lock
requirement in the Context section
- HID: asus: add support for turbo buttons
- The turbo parameters and attributes are born on ally_prefs. The
button attributes do not carry an Ally back-pointer
- HID: asus: add support for btn remapping
- Changed btn_remap_index to btn_remap_available to follow conventions
- The mapping cache and the attributes are born on ally_prefs. The
button attributes do not carry an Ally back-pointer. Mappings are
applied on the configuration interface under ally_prefs.lock
- ally_encode_button_code() writes one code into a slot of
ASUS_ALLY_BTN_FUNCTIONS_LEN (11) bytes. ally_set_button_mapping()
calls it at offsets 5, 16, 27 and 38. A type of 0 writes only the
type byte
- ally_apply_button_mappings() says the caller holds
ally_prefs.lock
- gamepad_mode_store() takes that mutex once, with guard(), and
holds it until the function returns. The mode update, the
default-mapping refresh, and the notifies all run under that
lock. The store notifies remap_index together with remap and
macro
- button_remap_store() restores the previous code through
err_restore. On a first-generation device, a pair that already
reached the hardware is pushed back through err_restore_hw
- HID: asus: identify XG Mobile stations and bind the LED to the
reported model
- Identity fields are copied by tag (MB, SN, PRO, PNUM)
- The LED name is asus:xgm-<model>-<uniq>:led. The second field is
hdev->uniq when that string is non-empty, and dev_name(&hdev->dev)
otherwise
- ASUS_XGM_QUERY_VERSION is not defined in this commit
- asus_xgm_get_identity() is tried three times, with ssleep(1)
between attempts. The LED stays unregistered only when all three
fail. An unsupported model is not retried
- HID: asus: expose XG Mobile identity and firmware information
- ASUS_XGM_QUERY_VERSION is defined next to the identity query
- A version field is a 3-character tag, a length byte, and the data
- HID: asus: document ROG XG Mobile sysfs attributes
- New patch. Documents xgm_model, xgm_serial, xgm_mainboard,
xgm_part_number, xgm_firmware_version and xgm_generation
- platform/x86: asus-armoury: pick the eGPU activation from the
reported GPU
- This commit also reports the GPU vendor. v11 had that report in
the following commit
- The version payload's four PCI IDs are stored high byte first.
asus_wmi_egpu_set_vendor() receives the 16-bit vendor
- The message records that the WMI method returned 1 for every
activation value, that 0x0001, 0x0101 and 0x0201 are NVIDIA, AMD
and Intel, and that known tools only write 1
- Removing the station stores 0, and the next activation uses the
default
- egpu_vendor_lock covers that one word. The armoury store reads
the vendor inside egpu_mutex. The getter takes egpu_vendor_lock
and drops it before returning
- MAINTAINERS: add maintainer for the ASUS HID driver
- The entry also lists Documentation/ABI/testing/sysfs-driver-hid-asus
HID: asus: size ally gamepad commands from the HID report descriptor
- Dropped. The descriptor-length commit sizes mcu_request_version(),
and the Ally commits allocate their commands from
FEATURE_KBD_REPORT_ID
HID: asus: report the XG Mobile GPU vendor to asus-wmi
- Dropped as its own commit. Joined into the armoury commit
Denis Benato (17):
HID: asus: size feature reports from the HID report descriptor
HID: asus: add support for ROG Ally handhelds
HID: asus: add gamepad configuration
HID: asus: add vibration strength configuration
HID: asus: add joysticks inner and outer range configuration
HID: asus: add triggers inner and outer range configuration
HID: asus: add joysticks anti-deadzone configuration
HID: asus: add support for response curve
HID: asus: add support to force feedback
HID: asus: add support for gamepad mode
HID: asus: add support for turbo buttons
HID: asus: add support for btn remapping
HID: asus: identify XG Mobile stations and bind the LED to the
reported model
HID: asus: expose XG Mobile identity and firmware information
HID: asus: document ROG XG Mobile sysfs attributes
platform/x86: asus-armoury: pick the eGPU activation from the reported
GPU
MAINTAINERS: add maintainer for the ASUS HID driver
.../ABI/testing/sysfs-driver-hid-asus | 95 +
MAINTAINERS | 7 +
drivers/hid/Kconfig | 1 +
drivers/hid/hid-asus.c | 4382 ++++++++++++++++-
drivers/platform/x86/asus-armoury.c | 57 +-
drivers/platform/x86/asus-wmi.c | 43 +
include/linux/platform_data/x86/asus-wmi.h | 11 +
7 files changed, 4491 insertions(+), 105 deletions(-)
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-asus
--
2.47.3
^ permalink raw reply [flat|nested] 29+ messages in thread
* [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 18:04 ` sashiko-bot
2026-10-31 18:30 ` Rebecca Mara Müller
2026-10-31 17:41 ` [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (15 subsequent siblings)
16 siblings, 2 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The ASUS vendor protocol uses the same feature report IDs across very
different devices: on N-KEY keyboards and ROG Ally report 0x5A/0x5E are
64-byte reports, while on XG Mobile stations report 0x5E is a 300-byte
report. The driver currently hardcodes transfer lengths (16 bytes for
the handshake, FEATURE_KBD_REPORT_SIZE for reads, ROG_XGM_REPORT_SIZE
for the XG Mobile LED, ROG_ALLY_REPORT_SIZE for the MCU version read),
which mismatches what the hardware expects and would break if a new
device used yet another size for the same report ID.
Derive the transfer length of every feature report from the HID report
descriptor of the device at hand:
- add asus_report_len(), which resolves a report ID to its struct
hid_report and returns hid_report_len();
- asus_kbd_set_report() now zero-pads short payloads to the exact
report size and rejects payloads that do not fit the report;
- add asus_kbd_get_report() and use it from asus_kbd_init() and
asus_kbd_get_functions() so reads are sized per report as well;
- asus_xgm_led_set() sends a minimal payload and checks the transfer
result against the descriptor length instead of the hardcoded
ROG_XGM_REPORT_SIZE, which is removed along with the fixed-size
buffers in asus_kbd_set_fn_lock() and asus_kbd_set_brightness();
- mcu_request_version() sizes the 0x0d response from the descriptor
and names the keyboard feature report FEATURE_KBD_REPORT_ID.
ROG_ALLY_REPORT_SIZE has no remaining user and is removed.
Captured USB traffic of the same commands issued by Windows shows they
are always written at the full report length, which this change now
matches.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 181 ++++++++++++++++++++++++++++++-----------
1 file changed, 135 insertions(+), 46 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index f6aa1be09ad5..327bb4ab1a0d 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -22,6 +22,7 @@
#include <linux/acpi.h>
#include <linux/cleanup.h>
+#include <linux/container_of.h>
#include <linux/dmi.h>
#include <linux/hid.h>
#include <linux/module.h>
@@ -30,6 +31,7 @@
#include <linux/usb.h> /* For to_usb_interface for T100 touchpad intf check */
#include <linux/power_supply.h>
#include <linux/leds.h>
+#include <linux/types.h>
#include "hid-ids.h"
@@ -51,9 +53,6 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define FEATURE_KBD_LED_REPORT_ID1 0x5d
#define FEATURE_KBD_LED_REPORT_ID2 0x5e
-#define ROG_XGM_REPORT_SIZE 300
-
-#define ROG_ALLY_REPORT_SIZE 64
#define ROG_ALLY_X_MIN_MCU 313
#define ROG_ALLY_MIN_MCU 319
@@ -582,20 +581,104 @@ static int asus_raw_event(struct hid_device *hdev,
return 0;
}
+/**
+ * asus_find_report() - Find a report of the given type by its ID
+ * @hdev: HID device
+ * @type: report type (HID_INPUT_REPORT, HID_OUTPUT_REPORT or
+ * HID_FEATURE_REPORT)
+ * @report_id: report ID to look for
+ *
+ * Return: the report with the given ID, or NULL if the device does not
+ * declare such a report.
+ */
+static struct hid_report *asus_find_report(struct hid_device *hdev,
+ unsigned int type, u8 report_id)
+{
+ return hdev->report_enum[type].report_id_hash[report_id];
+}
+
+/**
+ * asus_report_len() - Length of a feature report for the device
+ * @hdev: HID device
+ * @report_id: report ID of the feature report
+ *
+ * The same ASUS report ID can have different sizes depending on the device:
+ * report 0x5E is a 64-byte report on laptops but a 300-byte report on XG
+ * Mobile stations. Always size transfers from the report descriptor of the
+ * device at hand instead of hardcoding lengths.
+ *
+ * Return: the length of the feature report in bytes, or 0 if the device
+ * does not declare the report.
+ */
+static size_t asus_report_len(struct hid_device *hdev, u8 report_id)
+{
+ struct hid_report *report;
+
+ report = asus_find_report(hdev, HID_FEATURE_REPORT, report_id);
+
+ return report ? hid_report_len(report) : 0;
+}
+
static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size)
{
- u8 *dmabuf __free(kfree) = kmemdup(buf, buf_size, GFP_KERNEL);
+ size_t hw_len = asus_report_len(hdev, buf[0]);
+
+ /* The payload (including the report ID) must fit the device report. */
+ if (!hw_len || buf_size > hw_len)
+ return -EINVAL;
+
+ u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
if (!dmabuf)
return -ENOMEM;
+ memcpy(dmabuf, buf, buf_size);
+
/*
- * The report ID should be set from the incoming buffer due to LED and key
- * interfaces having different pages
+ * The report ID should be set from the incoming buffer due to LED and
+ * key interfaces having different pages. Short payloads are zero-padded
+ * to the exact report size the hardware expects for this report ID.
*/
- return hid_hw_raw_request(hdev, buf[0], dmabuf, buf_size, HID_FEATURE_REPORT,
+ return hid_hw_raw_request(hdev, buf[0], dmabuf, hw_len, HID_FEATURE_REPORT,
HID_REQ_SET_REPORT);
}
+/**
+ * asus_kbd_get_report() - Read a report sized from the descriptor
+ * @hdev: HID device
+ * @report_id: report ID to read
+ * @buf: buffer to store the report in
+ * @buf_size: size of @buf
+ *
+ * The transfer is performed at the exact report size the device declares
+ * for @report_id. The device may answer with a short read, and only the
+ * bytes that fit in @buf are copied into it.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_kbd_get_report(struct hid_device *hdev, u8 report_id,
+ u8 *buf, size_t buf_size)
+{
+ size_t hw_len = asus_report_len(hdev, report_id);
+ int ret;
+
+ if (!hw_len)
+ return -EINVAL;
+
+ u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ ret = hid_hw_raw_request(hdev, report_id, dmabuf, hw_len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (ret < 0)
+ return ret;
+
+ /* The device may return less than requested (short read). */
+ memcpy(buf, dmabuf, min(buf_size, hw_len));
+
+ return 0;
+}
+
static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
{
/*
@@ -604,8 +687,13 @@ static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
*/
const u8 buf[] = { report_id, 0x41, 0x53, 0x55, 0x53, 0x20, 0x54,
0x65, 0x63, 0x68, 0x2e, 0x49, 0x6e, 0x63, 0x2e, 0x00 };
+ size_t hw_len = asus_report_len(hdev, report_id);
+ size_t cmp_len;
int ret;
+ if (!hw_len)
+ return -EINVAL;
+
ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
if (ret < 0) {
hid_err(hdev, "Asus handshake %02x failed to send: %d\n",
@@ -613,19 +701,20 @@ static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
return ret;
}
- u8 *readbuf __free(kfree) = kzalloc(FEATURE_KBD_REPORT_SIZE, GFP_KERNEL);
+ u8 *readbuf __free(kfree) = kzalloc(hw_len, 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);
+ ret = asus_kbd_get_report(hdev, report_id, readbuf, hw_len);
if (ret < 0) {
hid_warn(hdev, "Asus handshake %02x failed to receive ack: %d\n",
report_id, ret);
- } else if (memcmp(readbuf, buf, sizeof(buf)) != 0) {
- hid_warn(hdev, "Asus handshake %02x returned invalid response: %*ph\n",
- report_id, FEATURE_KBD_REPORT_SIZE, readbuf);
+ } else {
+ cmp_len = min_t(size_t, sizeof(buf), hw_len);
+ if (memcmp(readbuf, buf, cmp_len) != 0) {
+ hid_warn(hdev, "Asus handshake %02x returned invalid response: %*ph\n",
+ report_id, (int)cmp_len, readbuf);
+ }
}
/*
@@ -640,32 +729,35 @@ static int asus_kbd_get_functions(struct hid_device *hdev,
u8 report_id)
{
const u8 buf[] = { report_id, 0x05, 0x20, 0x31, 0x00, 0x08 };
- u8 *readbuf;
+ size_t hw_len = asus_report_len(hdev, report_id);
int ret;
+ /*
+ * Function flags are at byte 6. A shorter report does not
+ * contain that byte.
+ */
+ if (hw_len < 7)
+ return -EINVAL;
+
ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
if (ret < 0) {
hid_err(hdev, "Asus failed to send configuration command: %d\n", ret);
return ret;
}
- readbuf = kzalloc(FEATURE_KBD_REPORT_SIZE, GFP_KERNEL);
+ u8 *readbuf __free(kfree) = kzalloc(hw_len, 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);
+ ret = asus_kbd_get_report(hdev, report_id, readbuf, hw_len);
if (ret < 0) {
hid_err(hdev, "Asus failed to request functions: %d\n", ret);
- kfree(readbuf);
return ret;
}
*kbd_func = readbuf[6];
- kfree(readbuf);
- return ret;
+ return 0;
}
static int asus_kbd_disable_oobe(struct hid_device *hdev)
@@ -690,7 +782,7 @@ static int asus_kbd_disable_oobe(struct hid_device *hdev)
static void asus_kbd_set_fn_lock(struct hid_device *hdev, bool enabled)
{
- const u8 buf[FEATURE_KBD_REPORT_SIZE] = { FEATURE_KBD_REPORT_ID, 0xd0, 0x4e, !!enabled };
+ const u8 buf[] = { FEATURE_KBD_REPORT_ID, 0xd0, 0x4e, !!enabled };
int ret;
ret = asus_kbd_set_report(hdev, buf, sizeof(buf));
@@ -700,7 +792,7 @@ static void asus_kbd_set_fn_lock(struct hid_device *hdev, bool enabled)
static void asus_kbd_set_brightness(struct hid_device *hdev, u8 brightness)
{
- const u8 buf[FEATURE_KBD_REPORT_SIZE] = {
+ const u8 buf[] = {
FEATURE_KBD_REPORT_ID, 0xba, 0xc5, 0xc4, brightness
};
int ret;
@@ -900,10 +992,15 @@ static int mcu_parse_version_string(const u8 *response, size_t response_size)
static int mcu_request_version(struct hid_device *hdev)
{
- u8 *response __free(kfree) = kzalloc(ROG_ALLY_REPORT_SIZE, GFP_KERNEL);
- const u8 request[] = { 0x5a, 0x05, 0x03, 0x31, 0x00, 0x20 };
+ size_t hw_len = asus_report_len(hdev, FEATURE_REPORT_ID);
+ const u8 request[] = { FEATURE_KBD_REPORT_ID, 0x05, 0x03, 0x31, 0x00, 0x20 };
int ret;
+ if (!hw_len)
+ return -EINVAL;
+
+ u8 *response __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
+
if (!response)
return -ENOMEM;
@@ -911,17 +1008,16 @@ static int mcu_request_version(struct hid_device *hdev)
if (ret < 0)
return ret;
- ret = hid_hw_raw_request(hdev, FEATURE_REPORT_ID, response,
- ROG_ALLY_REPORT_SIZE, HID_FEATURE_REPORT,
- HID_REQ_GET_REPORT);
+ ret = hid_hw_raw_request(hdev, FEATURE_REPORT_ID, response, hw_len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
if (ret < 0)
return ret;
- ret = mcu_parse_version_string(response, ROG_ALLY_REPORT_SIZE);
+ ret = mcu_parse_version_string(response, hw_len);
if (ret < 0) {
pr_err("Failed to parse MCU version: %d\n", ret);
print_hex_dump(KERN_ERR, "MCU: ", DUMP_PREFIX_NONE,
- 16, 1, response, ROG_ALLY_REPORT_SIZE, false);
+ 16, 1, response, hw_len, false);
}
return ret;
@@ -958,17 +1054,12 @@ static void validate_mcu_fw_version(struct hid_device *hdev, int idProduct)
static bool asus_has_report_id(struct hid_device *hdev, u16 report_id)
{
- struct hid_report *report;
- int t;
+ if (report_id >= HID_MAX_IDS)
+ return false;
- for (t = HID_INPUT_REPORT; t <= HID_FEATURE_REPORT; t++) {
- list_for_each_entry(report, &hdev->report_enum[t].report_list, list) {
- if (report->id == report_id)
- return true;
- }
- }
-
- return false;
+ return asus_find_report(hdev, HID_INPUT_REPORT, report_id) ||
+ asus_find_report(hdev, HID_OUTPUT_REPORT, report_id) ||
+ asus_find_report(hdev, HID_FEATURE_REPORT, report_id);
}
static int asus_kbd_register_leds(struct hid_device *hdev)
@@ -1171,17 +1262,15 @@ static int asus_battery_probe(struct hid_device *hdev)
static int asus_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value)
{
- const u8 buf[ROG_XGM_REPORT_SIZE] = {
- FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00
- };
+ const u8 buf[] = { FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00 };
struct asus_xgm_led *xgm = container_of(led_cdev, struct asus_xgm_led, cdev);
int ret;
- ret = asus_kbd_set_report(xgm->hdev, buf, ROG_XGM_REPORT_SIZE);
+ ret = asus_kbd_set_report(xgm->hdev, buf, sizeof(buf));
if (ret < 0) {
hid_err(xgm->hdev, "Unable to set XG mobile led state: %d\n", ret);
return ret;
- } else if (ret != ROG_XGM_REPORT_SIZE) {
+ } else if (ret != (int)asus_report_len(xgm->hdev, FEATURE_KBD_LED_REPORT_ID2)) {
hid_err(xgm->hdev, "Unexpected partial transfer to XG mobile: %d\n", ret);
return -EIO;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
2026-10-31 17:41 ` [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 18:11 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 03/17] HID: asus: add gamepad configuration Denis Benato
` (14 subsequent siblings)
16 siblings, 1 reply; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
Implement the core functionality for ASUS handhelds:
- extend functionality of hid-asus if driver is enabled
- initialise the device to a "ready" state
- implement Ally X input mapping
- emit a single proper event for AC button long press
Each interface keeps its own state and reports on its own evdev.
Signed-off-by: Khamunetri Clark <khamunetriclark@gmail.com>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Matthew Schwartz <matthew.schwartz@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
---
drivers/hid/hid-asus.c | 969 ++++++++++++++++++++++++++++++++++++-----
1 file changed, 870 insertions(+), 99 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 327bb4ab1a0d..552d3b25a1d1 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -22,16 +22,24 @@
#include <linux/acpi.h>
#include <linux/cleanup.h>
+#include <linux/compiler_attributes.h>
#include <linux/container_of.h>
#include <linux/dmi.h>
+#include <linux/err.h>
#include <linux/hid.h>
+#include <linux/jiffies.h>
+#include <linux/list.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/platform_data/x86/asus-wmi.h>
+#include <linux/types.h>
#include <linux/input/mt.h>
#include <linux/usb.h> /* For to_usb_interface for T100 touchpad intf check */
#include <linux/power_supply.h>
+#include <linux/stddef.h>
#include <linux/leds.h>
#include <linux/types.h>
+#include <linux/unaligned.h>
#include "hid-ids.h"
@@ -39,6 +47,9 @@ MODULE_AUTHOR("Yusuke Fujimaki <usk.fujimaki@gmail.com>");
MODULE_AUTHOR("Brendan McGrath <redmcg@redmandi.dyndns.org>");
MODULE_AUTHOR("Victor Vlasenko <victor.vlasenko@sysgears.com>");
MODULE_AUTHOR("Frederik Wenigwieser <frederik.wenigwieser@gmail.com>");
+MODULE_AUTHOR("Denis Benato <denis.benato@linux.dev>");
+MODULE_AUTHOR("Luke Jones <luke@ljones.dev>");
+MODULE_AUTHOR("Khamunetri Clark <khamunetriclark@gmail.com>");
MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define T100_TPAD_INTF 2
@@ -56,6 +67,30 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ROG_ALLY_X_MIN_MCU 313
#define ROG_ALLY_MIN_MCU 319
+#define HID_ALLY_INTF_KEYBOARD_IN 0x81
+#define HID_ALLY_INTF_CFG_IN 0x83
+#define HID_ALLY_X_INTF_IN 0x87
+
+#define HID_ALLY_GET_REPORT_ID 0x0D
+#define HID_ALLY_FEATURE_CODE_PAGE 0xD1
+
+#define HID_ALLY_X_INPUT_REPORT 0x0B
+
+#define HID_ALLY_READY_MAX_TRIES 6
+
+#define HID_INPUT_KEY_DELETE 0x4c
+#define HID_INPUT_KEY_F20 0x6f
+#define HID_INPUT_LCTRL_MODIFIER 0x01
+#define HID_INPUT_LALT_MODIFIER 0x04
+#define HID_INPUT_LCTRL_LALT (HID_INPUT_LCTRL_MODIFIER | HID_INPUT_LALT_MODIFIER)
+
+#define HID_INPUT_REPORT_ID_OFFSET 0
+#define HID_INPUT_MODIFIER_OFFSET 1
+#define HID_INPUT_RESERVED_OFFSET 2
+#define HID_INPUT_KEY_OFFSET 3
+
+#define ASUS_HID_INPUT_KBD_REPORT_ID 0x01
+
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
#define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -160,6 +195,17 @@ struct asus_touchpad_info {
int report_size;
};
+struct ally_intf {
+ struct hid_device *hdev;
+ /* HID-core evdev of this interface. Reports for it land here. */
+ struct input_dev *input;
+ /* Private gamepad node for report 0x0B. NULL on the other interfaces. */
+ struct input_dev *gamepad;
+ u8 cad_state;
+ unsigned long cad_last_event_time;
+ struct delayed_work resume_work;
+};
+
struct asus_drvdata {
unsigned long quirks;
struct hid_device *hdev;
@@ -167,6 +213,7 @@ struct asus_drvdata {
struct input_dev *tp_kbd_input;
struct asus_worker *worker;
unsigned int kbd_backlight_brightness;
+ struct ally_intf *ally;
const struct asus_touchpad_info *tp;
struct power_supply *battery;
struct power_supply_desc battery_desc;
@@ -239,12 +286,761 @@ static const struct asus_touchpad_info medion_e1239t_tp = {
.report_size = 32 /* 2 byte header + 5 * 5 + 5 byte footer */,
};
+enum ally_command_codes {
+ CMD_SET_GAMEPAD_MODE = 0x01,
+ CMD_SET_MAPPING = 0x02,
+ CMD_SET_JOYSTICK_MAPPING = 0x03,
+ CMD_SET_JOYSTICK_DEADZONE = 0x04,
+ CMD_SET_TRIGGER_RANGE = 0x05,
+ CMD_SET_VIBRATION_INTENSITY = 0x06,
+ CMD_LED_CONTROL = 0x08,
+ CMD_CHECK_READY = 0x0A,
+ CMD_SET_XBOX_CONTROLLER = 0x0B,
+ CMD_CHECK_XBOX_SUPPORT = 0x0C,
+ CMD_USER_CAL_DATA = 0x0D,
+ CMD_CHECK_USER_CAL_SUPPORT = 0x0E,
+ CMD_SET_TURBO_PARAMS = 0x0F,
+ CMD_CHECK_TURBO_SUPPORT = 0x10,
+ CMD_CHECK_RESP_CURVE_SUPPORT = 0x12,
+ CMD_SET_RESP_CURVE = 0x13,
+ CMD_CHECK_DIR_TO_BTN_SUPPORT = 0x14,
+ CMD_SET_GYRO_PARAMS = 0x15,
+ CMD_CHECK_GYRO_TO_JOYSTICK = 0x16,
+ CMD_CHECK_ANTI_DEADZONE = 0x17,
+ CMD_SET_ANTI_DEADZONE = 0x18,
+};
+
+static const u8 ally_force_feedback_off[] = {
+ 0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB
+};
+
+/*
+ * Each Ally interface owns its own ally_intf. A report is delivered on the
+ * evdev of the interface that received it. Nothing here points at another
+ * interface.
+ */
+static void ally_release_vendor_keys(struct input_dev *input)
+{
+ if (!input)
+ return;
+
+ input_report_key(input, KEY_F16, 0);
+ input_report_key(input, KEY_F17, 0);
+ input_report_key(input, KEY_F18, 0);
+ input_report_key(input, KEY_F19, 0);
+ input_report_key(input, KEY_PROG1, 0);
+ input_sync(input);
+}
+
+static void ally_resume_work_fn(struct work_struct *work)
+{
+ struct ally_intf *intf = container_of(work, struct ally_intf,
+ resume_work.work);
+
+ /*
+ * Force release vendor buttons to prevent stuck keys on resume
+ * (workaround for Ally X USB re-probing during suspend/resume).
+ */
+ ally_release_vendor_keys(intf->input);
+}
+
+static void ally_quiesce(struct ally_intf *intf)
+{
+ if (!intf)
+ return;
+
+ cancel_delayed_work_sync(&intf->resume_work);
+}
+
+static void ally_free(struct ally_intf *intf)
+{
+ if (!intf)
+ return;
+
+ if (intf->gamepad)
+ input_unregister_device(intf->gamepad);
+ kfree(intf);
+}
+
+/**
+ * handle_ctrl_alt_del() - detect a left button long press
+ * @intf: ally interface holding the sequence state
+ * @data: raw report buffer, rewritten in place when the sequence matches
+ * @size: length of @data in bytes
+ *
+ * The Ally left button emits a sequence of ctrl+alt+del events. Capture that
+ * and emit only a single code for that single event by rewriting @data in
+ * place. The return value tells the caller whether ally processing of this
+ * report should stop.
+ *
+ * Return: true when this step was recognized and @data was rewritten, so the
+ * caller stops and the generic parser consumes the rewritten report. false
+ * when @data was left unchanged and ally processing should continue.
+ */
+static bool handle_ctrl_alt_del(struct ally_intf *intf, u8 *data, int size)
+{
+ bool time_is_past = time_after(jiffies,
+ intf->cad_last_event_time + msecs_to_jiffies(100));
+
+ if (size < 16 || data[HID_INPUT_REPORT_ID_OFFSET] != ASUS_HID_INPUT_KBD_REPORT_ID)
+ return false;
+
+ if (intf->cad_state > 0 && time_is_past)
+ intf->cad_state = 0;
+
+ intf->cad_last_event_time = jiffies;
+ switch (intf->cad_state) {
+ case 0:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_MODIFIER) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == 0x00)) {
+ intf->cad_state = 1;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ case 1:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_LALT) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == 0x00)) {
+ intf->cad_state = 2;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ case 2:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LCTRL_LALT) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) {
+ intf->cad_state = 3;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ data[HID_INPUT_KEY_OFFSET] = HID_INPUT_KEY_F20;
+ return true;
+ }
+ break;
+ case 3:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == HID_INPUT_LALT_MODIFIER) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) {
+ intf->cad_state = 4;
+ data[HID_INPUT_MODIFIER_OFFSET] = 0x00;
+ data[HID_INPUT_KEY_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ case 4:
+ if ((data[HID_INPUT_MODIFIER_OFFSET] == 0x00) &&
+ (data[HID_INPUT_RESERVED_OFFSET] == 0x00) &&
+ (data[HID_INPUT_KEY_OFFSET] == HID_INPUT_KEY_DELETE)) {
+ intf->cad_state = 5;
+ data[HID_INPUT_KEY_OFFSET] = 0x00;
+ return true;
+ }
+ break;
+ }
+ intf->cad_state = 0;
+ return false;
+}
+
+/**
+ * ally_vendor_keys() - Report a vendor button on this interface's evdev
+ * @input: HID-core input device of the interface that received the report
+ * @data: raw report buffer
+ * @size: length of @data in bytes
+ *
+ * The key is down while @data[1] matches that code. The same decode is used
+ * for every Ally interface: 0x38 is the ROG key (KEY_PROG1 and KEY_F19).
+ *
+ * Return: true if the report was a known vendor button.
+ */
+static bool ally_vendor_keys(struct input_dev *input, const u8 *data, int size)
+{
+ if (size < 2 || data[0] != FEATURE_KBD_REPORT_ID)
+ return false;
+
+ if (data[1] != 0x38 && data[1] != 0xA6 && data[1] != 0xA7 &&
+ data[1] != 0xA8)
+ return false;
+
+ if (!input)
+ return true;
+
+ input_report_key(input, KEY_PROG1, data[1] == 0x38);
+ input_report_key(input, KEY_F16, data[1] == 0xA6);
+ input_report_key(input, KEY_F17, data[1] == 0xA7);
+ input_report_key(input, KEY_F18, data[1] == 0xA8);
+ input_report_key(input, KEY_F19, data[1] == 0x38);
+ input_sync(input);
+ return true;
+}
+
+/**
+ * asus_find_report() - Find a report of the given type by its ID
+ * @hdev: HID device
+ * @type: report type (HID_INPUT_REPORT, HID_OUTPUT_REPORT or
+ * HID_FEATURE_REPORT)
+ * @report_id: report ID to look for
+ *
+ * Return: the report with the given ID, or NULL if the device does not
+ * declare such a report.
+ */
+static struct hid_report *asus_find_report(struct hid_device *hdev,
+ unsigned int type, u8 report_id)
+{
+ return hdev->report_enum[type].report_id_hash[report_id];
+}
+
+/**
+ * asus_report_len() - Length of a feature report for the device
+ * @hdev: HID device
+ * @report_id: report ID of the feature report
+ *
+ * The same ASUS report ID can have different sizes depending on the device:
+ * report 0x5E is a 64-byte report on laptops but a 300-byte report on XG
+ * Mobile stations. Always size transfers from the report descriptor of the
+ * device at hand instead of hardcoding lengths.
+ *
+ * Return: the length of the feature report in bytes, or 0 if the device
+ * does not declare the report.
+ */
+static size_t asus_report_len(struct hid_device *hdev, u8 report_id)
+{
+ struct hid_report *report;
+
+ report = asus_find_report(hdev, HID_FEATURE_REPORT, report_id);
+
+ return report ? hid_report_len(report) : 0;
+}
+
+static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size)
+{
+ size_t hw_len = asus_report_len(hdev, buf[0]);
+
+ /* The payload (including the report ID) must fit the device report. */
+ if (!hw_len || buf_size > hw_len)
+ return -EINVAL;
+
+ u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ memcpy(dmabuf, buf, buf_size);
+
+ /*
+ * The report ID should be set from the incoming buffer due to LED and
+ * key interfaces having different pages. Short payloads are zero-padded
+ * to the exact report size the hardware expects for this report ID.
+ */
+ return hid_hw_raw_request(hdev, buf[0], dmabuf, hw_len, HID_FEATURE_REPORT,
+ HID_REQ_SET_REPORT);
+}
+
+/**
+ * asus_kbd_get_report() - Read a report sized from the descriptor
+ * @hdev: HID device
+ * @report_id: report ID to read
+ * @buf: buffer to store the report in
+ * @buf_size: size of @buf
+ *
+ * The transfer is performed at the exact report size the device declares
+ * for @report_id. The device may answer with a short read, and only the
+ * bytes that fit in @buf are copied into it.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_kbd_get_report(struct hid_device *hdev, u8 report_id,
+ u8 *buf, size_t buf_size)
+{
+ size_t hw_len = asus_report_len(hdev, report_id);
+ int ret;
+
+ if (!hw_len)
+ return -EINVAL;
+
+ u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
+ if (!dmabuf)
+ return -ENOMEM;
+
+ ret = hid_hw_raw_request(hdev, report_id, dmabuf, hw_len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+ if (ret < 0)
+ return ret;
+
+ /* The device may return less than requested (short read). */
+ memcpy(buf, dmabuf, min(buf_size, hw_len));
+
+ return 0;
+}
+
static const u8 asus_report_id_init[] = {
FEATURE_KBD_REPORT_ID,
FEATURE_KBD_LED_REPORT_ID1,
FEATURE_KBD_LED_REPORT_ID2
};
+static int ally_dev_set_report(struct hid_device *hdev, const u8 *buf, size_t len)
+{
+ return asus_kbd_set_report(hdev, buf, len);
+}
+
+static int ally_dev_get_report(struct hid_device *hdev, u8 *out, size_t len)
+{
+ return hid_hw_raw_request(hdev, HID_ALLY_GET_REPORT_ID, out, len,
+ HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
+}
+
+/**
+ * ally_gamepad_send_packet() - Send a raw packet to the gamepad device
+ * @hdev: HID device
+ * @buf: buffer containing the packet data
+ * @len: length of data to send
+ *
+ * Return: count of data transferred, negative if error.
+ */
+static int ally_gamepad_send_packet(struct hid_device *hdev, const u8 *buf, size_t len)
+{
+ return ally_dev_set_report(hdev, buf, len);
+}
+
+/**
+ * ally_gamepad_send_receive_packet() - Send a packet and receive the response
+ * @hdev: HID device
+ * @buf: buffer containing the packet data to send and receive response in
+ * @len: length of buffer
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_gamepad_send_receive_packet(struct hid_device *hdev,
+ u8 *buf, size_t len)
+{
+ int ret;
+
+ ret = ally_dev_set_report(hdev, buf, len);
+ if (ret >= 0) {
+ memset(buf, 0, len);
+ ret = ally_dev_get_report(hdev, buf, len);
+ }
+
+ return ret;
+}
+
+/**
+ * ally_alloc_cmd() - Construct a command buffer for the gamepad
+ * @hdev: HID device the command is built for
+ * @cmd: Command code to send
+ * @payload: Optional payload data to include in the command
+ * @payload_size: Size of the payload data
+ * @size: Where to store the length of the allocated buffer
+ *
+ * The buffer is sized after the feature report the device declares for
+ * the gamepad commands, and it is the caller responsibility to free the
+ * buffer using kfree().
+ *
+ * Return: the newly allocated buffer containing the command, or NULL on
+ * allocation failure or if the payload would not fit in the report.
+ */
+static u8 *ally_alloc_cmd(struct hid_device *hdev, u8 cmd, const u8 *payload,
+ u8 payload_size, size_t *size)
+{
+ size_t hw_len = asus_report_len(hdev, FEATURE_KBD_REPORT_ID);
+ u8 *hidbuf;
+
+ /*
+ * The payload is written after the 4-byte command header:
+ * reject one that would spill past the end of the report.
+ */
+ if (hw_len < 4 || payload_size > hw_len - 4)
+ return NULL;
+
+ hidbuf = kzalloc(hw_len, GFP_KERNEL);
+ if (!hidbuf)
+ return NULL;
+
+ hidbuf[0] = FEATURE_KBD_REPORT_ID;
+ hidbuf[1] = HID_ALLY_FEATURE_CODE_PAGE;
+ hidbuf[2] = cmd;
+ hidbuf[3] = payload_size;
+
+ if (payload_size > 0 && payload)
+ memcpy(&hidbuf[4], payload, payload_size);
+
+ *size = hw_len;
+
+ return hidbuf;
+}
+
+/**
+ * ally_gamepad_check_ready() - Wait for the gamepad MCU to report ready
+ * @hdev: HID device
+ *
+ * This should be called before any remapping attempts, and on driver
+ * init/resume, after the asus handshake has been performed on the
+ * configuration endpoint.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_gamepad_check_ready(struct hid_device *hdev)
+{
+ const u8 payload[] = { 0x00 };
+ size_t buf_len;
+ int ret;
+
+ for (int i = 0; i < HID_ALLY_READY_MAX_TRIES; i++) {
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_CHECK_READY,
+ payload, sizeof(payload),
+ &buf_len);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_receive_packet(hdev, buf, buf_len);
+ if (ret < 0)
+ hid_dbg(hdev, "ROG Ally check %d/%d failed: %d\n", i,
+ HID_ALLY_READY_MAX_TRIES, ret);
+ else if (buf[2] == CMD_CHECK_READY)
+ return 0;
+
+ /* Back off on a bus error as well as on a not-ready answer. */
+ usleep_range(1000, 2000);
+ }
+
+ hid_err(hdev, "ROG Ally never responded with a ready\n");
+ return -ENODEV;
+}
+
+static int ally_get_endpoint_address(struct hid_device *hdev)
+{
+ struct usb_host_endpoint *ep;
+ struct usb_interface *intf;
+
+ if (!hid_is_usb(hdev))
+ return -ENODEV;
+
+ intf = to_usb_interface(hdev->dev.parent);
+ if (!intf || !intf->cur_altsetting)
+ return -ENODEV;
+
+ if (intf->cur_altsetting->desc.bNumEndpoints == 0)
+ return -ENODEV;
+
+ ep = intf->cur_altsetting->endpoint;
+ if (!ep)
+ return -ENODEV;
+
+ return ep->desc.bEndpointAddress;
+}
+
+struct ally_x_input_report {
+ __le16 x, y;
+ __le16 rx, ry;
+ __le16 z, rz;
+ u8 buttons[3];
+} __packed;
+
+/* The hatswitch outputs integers, we use them to index this X|Y pair */
+static const int hat_values[][2] = {
+ { 0, 0 }, { 0, -1 }, { 1, -1 }, { 1, 0 }, { 1, 1 },
+ { 0, 1 }, { -1, 1 }, { -1, 0 }, { -1, -1 },
+};
+
+/**
+ * ally_x_raw_event() - Parse and report an Ally X gamepad input report
+ * @input: input device to report the events on
+ * @data: raw report buffer
+ * @size: length of @data in bytes
+ *
+ * Return: true if the event was handled, otherwise false.
+ */
+static bool ally_x_raw_event(struct input_dev *input, u8 *data, int size)
+{
+ struct ally_x_input_report *in_report;
+ u16 x, y, rx, ry, z, rz;
+ u8 byte;
+
+ if (!input)
+ return false;
+
+ if (size < 1)
+ return false;
+
+ if (data[0] != HID_ALLY_X_INPUT_REPORT)
+ return false;
+
+ /*
+ * hid-core only guarantees size >= 1 and does not zero-pad short
+ * reports before ->raw_event, so a truncated transfer would leave the
+ * payload below pointing at stale DMA buffer contents.
+ */
+ if (size < 1 + sizeof(*in_report))
+ return false;
+
+ in_report = (struct ally_x_input_report *)&data[1];
+
+ /* USB HID payloads are little-endian: convert them explicitly. */
+ x = get_unaligned_le16(&in_report->x);
+ y = get_unaligned_le16(&in_report->y);
+ rx = get_unaligned_le16(&in_report->rx);
+ ry = get_unaligned_le16(&in_report->ry);
+ z = get_unaligned_le16(&in_report->z);
+ rz = get_unaligned_le16(&in_report->rz);
+
+ input_report_abs(input, ABS_X, x - 32768);
+ input_report_abs(input, ABS_Y, y - 32768);
+ input_report_abs(input, ABS_RX, rx - 32768);
+ input_report_abs(input, ABS_RY, ry - 32768);
+ input_report_abs(input, ABS_Z, z);
+ input_report_abs(input, ABS_RZ, rz);
+
+ byte = in_report->buttons[0];
+ input_report_key(input, BTN_A, byte & BIT(0));
+ input_report_key(input, BTN_B, byte & BIT(1));
+ input_report_key(input, BTN_X, byte & BIT(2));
+ input_report_key(input, BTN_Y, byte & BIT(3));
+ input_report_key(input, BTN_TL, byte & BIT(4));
+ input_report_key(input, BTN_TR, byte & BIT(5));
+ input_report_key(input, BTN_SELECT, byte & BIT(6));
+ input_report_key(input, BTN_START, byte & BIT(7));
+
+ byte = in_report->buttons[1];
+ input_report_key(input, BTN_THUMBL, byte & BIT(0));
+ input_report_key(input, BTN_THUMBR, byte & BIT(1));
+ input_report_key(input, BTN_MODE, byte & BIT(2));
+
+ /*
+ * The hatswitch byte is device-controlled; treat anything the table
+ * does not cover as centred rather than indexing out of bounds.
+ */
+ byte = in_report->buttons[2];
+ if (byte >= ARRAY_SIZE(hat_values))
+ byte = 0;
+ input_report_abs(input, ABS_HAT0X, hat_values[byte][0]);
+ input_report_abs(input, ABS_HAT0Y, hat_values[byte][1]);
+
+ input_sync(input);
+
+ return true;
+}
+
+static int ally_x_input_open(struct input_dev *dev)
+{
+ struct ally_intf *intf = input_get_drvdata(dev);
+
+ return hid_hw_open(intf->hdev);
+}
+
+static void ally_x_input_close(struct input_dev *dev)
+{
+ struct ally_intf *intf = input_get_drvdata(dev);
+
+ hid_hw_close(intf->hdev);
+}
+
+static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev,
+ struct ally_intf *intf)
+{
+ struct input_dev *input_dev = input_allocate_device();
+
+ if (!input_dev)
+ return ERR_PTR(-ENOMEM);
+
+ input_dev->dev.parent = &hdev->dev;
+ input_dev->id.bustype = hdev->bus;
+ input_dev->id.vendor = hdev->vendor;
+ input_dev->id.product = hdev->product;
+ input_dev->id.version = hdev->version;
+ input_dev->uniq = hdev->uniq;
+ input_dev->name = "ASUS ROG Ally X Gamepad";
+
+ input_set_drvdata(input_dev, intf);
+ /*
+ * Let the input core hold the hardware open while the device node
+ * is in use: without these the interface could be runtime-suspended
+ * and drop events.
+ */
+ input_dev->open = ally_x_input_open;
+ input_dev->close = ally_x_input_close;
+
+ return input_dev;
+}
+
+static int ally_x_setup_input(struct hid_device *hdev, struct ally_intf *intf)
+{
+ struct input_dev *input = ally_x_alloc_input_dev(hdev, intf);
+ int ret;
+
+ if (IS_ERR(input))
+ return PTR_ERR(input);
+
+ input_set_abs_params(input, ABS_X, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_Y, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_RX, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_RY, -32768, 32767, 0, 0);
+ input_set_abs_params(input, ABS_Z, 0, 1023, 0, 0);
+ input_set_abs_params(input, ABS_RZ, 0, 1023, 0, 0);
+ input_set_abs_params(input, ABS_HAT0X, -1, 1, 0, 0);
+ input_set_abs_params(input, ABS_HAT0Y, -1, 1, 0, 0);
+ input_set_capability(input, EV_KEY, BTN_A);
+ input_set_capability(input, EV_KEY, BTN_B);
+ input_set_capability(input, EV_KEY, BTN_X);
+ input_set_capability(input, EV_KEY, BTN_Y);
+ input_set_capability(input, EV_KEY, BTN_TL);
+ input_set_capability(input, EV_KEY, BTN_TR);
+ input_set_capability(input, EV_KEY, BTN_SELECT);
+ input_set_capability(input, EV_KEY, BTN_START);
+ input_set_capability(input, EV_KEY, BTN_MODE);
+ input_set_capability(input, EV_KEY, BTN_THUMBL);
+ input_set_capability(input, EV_KEY, BTN_THUMBR);
+
+ ret = input_register_device(input);
+ if (ret) {
+ hid_err(hdev, "Failed to register Ally X gamepad device: %d\n", ret);
+ input_free_device(input);
+ return ret;
+ }
+
+ intf->gamepad = input;
+ return 0;
+}
+
+static int hid_asus_ally_init(struct hid_device *hdev)
+{
+ int ret;
+
+ /* Failure at this point is non-critical. The packet goes to this interface. */
+ ret = ally_gamepad_send_packet(hdev, ally_force_feedback_off,
+ sizeof(ally_force_feedback_off));
+ if (ret < 0)
+ hid_err(hdev, "Ally failed to init force-feedback off: %d\n", ret);
+
+ return 0;
+}
+
+/**
+ * hid_asus_ally_raw_event() - Route raw reports from one Ally interface
+ * @hdev: HID device the report arrived on
+ * @intf: that interface's ally state
+ * @data: raw report buffer
+ * @size: length of @data in bytes
+ *
+ * Called from the raw_event callback, which may run in atomic context.
+ * Only this interface's own input devices are used.
+ *
+ * Return: true if the event was handled, otherwise false.
+ */
+static bool hid_asus_ally_raw_event(struct hid_device *hdev, struct ally_intf *intf,
+ u8 *data, int size)
+{
+ if (!intf)
+ return false;
+
+ if (ally_vendor_keys(intf->input, data, size))
+ return true;
+
+ switch (ally_get_endpoint_address(hdev)) {
+ case HID_ALLY_X_INTF_IN:
+ return ally_x_raw_event(intf->gamepad, data, size);
+ case HID_ALLY_INTF_KEYBOARD_IN:
+ if (handle_ctrl_alt_del(intf, data, size))
+ return false;
+ break;
+ default:
+ break;
+ }
+
+ return false;
+}
+
+static struct input_dev *ally_interface_input(struct hid_device *hdev)
+{
+ struct hid_input *hidinput;
+
+ if (list_empty(&hdev->inputs))
+ return NULL;
+
+ hidinput = list_first_entry(&hdev->inputs, struct hid_input, list);
+ return hidinput->input;
+}
+
+/**
+ * hid_asus_ally_probe() - Initialize the ROG Ally HID extension
+ * @hdev: HID device
+ *
+ * The Ally code is a plugin of the Asus HID driver: one ally_intf per
+ * bound interface, freed by the caller after hid_hw_stop(). Interfaces
+ * do not share device pointers, so a second interface cannot evict the
+ * first.
+ *
+ * Return: an ally_intf pointer on success, NULL when this endpoint has no
+ * Ally role, or an ERR_PTR on failure. On failure the function frees
+ * anything it allocated.
+ */
+static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
+{
+ int ret, ep = ally_get_endpoint_address(hdev);
+
+ if (ep < 0)
+ return ERR_PTR(ep);
+
+ if (ep != HID_ALLY_INTF_CFG_IN && ep != HID_ALLY_X_INTF_IN &&
+ ep != HID_ALLY_INTF_KEYBOARD_IN)
+ return NULL;
+
+ struct ally_intf *intf __free(kfree) = kzalloc_obj(*intf);
+
+ if (!intf)
+ return ERR_PTR(-ENOMEM);
+
+ intf->hdev = hdev;
+ intf->input = ally_interface_input(hdev);
+ INIT_DELAYED_WORK(&intf->resume_work, ally_resume_work_fn);
+
+ switch (ep) {
+ case HID_ALLY_INTF_CFG_IN:
+ /*
+ * This function assumes the asus-specific initialization
+ * to have been performed already at this point.
+ */
+ ret = ally_gamepad_check_ready(hdev);
+ if (ret < 0) {
+ hid_err(hdev, "ROG Ally device is not ready: %d\n", ret);
+ return ERR_PTR(ret);
+ }
+
+ hid_asus_ally_init(hdev);
+ break;
+ case HID_ALLY_X_INTF_IN:
+ ret = ally_x_setup_input(hdev, intf);
+ if (ret) {
+ hid_err(hdev, "Failed to create Ally X gamepad device.\n");
+ return ERR_PTR(ret);
+ }
+ break;
+ case HID_ALLY_INTF_KEYBOARD_IN:
+ break;
+ default:
+ return NULL;
+ }
+
+ return no_free_ptr(intf);
+}
+
+static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf *intf)
+{
+ int ep;
+
+ /*
+ * The extensions failed to probe and the device is operating as a
+ * generic HID device: do not fail the resume because of that.
+ */
+ if (!intf)
+ return 0;
+
+ ep = ally_get_endpoint_address(hdev);
+ if (ep != HID_ALLY_INTF_CFG_IN)
+ return 0;
+
+ return hid_asus_ally_init(hdev);
+}
+
/*
* Send events to asus-wmi driver for handling special keys
*/
@@ -512,6 +1308,21 @@ static int asus_raw_event(struct hid_device *hdev,
if (drvdata->quirks & QUIRK_MEDION_E1239T)
return asus_e1239t_event(drvdata, data, size);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ struct ally_intf *intf;
+
+ /*
+ * Return -1 to suppress further processing by the generic HID
+ * input parser for reports we fully handle. The interface is
+ * published with smp_store_release() only after its fields
+ * are set. Letting report 0x0B reach the default parser
+ * creates a generic gamepad and Steam Input overlaps.
+ */
+ intf = smp_load_acquire(&drvdata->ally); /* fields are published */
+ if (hid_asus_ally_raw_event(hdev, intf, data, size))
+ return -1;
+ }
+
/*
* Skip these report ID, the device emits a continuous stream associated
* with the AURA mode it is in which looks like an 'echo'.
@@ -581,104 +1392,6 @@ static int asus_raw_event(struct hid_device *hdev,
return 0;
}
-/**
- * asus_find_report() - Find a report of the given type by its ID
- * @hdev: HID device
- * @type: report type (HID_INPUT_REPORT, HID_OUTPUT_REPORT or
- * HID_FEATURE_REPORT)
- * @report_id: report ID to look for
- *
- * Return: the report with the given ID, or NULL if the device does not
- * declare such a report.
- */
-static struct hid_report *asus_find_report(struct hid_device *hdev,
- unsigned int type, u8 report_id)
-{
- return hdev->report_enum[type].report_id_hash[report_id];
-}
-
-/**
- * asus_report_len() - Length of a feature report for the device
- * @hdev: HID device
- * @report_id: report ID of the feature report
- *
- * The same ASUS report ID can have different sizes depending on the device:
- * report 0x5E is a 64-byte report on laptops but a 300-byte report on XG
- * Mobile stations. Always size transfers from the report descriptor of the
- * device at hand instead of hardcoding lengths.
- *
- * Return: the length of the feature report in bytes, or 0 if the device
- * does not declare the report.
- */
-static size_t asus_report_len(struct hid_device *hdev, u8 report_id)
-{
- struct hid_report *report;
-
- report = asus_find_report(hdev, HID_FEATURE_REPORT, report_id);
-
- return report ? hid_report_len(report) : 0;
-}
-
-static int asus_kbd_set_report(struct hid_device *hdev, const u8 *buf, size_t buf_size)
-{
- size_t hw_len = asus_report_len(hdev, buf[0]);
-
- /* The payload (including the report ID) must fit the device report. */
- if (!hw_len || buf_size > hw_len)
- return -EINVAL;
-
- u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
- if (!dmabuf)
- return -ENOMEM;
-
- memcpy(dmabuf, buf, buf_size);
-
- /*
- * The report ID should be set from the incoming buffer due to LED and
- * key interfaces having different pages. Short payloads are zero-padded
- * to the exact report size the hardware expects for this report ID.
- */
- return hid_hw_raw_request(hdev, buf[0], dmabuf, hw_len, HID_FEATURE_REPORT,
- HID_REQ_SET_REPORT);
-}
-
-/**
- * asus_kbd_get_report() - Read a report sized from the descriptor
- * @hdev: HID device
- * @report_id: report ID to read
- * @buf: buffer to store the report in
- * @buf_size: size of @buf
- *
- * The transfer is performed at the exact report size the device declares
- * for @report_id. The device may answer with a short read, and only the
- * bytes that fit in @buf are copied into it.
- *
- * Return: 0 on success, negative errno on failure.
- */
-static int asus_kbd_get_report(struct hid_device *hdev, u8 report_id,
- u8 *buf, size_t buf_size)
-{
- size_t hw_len = asus_report_len(hdev, report_id);
- int ret;
-
- if (!hw_len)
- return -EINVAL;
-
- u8 *dmabuf __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
- if (!dmabuf)
- return -ENOMEM;
-
- ret = hid_hw_raw_request(hdev, report_id, dmabuf, hw_len,
- HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
- if (ret < 0)
- return ret;
-
- /* The device may return less than requested (short read). */
- memcpy(buf, dmabuf, min(buf_size, hw_len));
-
- return 0;
-}
-
static int asus_kbd_init(struct hid_device *hdev, u8 report_id)
{
/*
@@ -1395,6 +2108,7 @@ static int asus_input_mapping(struct hid_device *hdev,
case 0x5f: asus_map_key_clear(KEY_PROG2); break; /* S-shaped programmable key */
case 0x6b: asus_map_key_clear(KEY_F21); break; /* ASUS touchpad toggle */
case 0x38: asus_map_key_clear(KEY_PROG1); break; /* ROG key */
+ case 0x93: asus_map_key_clear(KEY_PROG1); break; /* ROG Ally X AC button */
case 0xba: asus_map_key_clear(KEY_PROG2); break; /* Fn+C ASUS Splendid */
case 0x5c: asus_map_key_clear(KEY_PROG3); break; /* Fn+Space Power4Gear */
case 0x99: asus_map_key_clear(KEY_PROG4); break; /* Fn+F5 "fan" symbol */
@@ -1415,6 +2129,25 @@ static int asus_input_mapping(struct hid_device *hdev,
return -1;
}
+ /*
+ * Declare the Ally vendor keys before this input is registered.
+ * KEY_F19 is the second event of the ROG key (usage 0x38).
+ */
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ switch (usage->hid & HID_USAGE) {
+ case 0x38:
+ case 0xa6:
+ case 0xa7:
+ case 0xa8:
+ input_set_capability(hi->input, EV_KEY, KEY_PROG1);
+ input_set_capability(hi->input, EV_KEY, KEY_F16);
+ input_set_capability(hi->input, EV_KEY, KEY_F17);
+ input_set_capability(hi->input, EV_KEY, KEY_F18);
+ input_set_capability(hi->input, EV_KEY, KEY_F19);
+ break;
+ }
+ }
+
set_bit(EV_REP, hi->input->evbit);
return 1;
}
@@ -1569,6 +2302,8 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
static int __maybe_unused asus_resume(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ /* Published with smp_store_release() after the fields are set. */
+ struct ally_intf *intf = smp_load_acquire(&drvdata->ally);
/*
* If we have a backlight listener registered, restore the previous state,
@@ -1578,12 +2313,16 @@ static int __maybe_unused asus_resume(struct hid_device *hdev)
if (drvdata->listener.brightness_set)
asus_kbd_backlight_set(&drvdata->listener, drvdata->kbd_backlight_brightness);
+ if (intf)
+ schedule_delayed_work(&intf->resume_work, msecs_to_jiffies(500));
+
return 0;
}
static int __maybe_unused asus_reset_resume(struct hid_device *hdev)
{
struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+ int ret;
if (!drvdata->tp)
asus_initialize_reports(hdev);
@@ -1591,6 +2330,15 @@ static int __maybe_unused asus_reset_resume(struct hid_device *hdev)
if (drvdata->tp)
return asus_start_multitouch(hdev);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ /* Published with smp_store_release() after the fields are set. */
+ ret = hid_asus_ally_reset_resume(hdev, smp_load_acquire(&drvdata->ally));
+ if (ret) {
+ hid_err(hdev, "Failed to resume ROG Ally HID extensions: %d\n", ret);
+ return ret;
+ }
+ }
+
return 0;
}
@@ -1598,6 +2346,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
struct hid_report_enum *rep_enum;
struct asus_drvdata *drvdata;
+ struct ally_intf *ally;
struct hid_report *rep;
bool is_vendor = false;
int ret;
@@ -1715,12 +2464,22 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
}
}
- /* Laptops keyboard backlight is always at 0x5a */
+ /* Laptops keyboard backlight is always FEATURE_KBD_REPORT_ID */
if (is_vendor && (drvdata->quirks & QUIRK_USE_KBD_BACKLIGHT) &&
(asus_has_report_id(hdev, FEATURE_KBD_REPORT_ID)) &&
(asus_kbd_register_leds(hdev)))
hid_warn(hdev, "Failed to initialize backlight.\n");
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
+ ally = hid_asus_ally_probe(hdev);
+ if (IS_ERR(ally))
+ hid_err(hdev, "Failed to initialize ROG Ally HID extensions: %ld\n",
+ PTR_ERR(ally));
+ else
+ /* Readers take this with smp_load_acquire(). */
+ smp_store_release(&drvdata->ally, ally);
+ }
+
/*
* For ROG keyboards, skip rename for consistency and ->input check as
* some devices do not have inputs.
@@ -1755,8 +2514,14 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ ally_quiesce(drvdata->ally);
+
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
+
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ ally_free(drvdata->ally);
return ret;
}
@@ -1770,8 +2535,14 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ ally_quiesce(drvdata->ally);
+
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
+
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ ally_free(drvdata->ally);
}
static const __u8 asus_g752_fixed_rdesc[] = {
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 03/17] HID: asus: add gamepad configuration
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
2026-10-31 17:41 ` [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor Denis Benato
2026-10-31 17:41 ` [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:59 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 04/17] HID: asus: add vibration strength configuration Denis Benato
` (13 subsequent siblings)
16 siblings, 1 reply; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
Add the base configuration structures for the gamepad configuration,
detect capabilities and initialize the device in a known state.
The configuration interface exposes xbox_controller. Reading it
reports whether the handheld is presenting an Xbox gamepad, and
writing it switches that mode.
hid_asus_ally_reset_resume() now waits for the gamepad MCU to report
ready and returns that error. A reset resume can fail with -ENODEV
where it previously always succeeded.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
---
drivers/hid/hid-asus.c | 255 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 252 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 552d3b25a1d1..5f5393ea92b7 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -28,6 +28,7 @@
#include <linux/err.h>
#include <linux/hid.h>
#include <linux/jiffies.h>
+#include <linux/kstrtox.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/mutex.h>
@@ -37,6 +38,7 @@
#include <linux/usb.h> /* For to_usb_interface for T100 touchpad intf check */
#include <linux/power_supply.h>
#include <linux/stddef.h>
+#include <linux/sysfs.h>
#include <linux/leds.h>
#include <linux/types.h>
#include <linux/unaligned.h>
@@ -78,6 +80,11 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define HID_ALLY_READY_MAX_TRIES 6
+#define ASUS_ALLY_XBOX_CONTROLLER_ENABLE 0x01
+#define ASUS_ALLY_XBOX_CONTROLLER_DISABLE 0x00
+
+#define ASUS_ALLY_CAPABILITY_SUPPORTED 0x01
+
#define HID_INPUT_KEY_DELETE 0x4c
#define HID_INPUT_KEY_F20 0x6f
#define HID_INPUT_LCTRL_MODIFIER 0x01
@@ -729,6 +736,224 @@ static int ally_get_endpoint_address(struct hid_device *hdev)
return ep->desc.bEndpointAddress;
}
+/*
+ * Settings that must survive the controller powering off and coming back.
+ * No device or input pointers live here: every transfer uses the interface
+ * that is bound right now.
+ *
+ * One set for the machine is by design. The Ally controller is
+ * part of the PC, so two ROG Ally HID devices are not bound at
+ * once. The only way to see a second one is to emulate it with
+ * uhid.
+ */
+static struct ally_prefs {
+ /* Guards every field below, including across the feature report. */
+ struct mutex lock;
+
+ bool initialized;
+ bool sysfs_live;
+
+ bool is_ally_x;
+ bool xbox_controller_support;
+ bool user_cal_support;
+ bool turbo_support;
+ bool resp_curve_support;
+ bool dir_to_btn_support;
+ bool gyro_support;
+ bool anti_deadzone_support;
+
+ bool xbox_controller_enabled;
+ u8 gamepad_mode;
+} ally_prefs = {
+ .lock = __MUTEX_INITIALIZER(ally_prefs.lock),
+};
+
+/**
+ * ally_check_capability() - Check if a specific capability is supported
+ * @hdev: HID device to query
+ * @check_cmd: capability command code
+ *
+ * Return: true if the capability is supported, false otherwise.
+ */
+static bool ally_check_capability(struct hid_device *hdev,
+ enum ally_command_codes check_cmd)
+{
+ const u8 payload[] = {};
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, check_cmd, payload,
+ sizeof(payload), &buf_len);
+
+ if (!buf) {
+ hid_err(hdev, "Failed to allocate buffer for capability check.\n");
+ return false;
+ }
+
+ ret = ally_gamepad_send_receive_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to check capability 0x%02x: %d\n", check_cmd, ret);
+ return false;
+ }
+
+ return buf[1] == HID_ALLY_FEATURE_CODE_PAGE && buf[2] == check_cmd &&
+ buf[4] == ASUS_ALLY_CAPABILITY_SUPPORTED;
+}
+
+static int ally_detect_capabilities(struct hid_device *hdev)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ ally_prefs.is_ally_x = (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X);
+ ally_prefs.xbox_controller_support =
+ ally_check_capability(hdev, CMD_CHECK_XBOX_SUPPORT);
+ ally_prefs.user_cal_support =
+ ally_check_capability(hdev, CMD_CHECK_USER_CAL_SUPPORT);
+ ally_prefs.turbo_support =
+ ally_check_capability(hdev, CMD_CHECK_TURBO_SUPPORT);
+ ally_prefs.resp_curve_support =
+ ally_check_capability(hdev, CMD_CHECK_RESP_CURVE_SUPPORT);
+ ally_prefs.dir_to_btn_support =
+ ally_check_capability(hdev, CMD_CHECK_DIR_TO_BTN_SUPPORT);
+ ally_prefs.gyro_support =
+ ally_check_capability(hdev, CMD_CHECK_GYRO_TO_JOYSTICK);
+ ally_prefs.anti_deadzone_support =
+ ally_check_capability(hdev, CMD_CHECK_ANTI_DEADZONE);
+
+ return 0;
+}
+
+/* Caller holds ally_prefs.lock. The packet is sent on @hdev. */
+static int ally_set_xbox_controller(struct hid_device *hdev, bool enabled)
+{
+ const u8 payload[] = { enabled ?
+ ASUS_ALLY_XBOX_CONTROLLER_ENABLE :
+ ASUS_ALLY_XBOX_CONTROLLER_DISABLE };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_XBOX_CONTROLLER,
+ payload, sizeof(payload),
+ &buf_len);
+
+ if (!ally_prefs.xbox_controller_support)
+ return -EOPNOTSUPP;
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set Xbox controller mode: %d\n", ret);
+ return ret;
+ }
+
+ ally_prefs.xbox_controller_enabled = enabled;
+ return 0;
+}
+
+static ssize_t xbox_controller_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.xbox_controller_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%d\n", ally_prefs.xbox_controller_enabled ? 1 : 0);
+}
+
+static ssize_t xbox_controller_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ bool enabled;
+ int ret;
+
+ ret = kstrtobool(buf, &enabled);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.xbox_controller_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_xbox_controller(hdev, enabled);
+ if (ret < 0)
+ return ret;
+
+ sysfs_notify(&dev->kobj, NULL, "xbox_controller");
+
+ return count;
+}
+
+static DEVICE_ATTR_RW(xbox_controller);
+
+static struct attribute *ally_config_attrs[] = {
+ &dev_attr_xbox_controller.attr,
+ NULL
+};
+
+static const struct attribute_group ally_attr_groups[] = {
+ {
+ .attrs = ally_config_attrs,
+ },
+};
+
+static int ally_prefs_setup(struct hid_device *hdev)
+{
+ int ret, i;
+
+ ret = ally_detect_capabilities(hdev);
+ if (ret)
+ return ret;
+
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.initialized) {
+ ally_prefs.gamepad_mode = 0x01;
+ ally_prefs.xbox_controller_enabled = true;
+ ally_prefs.initialized = true;
+ }
+
+ if (ally_prefs.sysfs_live)
+ return 0;
+
+ for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++) {
+ ret = sysfs_create_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+ if (ret) {
+ hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
+ ally_attr_groups[i].name ?: "", ret);
+ while (i--)
+ sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+ return ret;
+ }
+ }
+
+ ally_prefs.sysfs_live = true;
+ return 0;
+}
+
+static void ally_sysfs_remove(struct hid_device *hdev)
+{
+ int i;
+
+ if (ally_get_endpoint_address(hdev) != HID_ALLY_INTF_CFG_IN)
+ return;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.sysfs_live)
+ return;
+
+ ally_prefs.sysfs_live = false;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
+ sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+}
+
struct ally_x_input_report {
__le16 x, y;
__le16 rx, ry;
@@ -911,6 +1136,14 @@ static int hid_asus_ally_init(struct hid_device *hdev)
if (ret < 0)
hid_err(hdev, "Ally failed to init force-feedback off: %d\n", ret);
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized && ally_prefs.xbox_controller_support) {
+ ret = ally_set_xbox_controller(hdev, ally_prefs.xbox_controller_enabled);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to apply Xbox controller mode: %d\n", ret);
+ }
+
return 0;
}
@@ -1005,6 +1238,12 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
return ERR_PTR(ret);
}
+ ret = ally_prefs_setup(hdev);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to create Ally cfg: %d\n", ret);
+ return ERR_PTR(ret);
+ }
+
hid_asus_ally_init(hdev);
break;
case HID_ALLY_X_INTF_IN:
@@ -1025,7 +1264,7 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf *intf)
{
- int ep;
+ int ep, ret;
/*
* The extensions failed to probe and the device is operating as a
@@ -1038,6 +1277,12 @@ static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf
if (ep != HID_ALLY_INTF_CFG_IN)
return 0;
+ ret = ally_gamepad_check_ready(hdev);
+ if (ret < 0) {
+ hid_err(hdev, "ROG Ally device is not ready: %d\n", ret);
+ return ret;
+ }
+
return hid_asus_ally_init(hdev);
}
@@ -2514,8 +2759,10 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
- if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
ally_quiesce(drvdata->ally);
+ ally_sysfs_remove(hdev);
+ }
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
@@ -2535,8 +2782,10 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
- if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
+ if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
ally_quiesce(drvdata->ally);
+ ally_sysfs_remove(hdev);
+ }
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 04/17] HID: asus: add vibration strength configuration
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (2 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 03/17] HID: asus: add gamepad configuration Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 05/17] HID: asus: add joysticks inner and outer range configuration Denis Benato
` (12 subsequent siblings)
16 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ASUS ROG Ally handhelds support the vibration strength to be configured:
add sysfs attributes to allow userspace configure motors vibration
intensity.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/hid-asus.c | 173 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 173 insertions(+)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 5f5393ea92b7..fe2008f9d51d 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -764,6 +764,9 @@ static struct ally_prefs {
bool xbox_controller_enabled;
u8 gamepad_mode;
+
+ u8 vibration_intensity_left;
+ u8 vibration_intensity_right;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -891,6 +894,154 @@ static ssize_t xbox_controller_store(struct device *dev,
static DEVICE_ATTR_RW(xbox_controller);
+/**
+ * ally_set_vibration_intensity() - Set the gamepad vibration intensity
+ * @hdev: HID device the command packet is sent on
+ * @left: left motor intensity
+ * @right: right motor intensity
+ *
+ * Context: Caller holds ally_prefs.lock.
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_set_vibration_intensity(struct hid_device *hdev, u8 left, u8 right)
+{
+ const u8 data[] = { left, right };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_VIBRATION_INTENSITY,
+ data, sizeof(data), &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set vibration intensity: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void ally_vibration_defaults(void)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized)
+ return;
+
+ ally_prefs.vibration_intensity_left = 100;
+ ally_prefs.vibration_intensity_right = 100;
+}
+
+static ssize_t left_vibration_intensity_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.vibration_intensity_left);
+}
+
+static ssize_t left_vibration_intensity_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > 100)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ ret = ally_set_vibration_intensity(hdev, value,
+ ally_prefs.vibration_intensity_right);
+ if (ret < 0)
+ return ret;
+
+ ally_prefs.vibration_intensity_left = value;
+ }
+
+ sysfs_notify(&dev->kobj, "left_vibration", "intensity");
+
+ return count;
+}
+
+static ssize_t left_vibration_intensity_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 100\n");
+}
+
+static ssize_t right_vibration_intensity_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.vibration_intensity_right);
+}
+
+static ssize_t right_vibration_intensity_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > 100)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ ret = ally_set_vibration_intensity(hdev,
+ ally_prefs.vibration_intensity_left,
+ value);
+ if (ret < 0)
+ return ret;
+
+ ally_prefs.vibration_intensity_right = value;
+ }
+
+ sysfs_notify(&dev->kobj, "right_vibration", "intensity");
+
+ return count;
+}
+
+static ssize_t right_vibration_intensity_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 100\n");
+}
+
+static struct device_attribute dev_attr_left_vibration_intensity =
+ __ATTR(intensity, 0644, left_vibration_intensity_show, left_vibration_intensity_store);
+
+static struct device_attribute dev_attr_left_vibration_intensity_range =
+ __ATTR(intensity_range, 0444, left_vibration_intensity_range_show, NULL);
+
+static struct device_attribute dev_attr_right_vibration_intensity =
+ __ATTR(intensity, 0644, right_vibration_intensity_show, right_vibration_intensity_store);
+
+static struct device_attribute dev_attr_right_vibration_intensity_range =
+ __ATTR(intensity_range, 0444, right_vibration_intensity_range_show, NULL);
+
+static struct attribute *ally_left_vibration_attrs[] = {
+ &dev_attr_left_vibration_intensity.attr,
+ &dev_attr_left_vibration_intensity_range.attr,
+ NULL
+};
+
+static struct attribute *ally_right_vibration_attrs[] = {
+ &dev_attr_right_vibration_intensity.attr,
+ &dev_attr_right_vibration_intensity_range.attr,
+ NULL
+};
+
static struct attribute *ally_config_attrs[] = {
&dev_attr_xbox_controller.attr,
NULL
@@ -900,6 +1051,14 @@ static const struct attribute_group ally_attr_groups[] = {
{
.attrs = ally_config_attrs,
},
+ {
+ .name = "left_vibration",
+ .attrs = ally_left_vibration_attrs,
+ },
+ {
+ .name = "right_vibration",
+ .attrs = ally_right_vibration_attrs,
+ },
};
static int ally_prefs_setup(struct hid_device *hdev)
@@ -910,6 +1069,8 @@ static int ally_prefs_setup(struct hid_device *hdev)
if (ret)
return ret;
+ ally_vibration_defaults();
+
guard(mutex)(&ally_prefs.lock);
if (!ally_prefs.initialized) {
@@ -1144,6 +1305,18 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to apply Xbox controller mode: %d\n", ret);
}
+ /*
+ * The MCU does not keep the vibration intensity across a reset:
+ * send the recorded values again.
+ */
+ if (ally_prefs.initialized) {
+ ret = ally_set_vibration_intensity(hdev,
+ ally_prefs.vibration_intensity_left,
+ ally_prefs.vibration_intensity_right);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore vibration intensity: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 05/17] HID: asus: add joysticks inner and outer range configuration
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (3 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 04/17] HID: asus: add vibration strength configuration Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:57 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 06/17] HID: asus: add triggers " Denis Benato
` (11 subsequent siblings)
16 siblings, 1 reply; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG Ally devices supports configuring joysticks inner and outer range:
add sysfs attributes to allow userspace modifying the sensitivity
of those controllers.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/hid-asus.c | 354 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 348 insertions(+), 6 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index fe2008f9d51d..50b0e6040595 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -98,6 +98,9 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_HID_INPUT_KBD_REPORT_ID 0x01
+#define ASUS_ALLY_OUTER_THRESHOLD_MIN 70
+#define ASUS_ALLY_OUTER_THRESHOLD_MAX 100
+
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
#define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -767,6 +770,11 @@ static struct ally_prefs {
u8 vibration_intensity_left;
u8 vibration_intensity_right;
+
+ u8 left_deadzone;
+ u8 left_outer_threshold;
+ u8 right_deadzone;
+ u8 right_outer_threshold;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -826,7 +834,6 @@ static int ally_detect_capabilities(struct hid_device *hdev)
return 0;
}
-/* Caller holds ally_prefs.lock. The packet is sent on @hdev. */
static int ally_set_xbox_controller(struct hid_device *hdev, bool enabled)
{
const u8 payload[] = { enabled ?
@@ -1042,6 +1049,304 @@ static struct attribute *ally_right_vibration_attrs[] = {
NULL
};
+static int ally_set_joystick_thresholds(struct hid_device *hdev, u8 left_it,
+ u8 left_ot, u8 right_it, u8 right_ot)
+{
+ const u8 payload[] = { left_it, left_ot, right_it, right_ot };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_JOYSTICK_DEADZONE,
+ payload, sizeof(payload),
+ &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set joystick ranges: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void ally_joystick_defaults(void)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized)
+ return;
+
+ ally_prefs.left_deadzone = 10;
+ ally_prefs.left_outer_threshold = 90;
+ ally_prefs.right_deadzone = 10;
+ ally_prefs.right_outer_threshold = 90;
+}
+
+static ssize_t left_joystick_inner_threshold_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.left_deadzone);
+}
+
+static ssize_t left_joystick_inner_threshold_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > 50)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_joystick_thresholds(hdev, value,
+ ally_prefs.left_outer_threshold,
+ ally_prefs.right_deadzone,
+ ally_prefs.right_outer_threshold);
+ if (ret)
+ return ret;
+
+ ally_prefs.left_deadzone = value;
+ }
+
+ sysfs_notify(&dev->kobj, "left_joystick_axis", "inner_threshold");
+
+ return count;
+}
+
+static ssize_t left_joystick_inner_threshold_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 50\n");
+}
+
+static ssize_t left_joystick_outer_threshold_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.left_outer_threshold);
+}
+
+static ssize_t left_joystick_outer_threshold_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value < 70 || value > 100)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_joystick_thresholds(hdev, ally_prefs.left_deadzone,
+ value, ally_prefs.right_deadzone,
+ ally_prefs.right_outer_threshold);
+ if (ret)
+ return ret;
+
+ ally_prefs.left_outer_threshold = value;
+ }
+
+ sysfs_notify(&dev->kobj, "left_joystick_axis", "outer_threshold");
+
+ return count;
+}
+
+static ssize_t left_joystick_outer_threshold_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "70 100\n");
+}
+
+static ssize_t right_joystick_inner_threshold_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.right_deadzone);
+}
+
+static ssize_t right_joystick_inner_threshold_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > 50)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_joystick_thresholds(hdev, ally_prefs.left_deadzone,
+ ally_prefs.left_outer_threshold,
+ value,
+ ally_prefs.right_outer_threshold);
+ if (ret)
+ return ret;
+
+ ally_prefs.right_deadzone = value;
+ }
+
+ sysfs_notify(&dev->kobj, "right_joystick_axis", "inner_threshold");
+
+ return count;
+}
+
+static ssize_t right_joystick_inner_threshold_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 50\n");
+}
+
+static ssize_t right_joystick_outer_threshold_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", ally_prefs.right_outer_threshold);
+}
+
+static ssize_t right_joystick_outer_threshold_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value < ASUS_ALLY_OUTER_THRESHOLD_MIN || value > ASUS_ALLY_OUTER_THRESHOLD_MAX)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_joystick_thresholds(hdev, ally_prefs.left_deadzone,
+ ally_prefs.left_outer_threshold,
+ ally_prefs.right_deadzone, value);
+ if (ret)
+ return ret;
+
+ ally_prefs.right_outer_threshold = value;
+ }
+
+ sysfs_notify(&dev->kobj, "right_joystick_axis", "outer_threshold");
+
+ return count;
+}
+
+static ssize_t right_joystick_outer_threshold_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "%u %u\n", ASUS_ALLY_OUTER_THRESHOLD_MIN,
+ ASUS_ALLY_OUTER_THRESHOLD_MAX);
+}
+
+static struct device_attribute dev_attr_left_joystick_inner_threshold =
+ __ATTR(inner_threshold, 0644, left_joystick_inner_threshold_show,
+ left_joystick_inner_threshold_store);
+
+static struct device_attribute dev_attr_left_joystick_inner_threshold_range =
+ __ATTR(inner_threshold_range, 0444, left_joystick_inner_threshold_range_show, NULL);
+
+static struct device_attribute dev_attr_left_joystick_outer_threshold =
+ __ATTR(outer_threshold, 0644, left_joystick_outer_threshold_show,
+ left_joystick_outer_threshold_store);
+
+static struct device_attribute dev_attr_left_joystick_outer_threshold_range =
+ __ATTR(outer_threshold_range, 0444, left_joystick_outer_threshold_range_show, NULL);
+
+static struct device_attribute dev_attr_right_joystick_inner_threshold =
+ __ATTR(inner_threshold, 0644, right_joystick_inner_threshold_show,
+ right_joystick_inner_threshold_store);
+
+static struct device_attribute dev_attr_right_joystick_inner_threshold_range =
+ __ATTR(inner_threshold_range, 0444, right_joystick_inner_threshold_range_show, NULL);
+
+static struct device_attribute dev_attr_right_joystick_outer_threshold =
+ __ATTR(outer_threshold, 0644, right_joystick_outer_threshold_show,
+ right_joystick_outer_threshold_store);
+
+static struct device_attribute dev_attr_right_joystick_outer_threshold_range =
+ __ATTR(outer_threshold_range, 0444, right_joystick_outer_threshold_range_show, NULL);
+
+static struct attribute *left_joystick_axis_attrs[] = {
+ &dev_attr_left_joystick_inner_threshold.attr,
+ &dev_attr_left_joystick_outer_threshold.attr,
+ &dev_attr_left_joystick_inner_threshold_range.attr,
+ &dev_attr_left_joystick_outer_threshold_range.attr,
+ NULL
+};
+
+static struct attribute *right_joystick_axis_attrs[] = {
+ &dev_attr_right_joystick_inner_threshold.attr,
+ &dev_attr_right_joystick_outer_threshold.attr,
+ &dev_attr_right_joystick_inner_threshold_range.attr,
+ &dev_attr_right_joystick_outer_threshold_range.attr,
+ NULL
+};
+
+/*
+ * Registered only when the device can configure these parameters. The
+ * callbacks still reject a missing capability.
+ */
+static const struct attribute_group ally_left_joystick_axis_group = {
+ .name = "left_joystick_axis",
+ .attrs = left_joystick_axis_attrs,
+};
+
+static const struct attribute_group ally_right_joystick_axis_group = {
+ .name = "right_joystick_axis",
+ .attrs = right_joystick_axis_attrs,
+};
+
+static const struct attribute_group *const ally_cal_attr_groups[] = {
+ &ally_left_joystick_axis_group,
+ &ally_right_joystick_axis_group,
+};
+
static struct attribute *ally_config_attrs[] = {
&dev_attr_xbox_controller.attr,
NULL
@@ -1070,6 +1375,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
return ret;
ally_vibration_defaults();
+ ally_joystick_defaults();
guard(mutex)(&ally_prefs.lock);
@@ -1093,6 +1399,27 @@ static int ally_prefs_setup(struct hid_device *hdev)
}
}
+ /* Skip the calibration groups when the capability is missing. */
+ if (ally_prefs.user_cal_support) {
+ int cal_i;
+
+ for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
+ ret = sysfs_create_group(&hdev->dev.kobj,
+ ally_cal_attr_groups[cal_i]);
+ if (ret) {
+ hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
+ ally_cal_attr_groups[cal_i]->name, ret);
+ while (cal_i--)
+ sysfs_remove_group(&hdev->dev.kobj,
+ ally_cal_attr_groups[cal_i]);
+ for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
+ sysfs_remove_group(&hdev->dev.kobj,
+ &ally_attr_groups[i]);
+ return ret;
+ }
+ }
+ }
+
ally_prefs.sysfs_live = true;
return 0;
}
@@ -1104,15 +1431,21 @@ static void ally_sysfs_remove(struct hid_device *hdev)
if (ally_get_endpoint_address(hdev) != HID_ALLY_INTF_CFG_IN)
return;
- scoped_guard(mutex, &ally_prefs.lock) {
- if (!ally_prefs.sysfs_live)
- return;
+ guard(mutex)(&ally_prefs.lock);
- ally_prefs.sysfs_live = false;
- }
+ if (!ally_prefs.sysfs_live)
+ return;
+
+ ally_prefs.sysfs_live = false;
for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+
+ if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
+ ally_prefs.resp_curve_support) {
+ for (i = 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++)
+ sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]);
+ }
}
struct ally_x_input_report {
@@ -1317,6 +1650,15 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore vibration intensity: %d\n", ret);
}
+ if (ally_prefs.initialized && ally_prefs.user_cal_support) {
+ ret = ally_set_joystick_thresholds(hdev, ally_prefs.left_deadzone,
+ ally_prefs.left_outer_threshold,
+ ally_prefs.right_deadzone,
+ ally_prefs.right_outer_threshold);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore joystick thresholds: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 06/17] HID: asus: add triggers inner and outer range configuration
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (4 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 05/17] HID: asus: add joysticks inner and outer range configuration Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:54 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 07/17] HID: asus: add joysticks anti-deadzone configuration Denis Benato
` (10 subsequent siblings)
16 siblings, 1 reply; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG Ally devices allows configuring inner and outer ranges for triggers
buttons on the back: allow userspace to configure the sensitivity by
exposing sysfs attributes.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/hid-asus.c | 212 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 212 insertions(+)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 50b0e6040595..5cf7258bc427 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -100,6 +100,10 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_ALLY_OUTER_THRESHOLD_MIN 70
#define ASUS_ALLY_OUTER_THRESHOLD_MAX 100
+#define ASUS_ALLY_TRIGGER_RANGE_LOWER_MIN 0
+#define ASUS_ALLY_TRIGGER_RANGE_LOWER_MAX 50
+#define ASUS_ALLY_TRIGGER_RANGE_UPPER_MIN 70
+#define ASUS_ALLY_TRIGGER_RANGE_UPPER_MAX 100
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
@@ -775,6 +779,11 @@ static struct ally_prefs {
u8 left_outer_threshold;
u8 right_deadzone;
u8 right_outer_threshold;
+
+ u8 left_trigger_min;
+ u8 left_trigger_max;
+ u8 right_trigger_min;
+ u8 right_trigger_max;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -1342,9 +1351,201 @@ static const struct attribute_group ally_right_joystick_axis_group = {
.attrs = right_joystick_axis_attrs,
};
+static int ally_set_trigger_ranges(struct hid_device *hdev, u8 left_min, u8 left_max,
+ u8 right_min, u8 right_max)
+{
+ const u8 payload[] = { left_min, left_max, right_min, right_max };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_TRIGGER_RANGE,
+ payload, sizeof(payload),
+ &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set trigger ranges: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static void ally_trigger_defaults(void)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized)
+ return;
+
+ ally_prefs.left_trigger_min = ASUS_ALLY_TRIGGER_RANGE_LOWER_MIN;
+ ally_prefs.left_trigger_max = ASUS_ALLY_TRIGGER_RANGE_UPPER_MAX;
+ ally_prefs.right_trigger_min = ASUS_ALLY_TRIGGER_RANGE_LOWER_MIN;
+ ally_prefs.right_trigger_max = ASUS_ALLY_TRIGGER_RANGE_UPPER_MAX;
+}
+
+/* Indices into ally_trigger_limits[] */
+enum ally_trigger_limit {
+ ALLY_LEFT_MIN,
+ ALLY_LEFT_MAX,
+ ALLY_RIGHT_MIN,
+ ALLY_RIGHT_MAX,
+};
+
+static u8 * const ally_trigger_limits[] = {
+ [ALLY_LEFT_MIN] = &ally_prefs.left_trigger_min,
+ [ALLY_LEFT_MAX] = &ally_prefs.left_trigger_max,
+ [ALLY_RIGHT_MIN] = &ally_prefs.right_trigger_min,
+ [ALLY_RIGHT_MAX] = &ally_prefs.right_trigger_max,
+};
+
+static ssize_t ally_trigger_limit_show(char *buf, enum ally_trigger_limit which)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u\n", *ally_trigger_limits[which]);
+}
+
+static ssize_t ally_trigger_limit_store(struct device *dev, const char *buf,
+ size_t count,
+ enum ally_trigger_limit which,
+ int lo, int hi,
+ const char *group, const char *name)
+{
+ u8 value, v[ARRAY_SIZE(ally_trigger_limits)];
+ struct hid_device *hdev = to_hid_device(dev);
+ size_t i;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value < lo || value > hi)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.user_cal_support)
+ return -EOPNOTSUPP;
+
+ for (i = 0; i < ARRAY_SIZE(v); i++)
+ v[i] = *ally_trigger_limits[i];
+ v[which] = value;
+
+ ret = ally_set_trigger_ranges(hdev, v[ALLY_LEFT_MIN], v[ALLY_LEFT_MAX],
+ v[ALLY_RIGHT_MIN], v[ALLY_RIGHT_MAX]);
+ if (ret)
+ return ret;
+
+ *ally_trigger_limits[which] = value;
+ }
+
+ sysfs_notify(&dev->kobj, group, name);
+
+ return count;
+}
+
+/*
+ * Generates <side>_trigger_range_<limit>_limit_{show,store,range_show}.
+ * @lo is the lowest value the store accepts; it is 0 (no check) for the
+ * "lower" limits, matching the original code, which only checked the maximum.
+ */
+#define ALLY_TRIGGER_LIMIT_FUNCS(side, limit, LIMIT, which, lo) \
+static ssize_t side##_trigger_range_##limit##_limit_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return ally_trigger_limit_show(buf, which); \
+} \
+ \
+static ssize_t side##_trigger_range_##limit##_limit_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, \
+ size_t count) \
+{ \
+ return ally_trigger_limit_store(dev, buf, count, which, lo, \
+ ASUS_ALLY_TRIGGER_RANGE_##LIMIT##_MAX, \
+ #side "_trigger", "range_" #limit "_limit"); \
+} \
+ \
+static ssize_t side##_trigger_range_##limit##_limit_range_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return sysfs_emit(buf, "%u %u\n", ASUS_ALLY_TRIGGER_RANGE_##LIMIT##_MIN, \
+ ASUS_ALLY_TRIGGER_RANGE_##LIMIT##_MAX); \
+}
+
+ALLY_TRIGGER_LIMIT_FUNCS(left, lower, LOWER, ALLY_LEFT_MIN, 0)
+ALLY_TRIGGER_LIMIT_FUNCS(left, upper, UPPER, ALLY_LEFT_MAX, ASUS_ALLY_TRIGGER_RANGE_UPPER_MIN)
+ALLY_TRIGGER_LIMIT_FUNCS(right, lower, LOWER, ALLY_RIGHT_MIN, 0)
+ALLY_TRIGGER_LIMIT_FUNCS(right, upper, UPPER, ALLY_RIGHT_MAX, ASUS_ALLY_TRIGGER_RANGE_UPPER_MIN)
+
+static struct device_attribute dev_attr_left_trigger_range_lower_limit =
+ __ATTR(range_lower_limit, 0644, left_trigger_range_lower_limit_show,
+ left_trigger_range_lower_limit_store);
+
+static struct device_attribute dev_attr_left_trigger_range_lower_limit_range =
+ __ATTR(range_lower_limit_range, 0444,
+ left_trigger_range_lower_limit_range_show, NULL);
+
+static struct device_attribute dev_attr_left_trigger_range_upper_limit =
+ __ATTR(range_upper_limit, 0644, left_trigger_range_upper_limit_show,
+ left_trigger_range_upper_limit_store);
+
+static struct device_attribute dev_attr_left_trigger_range_upper_limit_range =
+ __ATTR(range_upper_limit_range, 0444,
+ left_trigger_range_upper_limit_range_show, NULL);
+
+static struct device_attribute dev_attr_right_trigger_range_lower_limit =
+ __ATTR(range_lower_limit, 0644, right_trigger_range_lower_limit_show,
+ right_trigger_range_lower_limit_store);
+
+static struct device_attribute dev_attr_right_trigger_range_lower_limit_range =
+ __ATTR(range_lower_limit_range, 0444,
+ right_trigger_range_lower_limit_range_show, NULL);
+
+static struct device_attribute dev_attr_right_trigger_range_upper_limit =
+ __ATTR(range_upper_limit, 0644, right_trigger_range_upper_limit_show,
+ right_trigger_range_upper_limit_store);
+
+static struct device_attribute dev_attr_right_trigger_range_upper_limit_range =
+ __ATTR(range_upper_limit_range, 0444,
+ right_trigger_range_upper_limit_range_show, NULL);
+
+static struct attribute *left_trigger_attrs[] = {
+ &dev_attr_left_trigger_range_lower_limit.attr,
+ &dev_attr_left_trigger_range_upper_limit.attr,
+ &dev_attr_left_trigger_range_lower_limit_range.attr,
+ &dev_attr_left_trigger_range_upper_limit_range.attr,
+ NULL
+};
+
+static struct attribute *right_trigger_attrs[] = {
+ &dev_attr_right_trigger_range_lower_limit.attr,
+ &dev_attr_right_trigger_range_upper_limit.attr,
+ &dev_attr_right_trigger_range_lower_limit_range.attr,
+ &dev_attr_right_trigger_range_upper_limit_range.attr,
+ NULL
+};
+
+static const struct attribute_group ally_left_trigger_group = {
+ .name = "left_trigger",
+ .attrs = left_trigger_attrs,
+};
+
+static const struct attribute_group ally_right_trigger_group = {
+ .name = "right_trigger",
+ .attrs = right_trigger_attrs,
+};
static const struct attribute_group *const ally_cal_attr_groups[] = {
&ally_left_joystick_axis_group,
&ally_right_joystick_axis_group,
+ &ally_left_trigger_group,
+ &ally_right_trigger_group,
};
static struct attribute *ally_config_attrs[] = {
@@ -1376,6 +1577,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
ally_vibration_defaults();
ally_joystick_defaults();
+ ally_trigger_defaults();
guard(mutex)(&ally_prefs.lock);
@@ -1659,6 +1861,16 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore joystick thresholds: %d\n", ret);
}
+ if (ally_prefs.initialized && ally_prefs.user_cal_support) {
+ ret = ally_set_trigger_ranges(hdev,
+ ally_prefs.left_trigger_min,
+ ally_prefs.left_trigger_max,
+ ally_prefs.right_trigger_min,
+ ally_prefs.right_trigger_max);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore trigger ranges: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 07/17] HID: asus: add joysticks anti-deadzone configuration
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (5 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 06/17] HID: asus: add triggers " Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:51 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 08/17] HID: asus: add support for response curve Denis Benato
` (9 subsequent siblings)
16 siblings, 1 reply; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG Ally devices allow configuring the anti-deadzone parameter for
the resistive joysticks devices as over time those develops drift,
therefore allow userspace to configure the anti-deadzone by exposing
relevant sysfs attributes.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/hid-asus.c | 158 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 155 insertions(+), 3 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 5cf7258bc427..9bfdde560834 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -784,6 +784,9 @@ static struct ally_prefs {
u8 left_trigger_max;
u8 right_trigger_min;
u8 right_trigger_max;
+
+ u8 left_anti_deadzone;
+ u8 right_anti_deadzone;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -1321,11 +1324,152 @@ static struct device_attribute dev_attr_right_joystick_outer_threshold =
static struct device_attribute dev_attr_right_joystick_outer_threshold_range =
__ATTR(outer_threshold_range, 0444, right_joystick_outer_threshold_range_show, NULL);
+/**
+ * ally_set_anti_deadzone() - Send the joystick anti-deadzone values to the device
+ * @hdev: HID device the packet is sent on
+ * @left: Anti-deadzone of the left joystick
+ * @right: Anti-deadzone of the right joystick
+ *
+ * Context: Caller must hold ally_prefs.lock.
+ *
+ * Return: 0 on success, -%EOPNOTSUPP if the device does not support
+ * anti-deadzone, -%ENOMEM if the command buffer cannot be allocated, or a
+ * negative error code if sending the packet fails.
+ */
+static int ally_set_anti_deadzone(struct hid_device *hdev, u8 left, u8 right)
+{
+ const u8 payload[] = { left, right };
+ size_t buf_len;
+ int ret;
+
+ if (!ally_prefs.anti_deadzone_support) {
+ hid_dbg(hdev, "Anti-deadzone not supported on this device\n");
+ return -EOPNOTSUPP;
+ }
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_ANTI_DEADZONE,
+ payload, sizeof(payload),
+ &buf_len);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set anti-deadzone values: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+/* Indices into ally_anti_deadzones[] */
+enum ally_joystick {
+ ALLY_LEFT_JOYSTICK,
+ ALLY_RIGHT_JOYSTICK,
+};
+
+static u8 *const ally_anti_deadzones[] = {
+ [ALLY_LEFT_JOYSTICK] = &ally_prefs.left_anti_deadzone,
+ [ALLY_RIGHT_JOYSTICK] = &ally_prefs.right_anti_deadzone,
+};
+
+static ssize_t ally_anti_deadzone_show(struct device *dev, char *buf,
+ enum ally_joystick which)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.anti_deadzone_support) {
+ hid_dbg(to_hid_device(dev), "Anti-deadzone not supported on this device\n");
+ return -EOPNOTSUPP;
+ }
+
+ return sysfs_emit(buf, "%u\n", *ally_anti_deadzones[which]);
+}
+
+static ssize_t ally_anti_deadzone_store(struct device *dev, const char *buf,
+ size_t count, enum ally_joystick which,
+ const char *group)
+{
+ u8 value, dz[ARRAY_SIZE(ally_anti_deadzones)];
+ struct hid_device *hdev = to_hid_device(dev);
+ unsigned int i;
+ int ret;
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > 100)
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.anti_deadzone_support) {
+ hid_dbg(hdev, "Anti-deadzone not supported on this device\n");
+ return -EOPNOTSUPP;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(dz); i++)
+ dz[i] = *ally_anti_deadzones[i];
+ dz[which] = value;
+
+ ret = ally_set_anti_deadzone(hdev, dz[ALLY_LEFT_JOYSTICK],
+ dz[ALLY_RIGHT_JOYSTICK]);
+ if (ret)
+ return ret;
+
+ *ally_anti_deadzones[which] = value;
+ }
+
+ sysfs_notify(&dev->kobj, group, "anti_deadzone");
+
+ return count;
+}
+
+/* Generates <side>_joystick_anti_deadzone_{show,store,range_show}. */
+#define ALLY_ANTI_DEADZONE_FUNCS(side, SIDE) \
+static ssize_t side##_joystick_anti_deadzone_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return ally_anti_deadzone_show(dev, buf, ALLY_##SIDE##_JOYSTICK); \
+} \
+ \
+static ssize_t side##_joystick_anti_deadzone_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+{ \
+ return ally_anti_deadzone_store(dev, buf, count, ALLY_##SIDE##_JOYSTICK, \
+ #side "_joystick_axis"); \
+} \
+ \
+static ssize_t side##_joystick_anti_deadzone_range_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return sysfs_emit(buf, "0 100\n"); \
+}
+
+ALLY_ANTI_DEADZONE_FUNCS(left, LEFT)
+ALLY_ANTI_DEADZONE_FUNCS(right, RIGHT)
+
+static struct device_attribute dev_attr_left_joystick_anti_deadzone =
+ __ATTR(anti_deadzone, 0644, left_joystick_anti_deadzone_show,
+ left_joystick_anti_deadzone_store);
+
+static struct device_attribute dev_attr_left_joystick_anti_deadzone_range =
+ __ATTR(anti_deadzone_range, 0444, left_joystick_anti_deadzone_range_show, NULL);
+
+static struct device_attribute dev_attr_right_joystick_anti_deadzone =
+ __ATTR(anti_deadzone, 0644, right_joystick_anti_deadzone_show,
+ right_joystick_anti_deadzone_store);
+
+static struct device_attribute dev_attr_right_joystick_anti_deadzone_range =
+ __ATTR(anti_deadzone_range, 0444, right_joystick_anti_deadzone_range_show, NULL);
+
static struct attribute *left_joystick_axis_attrs[] = {
&dev_attr_left_joystick_inner_threshold.attr,
&dev_attr_left_joystick_outer_threshold.attr,
&dev_attr_left_joystick_inner_threshold_range.attr,
&dev_attr_left_joystick_outer_threshold_range.attr,
+ &dev_attr_left_joystick_anti_deadzone.attr,
+ &dev_attr_left_joystick_anti_deadzone_range.attr,
NULL
};
@@ -1334,6 +1478,8 @@ static struct attribute *right_joystick_axis_attrs[] = {
&dev_attr_right_joystick_outer_threshold.attr,
&dev_attr_right_joystick_inner_threshold_range.attr,
&dev_attr_right_joystick_outer_threshold_range.attr,
+ &dev_attr_right_joystick_anti_deadzone.attr,
+ &dev_attr_right_joystick_anti_deadzone_range.attr,
NULL
};
@@ -1602,7 +1748,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
}
/* Skip the calibration groups when the capability is missing. */
- if (ally_prefs.user_cal_support) {
+ if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support) {
int cal_i;
for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
@@ -1643,8 +1789,7 @@ static void ally_sysfs_remove(struct hid_device *hdev)
for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
- if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
- ally_prefs.resp_curve_support) {
+ if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support) {
for (i = 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]);
}
@@ -1871,6 +2016,13 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore trigger ranges: %d\n", ret);
}
+ if (ally_prefs.initialized && ally_prefs.anti_deadzone_support) {
+ ret = ally_set_anti_deadzone(hdev, ally_prefs.left_anti_deadzone,
+ ally_prefs.right_anti_deadzone);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore anti-deadzone: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 08/17] HID: asus: add support for response curve
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (6 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 07/17] HID: asus: add joysticks anti-deadzone configuration Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 09/17] HID: asus: add support to force feedback Denis Benato
` (8 subsequent siblings)
16 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG ally devices allows configuring the response curve of both joysticks,
therefore add the ability of userspace to modify the response curve by
exposing relevant sysfs attributes.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/hid-asus.c | 277 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 275 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 9bfdde560834..783c78160277 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -24,6 +24,7 @@
#include <linux/cleanup.h>
#include <linux/compiler_attributes.h>
#include <linux/container_of.h>
+#include <linux/ctype.h>
#include <linux/dmi.h>
#include <linux/err.h>
#include <linux/hid.h>
@@ -753,6 +754,18 @@ static int ally_get_endpoint_address(struct hid_device *hdev)
* once. The only way to see a second one is to emulate it with
* uhid.
*/
+struct ally_joystick_resp_curve_param {
+ u8 move;
+ u8 resp;
+} __packed;
+
+struct ally_joystick_resp_curve {
+ struct ally_joystick_resp_curve_param entry_1;
+ struct ally_joystick_resp_curve_param entry_2;
+ struct ally_joystick_resp_curve_param entry_3;
+ struct ally_joystick_resp_curve_param entry_4;
+} __packed;
+
static struct ally_prefs {
/* Guards every field below, including across the feature report. */
struct mutex lock;
@@ -787,6 +800,9 @@ static struct ally_prefs {
u8 left_anti_deadzone;
u8 right_anti_deadzone;
+
+ struct ally_joystick_resp_curve left_curve;
+ struct ally_joystick_resp_curve right_curve;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -1463,6 +1479,239 @@ static struct device_attribute dev_attr_right_joystick_anti_deadzone =
static struct device_attribute dev_attr_right_joystick_anti_deadzone_range =
__ATTR(anti_deadzone_range, 0444, right_joystick_anti_deadzone_range_show, NULL);
+enum ally_joystick_side {
+ JOYSTICK_LEFT = 0,
+ JOYSTICK_RIGHT,
+};
+
+/**
+ * ally_set_joystick_resp_curve() - Set a joystick response curve
+ * @hdev: HID device the command packet is sent on
+ * @side: joystick to program, %JOYSTICK_LEFT or %JOYSTICK_RIGHT
+ * @curve: response curve to apply
+ *
+ * Context: Caller holds ally_prefs.lock.
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_set_joystick_resp_curve(struct hid_device *hdev,
+ enum ally_joystick_side side,
+ struct ally_joystick_resp_curve *curve)
+{
+ const u8 payload[] = {
+ side,
+ curve->entry_1.move, curve->entry_1.resp,
+ curve->entry_2.move, curve->entry_2.resp,
+ curve->entry_3.move, curve->entry_3.resp,
+ curve->entry_4.move, curve->entry_4.resp
+ };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_RESP_CURVE, payload,
+ sizeof(payload), &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+
+ /*
+ * Previous function returns the number of bytes sent, or a negative error code.
+ * From this function I only need either success or the failure.
+ */
+ return ret < 0 ? ret : 0;
+}
+
+static bool ally_resp_curve_is_valid(const struct ally_joystick_resp_curve *curve)
+{
+ return curve->entry_1.move < curve->entry_2.move &&
+ curve->entry_2.move < curve->entry_3.move &&
+ curve->entry_3.move < curve->entry_4.move;
+}
+
+/* Linear curve. */
+static const struct ally_joystick_resp_curve ally_linear_curve = {
+ .entry_1 = { .move = 0, .resp = 0 },
+ .entry_2 = { .move = 33, .resp = 33 },
+ .entry_3 = { .move = 66, .resp = 66 },
+ .entry_4 = { .move = 100, .resp = 100 },
+};
+
+static void ally_curve_defaults(void)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized)
+ return;
+
+ ally_prefs.left_curve = ally_linear_curve;
+ ally_prefs.right_curve = ally_linear_curve;
+}
+
+/* Indexed by enum ally_joystick_side */
+static struct ally_joystick_resp_curve * const ally_resp_curves[] = {
+ [JOYSTICK_LEFT] = &ally_prefs.left_curve,
+ [JOYSTICK_RIGHT] = &ally_prefs.right_curve,
+};
+
+static ssize_t response_curve_show(enum ally_joystick_side side, char *buf)
+{
+ const struct ally_joystick_resp_curve *curve = ally_resp_curves[side];
+
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.resp_curve_support)
+ return -EOPNOTSUPP;
+
+ return sysfs_emit(buf, "%u %u %u %u %u %u %u %u\n",
+ curve->entry_1.move, curve->entry_1.resp,
+ curve->entry_2.move, curve->entry_2.resp,
+ curve->entry_3.move, curve->entry_3.resp,
+ curve->entry_4.move, curve->entry_4.resp);
+}
+
+/**
+ * ally_parse_u8_token() - Parse one decimal u8 from a sysfs write
+ * @buf: write buffer
+ * @count: number of bytes in @buf
+ * @pos: offset in @buf to start scanning at; on success it is advanced to
+ * the first byte after the token
+ * @val: where the parsed value is stored; only written on success
+ *
+ * Skips leading whitespace, then parses an optional '+' followed by one or
+ * more decimal digits. The token, sign included, may be at most 15 characters
+ * long and its value must fit in a u8.
+ *
+ * The scan is bounded by @count, not by a terminating NUL, so an embedded NUL
+ * is a byte in the write, not the end of the buffer.
+ *
+ * Return: 0 on success, -%EINVAL if there is no number at @pos, the token is
+ * too long, or the value does not fit in a u8. On failure @pos is left at an
+ * unspecified offset.
+ */
+static int ally_parse_u8_token(const char *buf, size_t count, size_t *pos, u8 *val)
+{
+ char token[16];
+ size_t n = 0;
+
+ while (*pos < count && isspace((unsigned char)buf[*pos]))
+ (*pos)++;
+
+ if (*pos < count && buf[*pos] == '+')
+ token[n++] = buf[(*pos)++];
+
+ if (*pos >= count || !isdigit((unsigned char)buf[*pos]))
+ return -EINVAL;
+
+ while (*pos < count && isdigit((unsigned char)buf[*pos])) {
+ if (n >= sizeof(token) - 1)
+ return -EINVAL;
+ token[n++] = buf[(*pos)++];
+ }
+
+ token[n] = '\0';
+
+ return kstrtou8(token, 10, val) ? -EINVAL : 0;
+}
+
+static ssize_t response_curve_store(struct device *dev,
+ enum ally_joystick_side side,
+ const char *group, const char *buf,
+ size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct ally_joystick_resp_curve curve;
+ size_t pos = 0;
+ int ret, i;
+ u8 v[8];
+
+ /*
+ * Parse and validate the whole curve before taking the lock: a
+ * write is all-or-nothing and a rejected one must leave the
+ * recorded curve untouched.
+ */
+ for (i = 0; i < ARRAY_SIZE(v); i++) {
+ ret = ally_parse_u8_token(buf, count, &pos, &v[i]);
+ if (ret || v[i] > 100)
+ return -EINVAL;
+ }
+
+ for (; pos < count; pos++)
+ if (!isspace((unsigned char)buf[pos]))
+ return -EINVAL;
+
+ curve = (struct ally_joystick_resp_curve) {
+ .entry_1 = { .move = v[0], .resp = v[1] },
+ .entry_2 = { .move = v[2], .resp = v[3] },
+ .entry_3 = { .move = v[4], .resp = v[5] },
+ .entry_4 = { .move = v[6], .resp = v[7] },
+ };
+
+ if (!ally_resp_curve_is_valid(&curve))
+ return -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.resp_curve_support)
+ return -EOPNOTSUPP;
+
+ ret = ally_set_joystick_resp_curve(hdev, side, &curve);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set joystick response curve: %d\n",
+ ret);
+ return ret;
+ }
+
+ *ally_resp_curves[side] = curve;
+ }
+
+ sysfs_notify(&dev->kobj, group, "response_curve");
+
+ return count;
+}
+
+/* Generates <side>_response_curve_{show,store}. */
+#define ALLY_RESP_CURVE_FUNCS(side, SIDE) \
+static ssize_t side##_response_curve_show(struct device *dev, \
+ struct device_attribute *attr, \
+ char *buf) \
+{ \
+ return response_curve_show(JOYSTICK_##SIDE, buf); \
+} \
+ \
+static ssize_t side##_response_curve_store(struct device *dev, \
+ struct device_attribute *attr, \
+ const char *buf, size_t count) \
+{ \
+ return response_curve_store(dev, JOYSTICK_##SIDE, \
+ #side "_joystick_axis", buf, count); \
+}
+
+ALLY_RESP_CURVE_FUNCS(left, LEFT)
+ALLY_RESP_CURVE_FUNCS(right, RIGHT)
+
+static ssize_t response_curve_index_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf,
+ "move_1 resp_1 move_2 resp_2 move_3 resp_3 move_4 resp_4\n"
+ "\n"
+ "move_N: joystick deflection percentage at which curve point N\n"
+ "is placed (0-100, each move_N must be greater than the\n"
+ "previous one). resp_N: response percentage applied from\n"
+ "point N on (0-100).\n");
+}
+
+static struct device_attribute dev_attr_left_response_curve =
+ __ATTR(response_curve, 0644, left_response_curve_show,
+ left_response_curve_store);
+static struct device_attribute dev_attr_right_response_curve =
+ __ATTR(response_curve, 0644, right_response_curve_show,
+ right_response_curve_store);
+static struct device_attribute dev_attr_left_response_curve_index =
+ __ATTR(response_curve_index, 0444, response_curve_index_show, NULL);
+static struct device_attribute dev_attr_right_response_curve_index =
+ __ATTR(response_curve_index, 0444, response_curve_index_show, NULL);
+
static struct attribute *left_joystick_axis_attrs[] = {
&dev_attr_left_joystick_inner_threshold.attr,
&dev_attr_left_joystick_outer_threshold.attr,
@@ -1470,6 +1719,8 @@ static struct attribute *left_joystick_axis_attrs[] = {
&dev_attr_left_joystick_outer_threshold_range.attr,
&dev_attr_left_joystick_anti_deadzone.attr,
&dev_attr_left_joystick_anti_deadzone_range.attr,
+ &dev_attr_left_response_curve.attr,
+ &dev_attr_left_response_curve_index.attr,
NULL
};
@@ -1480,6 +1731,8 @@ static struct attribute *right_joystick_axis_attrs[] = {
&dev_attr_right_joystick_outer_threshold_range.attr,
&dev_attr_right_joystick_anti_deadzone.attr,
&dev_attr_right_joystick_anti_deadzone_range.attr,
+ &dev_attr_right_response_curve.attr,
+ &dev_attr_right_response_curve_index.attr,
NULL
};
@@ -1724,6 +1977,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
ally_vibration_defaults();
ally_joystick_defaults();
ally_trigger_defaults();
+ ally_curve_defaults();
guard(mutex)(&ally_prefs.lock);
@@ -1748,7 +2002,8 @@ static int ally_prefs_setup(struct hid_device *hdev)
}
/* Skip the calibration groups when the capability is missing. */
- if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support) {
+ if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
+ ally_prefs.resp_curve_support) {
int cal_i;
for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
@@ -1789,7 +2044,8 @@ static void ally_sysfs_remove(struct hid_device *hdev)
for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
- if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support) {
+ if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
+ ally_prefs.resp_curve_support) {
for (i = 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]);
}
@@ -2023,6 +2279,23 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore anti-deadzone: %d\n", ret);
}
+ /*
+ * The MCU does not keep the response curve across a reset.
+ * A sysfs write validates the curve before it is recorded, so the
+ * cache always holds one the controller accepted.
+ */
+ if (ally_prefs.initialized && ally_prefs.resp_curve_support) {
+ ret = ally_set_joystick_resp_curve(hdev, JOYSTICK_LEFT,
+ &ally_prefs.left_curve);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore left response curve: %d\n", ret);
+
+ ret = ally_set_joystick_resp_curve(hdev, JOYSTICK_RIGHT,
+ &ally_prefs.right_curve);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore right response curve: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 09/17] HID: asus: add support to force feedback
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (7 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 08/17] HID: asus: add support for response curve Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 10/17] HID: asus: add support for gamepad mode Denis Benato
` (7 subsequent siblings)
16 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
Unlike ROG ally the X version and following ones uses DInput protocol
and the force feedback needs to be implemented as its protocol is
vendor-specific, therefore add support for FF_RUMBLE with magnitude
scaling on a work-queue based approach to avoid using possibly
sleeping calls in atomic context.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Khamunetri Clark <khamunetriclark@gmail.com>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
---
drivers/hid/Kconfig | 1 +
drivers/hid/hid-asus.c | 143 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 144 insertions(+)
diff --git a/drivers/hid/Kconfig b/drivers/hid/Kconfig
index 2481bc01c435..dd411f6eff4f 100644
--- a/drivers/hid/Kconfig
+++ b/drivers/hid/Kconfig
@@ -189,6 +189,7 @@ config HID_ASUS
depends on USB_HID
depends on LEDS_CLASS
depends on ASUS_WMI || ASUS_WMI=n
+ select INPUT_FF_MEMLESS
select POWER_SUPPLY
help
Support for Asus notebook built-in keyboard and touchpad via i2c, and
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 783c78160277..3ade3dbda72e 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -22,6 +22,7 @@
#include <linux/acpi.h>
#include <linux/cleanup.h>
+#include <linux/compiler.h>
#include <linux/compiler_attributes.h>
#include <linux/container_of.h>
#include <linux/ctype.h>
@@ -30,6 +31,7 @@
#include <linux/hid.h>
#include <linux/jiffies.h>
#include <linux/kstrtox.h>
+#include <linux/limits.h>
#include <linux/list.h>
#include <linux/module.h>
#include <linux/mutex.h>
@@ -210,6 +212,23 @@ struct asus_touchpad_info {
int report_size;
};
+/* Output report 0x0D on the gamepad interface. */
+struct ally_ff_data {
+ u8 enable;
+ u8 magnitude_left;
+ u8 magnitude_right;
+ u8 magnitude_strong;
+ u8 magnitude_weak;
+ u8 pulse_sustain_10ms;
+ u8 pulse_release_10ms;
+ u8 loop_count;
+} __packed;
+
+struct ally_ff_report {
+ u8 report_id;
+ struct ally_ff_data ff;
+} __packed;
+
struct ally_intf {
struct hid_device *hdev;
/* HID-core evdev of this interface. Reports for it land here. */
@@ -219,6 +238,14 @@ struct ally_intf {
u8 cad_state;
unsigned long cad_last_event_time;
struct delayed_work resume_work;
+ struct {
+ /* Guards live, update, and packet. */
+ spinlock_t lock;
+ bool live;
+ bool update;
+ struct ally_ff_report packet;
+ struct work_struct work;
+ } ff;
};
struct asus_drvdata {
@@ -325,10 +352,15 @@ enum ally_command_codes {
CMD_SET_ANTI_DEADZONE = 0x18,
};
+/* Rumble magnitudes use the hardware's 0..100 intensity range. */
+#define ALLY_FF_MAX_INTENSITY 100
+
static const u8 ally_force_feedback_off[] = {
0x0D, 0x0F, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xEB
};
+static_assert(sizeof(struct ally_ff_report) == sizeof(ally_force_feedback_off));
+
/*
* Each Ally interface owns its own ally_intf. A report is delivered on the
* evdev of the interface that received it. Nothing here points at another
@@ -2184,6 +2216,84 @@ static struct input_dev *ally_x_alloc_input_dev(struct hid_device *hdev,
return input_dev;
}
+static int ally_ff_output(struct hid_device *hdev, const void *buf, size_t len)
+{
+ u8 *dmabuf __free(kfree) = kmemdup(buf, len, GFP_KERNEL);
+
+ if (!dmabuf)
+ return -ENOMEM;
+
+ return hid_hw_output_report(hdev, dmabuf, len);
+}
+
+static int ally_ff_off(struct hid_device *hdev)
+{
+ return ally_ff_output(hdev, ally_force_feedback_off,
+ sizeof(ally_force_feedback_off));
+}
+
+static void ally_ff_work_fn(struct work_struct *work)
+{
+ struct ally_intf *intf = container_of(work, struct ally_intf, ff.work);
+ struct ally_ff_report report;
+ bool update = false;
+ int ret;
+
+ scoped_guard(spinlock_irqsave, &intf->ff.lock) {
+ if (!intf->ff.update)
+ break;
+
+ report = intf->ff.packet;
+ intf->ff.update = false;
+ update = true;
+ }
+
+ if (!update)
+ return;
+
+ ret = ally_ff_output(intf->hdev, &report, sizeof(report));
+ if (ret < 0)
+ hid_err(intf->hdev, "Failed to send force-feedback: %d\n", ret);
+}
+
+static int ally_ff_play(struct input_dev *idev, void *data, struct ff_effect *effect)
+{
+ struct ally_intf *intf = input_get_drvdata(idev);
+
+ if (effect->type != FF_RUMBLE)
+ return 0;
+
+ guard(spinlock_irqsave)(&intf->ff.lock);
+
+ if (!intf->ff.live)
+ return 0;
+
+ intf->ff.packet.ff.magnitude_strong =
+ effect->u.rumble.strong_magnitude * ALLY_FF_MAX_INTENSITY / U16_MAX;
+ intf->ff.packet.ff.magnitude_weak =
+ effect->u.rumble.weak_magnitude * ALLY_FF_MAX_INTENSITY / U16_MAX;
+ intf->ff.update = true;
+ schedule_work(&intf->ff.work);
+ return 0;
+}
+
+static void ally_ff_quiesce(struct ally_intf *intf)
+{
+ int ret;
+
+ if (!intf || !intf->gamepad)
+ return;
+
+ scoped_guard(spinlock_irqsave, &intf->ff.lock)
+ intf->ff.live = false;
+
+ cancel_work_sync(&intf->ff.work);
+
+ ret = ally_ff_off(intf->hdev);
+ if (ret < 0)
+ hid_warn(intf->hdev, "Failed to stop force-feedback\n");
+}
+
static int ally_x_setup_input(struct hid_device *hdev, struct ally_intf *intf)
{
struct input_dev *input = ally_x_alloc_input_dev(hdev, intf);
@@ -2212,8 +2322,24 @@ static int ally_x_setup_input(struct hid_device *hdev, struct ally_intf *intf)
input_set_capability(input, EV_KEY, BTN_THUMBL);
input_set_capability(input, EV_KEY, BTN_THUMBR);
+ spin_lock_init(&intf->ff.lock);
+ INIT_WORK(&intf->ff.work, ally_ff_work_fn);
+ memcpy(&intf->ff.packet, ally_force_feedback_off, sizeof(intf->ff.packet));
+ intf->ff.update = false;
+ intf->ff.live = false;
+
+ input_set_capability(input, EV_FF, FF_RUMBLE);
+ ret = input_ff_create_memless(input, NULL, ally_ff_play);
+ if (ret) {
+ hid_err(hdev, "Failed to create force-feedback: %d\n", ret);
+ input_free_device(input);
+ return ret;
+ }
+
+ intf->ff.live = true;
ret = input_register_device(input);
if (ret) {
+ intf->ff.live = false;
hid_err(hdev, "Failed to register Ally X gamepad device: %d\n", ret);
input_free_device(input);
return ret;
@@ -2404,6 +2530,14 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
hid_err(hdev, "Failed to create Ally X gamepad device.\n");
return ERR_PTR(ret);
}
+
+ /*
+ * This is the interface that owns the rumble output report.
+ * Failure to silence it is not fatal.
+ */
+ ret = ally_ff_off(hdev);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to disable force-feedback: %d\n", ret);
break;
case HID_ALLY_INTF_KEYBOARD_IN:
break;
@@ -2426,6 +2560,13 @@ static int hid_asus_ally_reset_resume(struct hid_device *hdev, struct ally_intf
return 0;
ep = ally_get_endpoint_address(hdev);
+ if (ep == HID_ALLY_X_INTF_IN) {
+ ret = ally_ff_off(hdev);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to disable force-feedback: %d\n", ret);
+ return 0;
+ }
+
if (ep != HID_ALLY_INTF_CFG_IN)
return 0;
@@ -3914,6 +4055,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
ally_quiesce(drvdata->ally);
ally_sysfs_remove(hdev);
+ ally_ff_quiesce(drvdata->ally);
}
asus_worker_stop(drvdata->worker);
@@ -3937,6 +4079,7 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
ally_quiesce(drvdata->ally);
ally_sysfs_remove(hdev);
+ ally_ff_quiesce(drvdata->ally);
}
asus_worker_stop(drvdata->worker);
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 10/17] HID: asus: add support for gamepad mode
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (8 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 09/17] HID: asus: add support to force feedback Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 11/17] HID: asus: add support for turbo buttons Denis Benato
` (6 subsequent siblings)
16 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG Ally devices can emulate either a mouse+keyboard (desktop mode)
or an gamepad device (xbox360 controller in ROG ally and a custom
DInput device on newer models): add support for switching the current
controller mode.
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 130 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 129 insertions(+), 1 deletion(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 3ade3dbda72e..3e58c5ca0cbd 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -352,6 +352,20 @@ enum ally_command_codes {
CMD_SET_ANTI_DEADZONE = 0x18,
};
+enum ally_gamepad_mode_index {
+ ALLY_GAMEPAD_MODE_GAMEPAD = 0x01,
+ ALLY_GAMEPAD_MODE_KEYBOARD = 0x02,
+};
+
+static const char *const ally_gamepad_mode_text[] = {
+ "gamepad", "desktop"
+};
+
+static const u8 ally_gamepad_mode_value[] = {
+ ALLY_GAMEPAD_MODE_GAMEPAD,
+ ALLY_GAMEPAD_MODE_KEYBOARD
+};
+
/* Rumble magnitudes use the hardware's 0..100 intensity range. */
#define ALLY_FF_MAX_INTENSITY 100
@@ -1372,6 +1386,112 @@ static struct device_attribute dev_attr_right_joystick_outer_threshold =
static struct device_attribute dev_attr_right_joystick_outer_threshold_range =
__ATTR(outer_threshold_range, 0444, right_joystick_outer_threshold_range_show, NULL);
+/**
+ * ally_set_gamepad_mode() - Set the gamepad operating mode
+ * @hdev: HID device the command packet is sent on
+ * @mode: %ALLY_GAMEPAD_MODE_GAMEPAD or %ALLY_GAMEPAD_MODE_KEYBOARD
+ *
+ * Context: Caller holds ally_prefs.lock.
+ * Return: 0 on success, negative errno on failure.
+ */
+static int ally_set_gamepad_mode(struct hid_device *hdev, u8 mode)
+{
+ const u8 payload[] = { mode };
+ size_t buf_len;
+ int ret;
+
+ if (mode < ALLY_GAMEPAD_MODE_GAMEPAD || mode > ALLY_GAMEPAD_MODE_KEYBOARD) {
+ hid_err(hdev, "Invalid gamepad mode: %u\n", mode);
+ return -EINVAL;
+ }
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_GAMEPAD_MODE,
+ payload, sizeof(payload),
+ &buf_len);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set gamepad mode: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static ssize_t gamepad_mode_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ u8 mode_byte;
+ int i;
+
+ guard(mutex)(&ally_prefs.lock);
+
+ mode_byte = ally_prefs.gamepad_mode;
+ for (i = 0; i < ARRAY_SIZE(ally_gamepad_mode_value); i++) {
+ if (ally_gamepad_mode_value[i] == mode_byte)
+ return sysfs_emit(buf, "%s\n", ally_gamepad_mode_text[i]);
+ }
+
+ /*
+ * The recorded mode only comes from the table above: an unknown
+ * byte means memory corruption, not a userspace-visible value.
+ */
+ return -EIO;
+}
+
+static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ u8 mode_byte;
+ int mode, ret;
+
+ mode = sysfs_match_string(ally_gamepad_mode_text, buf);
+ if (mode < 0)
+ return mode;
+
+ /* The text index maps to the byte the MCU accepts. */
+ mode_byte = ally_gamepad_mode_value[mode];
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ ret = ally_set_gamepad_mode(hdev, mode_byte);
+ if (ret < 0)
+ return ret;
+
+ ally_prefs.gamepad_mode = mode_byte;
+ }
+
+ hid_dbg(hdev, "Set gamepad mode to %s\n", ally_gamepad_mode_text[mode]);
+ sysfs_notify(&dev->kobj, NULL, "gamepad_mode");
+
+ return count;
+}
+
+static ssize_t gamepad_mode_available_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ ssize_t len = 0;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(ally_gamepad_mode_text); i++) {
+ if (!ally_gamepad_mode_text[i] || ally_gamepad_mode_text[i][0] == '\0')
+ continue;
+
+ len += sysfs_emit_at(buf, len, "%s ", ally_gamepad_mode_text[i]);
+ }
+
+ /* Replace the last space with a newline. */
+ if (len > 0)
+ buf[len - 1] = '\n';
+
+ return len;
+}
+
+static DEVICE_ATTR_RW(gamepad_mode);
+static DEVICE_ATTR_RO(gamepad_mode_available);
+
/**
* ally_set_anti_deadzone() - Send the joystick anti-deadzone values to the device
* @hdev: HID device the packet is sent on
@@ -1981,6 +2101,8 @@ static const struct attribute_group *const ally_cal_attr_groups[] = {
static struct attribute *ally_config_attrs[] = {
&dev_attr_xbox_controller.attr,
+ &dev_attr_gamepad_mode.attr,
+ &dev_attr_gamepad_mode_available.attr,
NULL
};
@@ -2014,7 +2136,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
guard(mutex)(&ally_prefs.lock);
if (!ally_prefs.initialized) {
- ally_prefs.gamepad_mode = 0x01;
+ ally_prefs.gamepad_mode = ALLY_GAMEPAD_MODE_GAMEPAD;
ally_prefs.xbox_controller_enabled = true;
ally_prefs.initialized = true;
}
@@ -2422,6 +2544,12 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore right response curve: %d\n", ret);
}
+ if (ally_prefs.initialized) {
+ ret = ally_set_gamepad_mode(hdev, ally_prefs.gamepad_mode);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore gamepad mode: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 11/17] HID: asus: add support for turbo buttons
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (9 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 10/17] HID: asus: add support for gamepad mode Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:54 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 12/17] HID: asus: add support for btn remapping Denis Benato
` (5 subsequent siblings)
16 siblings, 1 reply; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
ROG ally devices supports a feature called turbo buttons that allows
the user to quickly toggle button state by keeping a button pressed,
therefore allow userspace to configure the feature by exposing
relevant sysfs attributes.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Khamunetri Clark <khamunetriclark@gmail.com>
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
---
drivers/hid/hid-asus.c | 410 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 397 insertions(+), 13 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 3e58c5ca0cbd..d20e433af574 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -812,6 +812,80 @@ struct ally_joystick_resp_curve {
struct ally_joystick_resp_curve_param entry_4;
} __packed;
+enum ally_button_id {
+ ALLY_BTN_A,
+ ALLY_BTN_B,
+ ALLY_BTN_X,
+ ALLY_BTN_Y,
+ ALLY_BTN_LB,
+ ALLY_BTN_RB,
+ ALLY_BTN_DU,
+ ALLY_BTN_DD,
+ ALLY_BTN_DL,
+ ALLY_BTN_DR,
+ ALLY_BTN_J0B,
+ ALLY_BTN_J1B,
+ ALLY_BTN_MENU,
+ ALLY_BTN_VIEW,
+ ALLY_BTN_M1,
+ ALLY_BTN_M2,
+ ALLY_BTN_MAX
+};
+
+static const char *const ally_button_names[ALLY_BTN_MAX] = {
+ [ALLY_BTN_A] = "btn_a",
+ [ALLY_BTN_B] = "btn_b",
+ [ALLY_BTN_X] = "btn_x",
+ [ALLY_BTN_Y] = "btn_y",
+ [ALLY_BTN_LB] = "btn_lb",
+ [ALLY_BTN_RB] = "btn_rb",
+ [ALLY_BTN_DU] = "dpad_up",
+ [ALLY_BTN_DD] = "dpad_down",
+ [ALLY_BTN_DL] = "dpad_left",
+ [ALLY_BTN_DR] = "dpad_right",
+ [ALLY_BTN_J0B] = "btn_l3",
+ [ALLY_BTN_J1B] = "btn_r3",
+ [ALLY_BTN_MENU] = "btn_menu",
+ [ALLY_BTN_VIEW] = "btn_view",
+ [ALLY_BTN_M1] = "btn_m1",
+ [ALLY_BTN_M2] = "btn_m2",
+};
+
+struct ally_btn_turbo_params {
+ u8 turbo;
+ u8 toggle;
+} __packed;
+
+#define ALLY_TURBO_PERIOD_MAX 20
+
+/* Payload order. Not the same order as enum ally_button_id. */
+struct ally_turbo_config {
+ struct ally_btn_turbo_params btn_du;
+ struct ally_btn_turbo_params btn_dd;
+ struct ally_btn_turbo_params btn_dl;
+ struct ally_btn_turbo_params btn_dr;
+ struct ally_btn_turbo_params btn_j0b;
+ struct ally_btn_turbo_params btn_j1b;
+ struct ally_btn_turbo_params btn_lb;
+ struct ally_btn_turbo_params btn_rb;
+ struct ally_btn_turbo_params btn_a;
+ struct ally_btn_turbo_params btn_b;
+ struct ally_btn_turbo_params btn_x;
+ struct ally_btn_turbo_params btn_y;
+ struct ally_btn_turbo_params btn_view;
+ struct ally_btn_turbo_params btn_menu;
+ struct ally_btn_turbo_params btn_m2;
+ struct ally_btn_turbo_params btn_m1;
+};
+
+struct ally_btn_sysfs_entry {
+ struct attribute_group group;
+ struct attribute *attrs[5];
+ enum ally_button_id btn;
+ struct device_attribute attr_turbo_period;
+ struct device_attribute attr_toggle_period;
+};
+
static struct ally_prefs {
/* Guards every field below, including across the feature report. */
struct mutex lock;
@@ -849,6 +923,9 @@ static struct ally_prefs {
struct ally_joystick_resp_curve left_curve;
struct ally_joystick_resp_curve right_curve;
+
+ struct ally_turbo_config turbo;
+ struct ally_btn_sysfs_entry *button_entries;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
@@ -2120,9 +2197,297 @@ static const struct attribute_group ally_attr_groups[] = {
},
};
+static struct ally_btn_turbo_params *ally_btn_param(enum ally_button_id btn)
+{
+ struct ally_btn_turbo_params *table[ALLY_BTN_MAX] = {
+ [ALLY_BTN_A] = &ally_prefs.turbo.btn_a,
+ [ALLY_BTN_B] = &ally_prefs.turbo.btn_b,
+ [ALLY_BTN_X] = &ally_prefs.turbo.btn_x,
+ [ALLY_BTN_Y] = &ally_prefs.turbo.btn_y,
+ [ALLY_BTN_LB] = &ally_prefs.turbo.btn_lb,
+ [ALLY_BTN_RB] = &ally_prefs.turbo.btn_rb,
+ [ALLY_BTN_DU] = &ally_prefs.turbo.btn_du,
+ [ALLY_BTN_DD] = &ally_prefs.turbo.btn_dd,
+ [ALLY_BTN_DL] = &ally_prefs.turbo.btn_dl,
+ [ALLY_BTN_DR] = &ally_prefs.turbo.btn_dr,
+ [ALLY_BTN_J0B] = &ally_prefs.turbo.btn_j0b,
+ [ALLY_BTN_J1B] = &ally_prefs.turbo.btn_j1b,
+ [ALLY_BTN_MENU] = &ally_prefs.turbo.btn_menu,
+ [ALLY_BTN_VIEW] = &ally_prefs.turbo.btn_view,
+ [ALLY_BTN_M1] = &ally_prefs.turbo.btn_m1,
+ [ALLY_BTN_M2] = &ally_prefs.turbo.btn_m2,
+ };
+
+ if (btn >= ALLY_BTN_MAX)
+ return NULL;
+
+ return table[btn];
+}
+
+/** ally_set_turbo_params() - Set the turbo parameters
+ *
+ * Caller holds ally_prefs.lock.
+ *
+ * @hdev: The HID device to send the packet to.
+ * Return: 0 on success, negative error code on failure.
+ */
+static int ally_set_turbo_params(struct hid_device *hdev)
+{
+ struct ally_turbo_config *turbo = &ally_prefs.turbo;
+ const u8 payload[] = {
+ turbo->btn_du.turbo, turbo->btn_du.toggle,
+ turbo->btn_dd.turbo, turbo->btn_dd.toggle,
+ turbo->btn_dl.turbo, turbo->btn_dl.toggle,
+ turbo->btn_dr.turbo, turbo->btn_dr.toggle,
+ turbo->btn_j0b.turbo, turbo->btn_j0b.toggle,
+ turbo->btn_j1b.turbo, turbo->btn_j1b.toggle,
+ turbo->btn_lb.turbo, turbo->btn_lb.toggle,
+ turbo->btn_rb.turbo, turbo->btn_rb.toggle,
+ turbo->btn_a.turbo, turbo->btn_a.toggle,
+ turbo->btn_b.turbo, turbo->btn_b.toggle,
+ turbo->btn_x.turbo, turbo->btn_x.toggle,
+ turbo->btn_y.turbo, turbo->btn_y.toggle,
+ turbo->btn_view.turbo, turbo->btn_view.toggle,
+ turbo->btn_menu.turbo, turbo->btn_menu.toggle,
+ turbo->btn_m2.turbo, turbo->btn_m2.toggle,
+ turbo->btn_m1.turbo, turbo->btn_m1.toggle,
+ };
+ size_t buf_len;
+ int ret;
+
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_TURBO_PARAMS, payload,
+ sizeof(payload), &buf_len);
+
+ if (!buf)
+ return -ENOMEM;
+
+ ret = ally_gamepad_send_packet(hdev, buf, buf_len);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to set turbo parameters: %d\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static ssize_t btn_turbo_period_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct ally_btn_sysfs_entry *entry;
+ struct ally_btn_turbo_params *params;
+
+ entry = container_of(attr, struct ally_btn_sysfs_entry, attr_turbo_period);
+
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.turbo_support)
+ return -EOPNOTSUPP;
+
+ params = ally_btn_param(entry->btn);
+ if (!params)
+ return -ENODEV;
+
+ return sysfs_emit(buf, "%hhu\n", params->turbo);
+}
+
+static ssize_t btn_turbo_period_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct ally_btn_sysfs_entry *entry;
+ struct hid_device *hdev = to_hid_device(dev);
+ struct ally_btn_turbo_params *params;
+ u8 old_value, value;
+ int ret;
+
+ entry = container_of(attr, struct ally_btn_sysfs_entry, attr_turbo_period);
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret || value > ALLY_TURBO_PERIOD_MAX)
+ return ret ? ret : -EINVAL;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.turbo_support)
+ return -EOPNOTSUPP;
+
+ params = ally_btn_param(entry->btn);
+ if (!params)
+ return -ENODEV;
+
+ old_value = params->turbo;
+ params->turbo = value;
+ ret = ally_set_turbo_params(hdev);
+ if (ret) {
+ params->turbo = old_value;
+ return ret;
+ }
+ }
+
+ sysfs_notify(&dev->kobj, entry->group.name, "turbo_period");
+
+ return count;
+}
+
+static ssize_t btn_toggle_period_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct ally_btn_sysfs_entry *entry;
+ struct ally_btn_turbo_params *params;
+
+ entry = container_of(attr, struct ally_btn_sysfs_entry, attr_toggle_period);
+
+ guard(mutex)(&ally_prefs.lock);
+
+ if (!ally_prefs.turbo_support)
+ return -EOPNOTSUPP;
+
+ params = ally_btn_param(entry->btn);
+ if (!params)
+ return -ENODEV;
+
+ return sysfs_emit(buf, "%hhu\n", params->toggle);
+}
+
+static ssize_t btn_toggle_period_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct ally_btn_sysfs_entry *entry;
+ struct hid_device *hdev = to_hid_device(dev);
+ struct ally_btn_turbo_params *params;
+ u8 old_value, value;
+ int ret;
+
+ entry = container_of(attr, struct ally_btn_sysfs_entry, attr_toggle_period);
+
+ ret = kstrtou8(buf, 10, &value);
+ if (ret)
+ return ret;
+
+ scoped_guard(mutex, &ally_prefs.lock) {
+ if (!ally_prefs.turbo_support)
+ return -EOPNOTSUPP;
+
+ params = ally_btn_param(entry->btn);
+ if (!params)
+ return -ENODEV;
+
+ old_value = params->toggle;
+ params->toggle = value;
+ ret = ally_set_turbo_params(hdev);
+ if (ret) {
+ params->toggle = old_value;
+ return ret;
+ }
+ }
+
+ sysfs_notify(&dev->kobj, entry->group.name, "toggle_period");
+
+ return count;
+}
+
+static ssize_t btn_turbo_period_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 20\n");
+}
+
+static ssize_t btn_toggle_period_range_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ return sysfs_emit(buf, "0 255\n");
+}
+
+static struct device_attribute dev_attr_btn_turbo_period_range =
+ __ATTR(turbo_period_range, 0444, btn_turbo_period_range_show, NULL);
+
+static struct device_attribute dev_attr_btn_toggle_period_range =
+ __ATTR(toggle_period_range, 0444, btn_toggle_period_range_show, NULL);
+
+static void ally_btn_turbo_init_attrs(struct ally_btn_sysfs_entry *entry)
+{
+ sysfs_attr_init(&entry->attr_turbo_period.attr);
+ entry->attr_turbo_period.attr.name = "turbo_period";
+ entry->attr_turbo_period.attr.mode = 0644;
+ entry->attr_turbo_period.show = btn_turbo_period_show;
+ entry->attr_turbo_period.store = btn_turbo_period_store;
+
+ sysfs_attr_init(&entry->attr_toggle_period.attr);
+ entry->attr_toggle_period.attr.name = "toggle_period";
+ entry->attr_toggle_period.attr.mode = 0644;
+ entry->attr_toggle_period.show = btn_toggle_period_show;
+ entry->attr_toggle_period.store = btn_toggle_period_store;
+
+ entry->attrs[0] = &entry->attr_turbo_period.attr;
+ entry->attrs[1] = &entry->attr_toggle_period.attr;
+ entry->attrs[2] = &dev_attr_btn_turbo_period_range.attr;
+ entry->attrs[3] = &dev_attr_btn_toggle_period_range.attr;
+ entry->attrs[4] = NULL;
+}
+
+static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_device *hdev)
+{
+ int i, ret;
+
+ if (!ally_prefs.turbo_support)
+ return NULL;
+
+ struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
+ if (!entries)
+ return ERR_PTR(-ENOMEM);
+
+ for (i = 0; i < ALLY_BTN_MAX; i++) {
+ entries[i].btn = i;
+
+ if (!ally_btn_param(i)) {
+ hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
+ ret = -EINVAL;
+ goto ally_btn_err_cleanup;
+ }
+
+ ally_btn_turbo_init_attrs(&entries[i]);
+ entries[i].group.name = ally_button_names[i];
+ entries[i].group.attrs = entries[i].attrs;
+
+ ret = sysfs_create_group(&hdev->dev.kobj, &entries[i].group);
+ if (ret < 0) {
+ hid_err(hdev, "Failed to create sysfs group for %s: %d\n",
+ ally_button_names[i], ret);
+ goto ally_btn_err_cleanup;
+ }
+ }
+
+ return no_free_ptr(entries);
+
+ally_btn_err_cleanup:
+ while (i--)
+ sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
+
+ return ERR_PTR(ret);
+}
+
+static void ally_remove_button_attributes(struct hid_device *hdev)
+{
+ struct ally_btn_sysfs_entry *entries;
+ int i;
+
+ entries = ally_prefs.button_entries;
+ if (!entries)
+ return;
+
+ for (i = 0; i < ALLY_BTN_MAX; i++)
+ sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
+
+ kfree(entries);
+ ally_prefs.button_entries = NULL;
+}
+
static int ally_prefs_setup(struct hid_device *hdev)
{
- int ret, i;
+ struct ally_btn_sysfs_entry *entries = NULL;
+ int ret, i, cal_n = 0;
ret = ally_detect_capabilities(hdev);
if (ret)
@@ -2158,27 +2523,38 @@ static int ally_prefs_setup(struct hid_device *hdev)
/* Skip the calibration groups when the capability is missing. */
if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
ally_prefs.resp_curve_support) {
- int cal_i;
-
- for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
+ for (; cal_n < ARRAY_SIZE(ally_cal_attr_groups); cal_n++) {
ret = sysfs_create_group(&hdev->dev.kobj,
- ally_cal_attr_groups[cal_i]);
+ ally_cal_attr_groups[cal_n]);
if (ret) {
hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
- ally_cal_attr_groups[cal_i]->name, ret);
- while (cal_i--)
- sysfs_remove_group(&hdev->dev.kobj,
- ally_cal_attr_groups[cal_i]);
- for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
- sysfs_remove_group(&hdev->dev.kobj,
- &ally_attr_groups[i]);
- return ret;
+ ally_cal_attr_groups[cal_n]->name, ret);
+ goto err_cal_groups;
}
}
}
+ if (ally_prefs.turbo_support) {
+ entries = ally_create_button_attributes(hdev);
+ if (IS_ERR(entries)) {
+ ret = PTR_ERR(entries);
+ hid_err(hdev, "Failed to create button attributes: %d\n", ret);
+ goto err_cal_groups;
+ }
+ }
+
+ ally_prefs.button_entries = entries;
ally_prefs.sysfs_live = true;
return 0;
+
+err_cal_groups:
+ while (cal_n--)
+ sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[cal_n]);
+
+ for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
+ sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+
+ return ret;
}
static void ally_sysfs_remove(struct hid_device *hdev)
@@ -2195,6 +2571,8 @@ static void ally_sysfs_remove(struct hid_device *hdev)
ally_prefs.sysfs_live = false;
+ ally_remove_button_attributes(hdev);
+
for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
@@ -2550,6 +2928,12 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore gamepad mode: %d\n", ret);
}
+ if (ally_prefs.initialized && ally_prefs.turbo_support) {
+ ret = ally_set_turbo_params(hdev);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to restore turbo parameters: %d\n", ret);
+ }
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 12/17] HID: asus: add support for btn remapping
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (10 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 11/17] HID: asus: add support for turbo buttons Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 18:00 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 13/17] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
` (4 subsequent siblings)
16 siblings, 1 reply; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
Add support for buttons remapping: the ability to assign different
button events to each one of the physical button on the device.
Each button exposes a remap_index sysfs attribute listing the valid
remap target names for the current gamepad mode: M1 and M2 can be
remapped to anything at any time, while other buttons may only be
remapped to gamepad codes in gamepad mode and keyboard/mouse/media
codes in desktop mode.
When the gamepad mode is changed the firmware automatically restores
its internal defaults, so the software cache is refreshed to reflect
the hardware state.
Co-developed-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Luke Jones <luke@ljones.dev>
Signed-off-by: Denis Benato <denis.benato@linux.dev>
Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
---
drivers/hid/hid-asus.c | 896 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 874 insertions(+), 22 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index d20e433af574..b94cb8c50b28 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -41,6 +41,7 @@
#include <linux/usb.h> /* For to_usb_interface for T100 touchpad intf check */
#include <linux/power_supply.h>
#include <linux/stddef.h>
+#include <linux/string.h>
#include <linux/sysfs.h>
#include <linux/leds.h>
#include <linux/types.h>
@@ -108,6 +109,10 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_ALLY_TRIGGER_RANGE_UPPER_MIN 70
#define ASUS_ALLY_TRIGGER_RANGE_UPPER_MAX 100
+#define ASUS_ALLY_BTN_CODE_LEN 0x2C
+
+#define ASUS_ALLY_BTN_FUNCTIONS_LEN 11
+
/* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
#define ASUS_SPURIOUS_CODE_0XEA 0xea
#define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -878,12 +883,215 @@ struct ally_turbo_config {
struct ally_btn_turbo_params btn_m1;
};
+struct button_remap_attr {
+ struct device_attribute dev_attr;
+ enum ally_button_id button_id;
+ bool is_macro;
+};
+
+#define to_button_remap_attr(x) container_of(x, struct button_remap_attr, dev_attr)
+
struct ally_btn_sysfs_entry {
struct attribute_group group;
- struct attribute *attrs[5];
+ struct attribute *attrs[8];
enum ally_button_id btn;
struct device_attribute attr_turbo_period;
struct device_attribute attr_toggle_period;
+ struct button_remap_attr *remap_attr;
+ struct button_remap_attr *macro_attr;
+ struct device_attribute attr_remap_available;
+};
+
+enum btn_map_type {
+ BTN_TYPE_NONE = 0,
+ BTN_TYPE_PAD = 0x01,
+ BTN_TYPE_KB = 0x02,
+ BTN_TYPE_MOUSE = 0x03,
+ BTN_TYPE_MEDIA = 0x05,
+};
+
+struct btn_code_map {
+ unsigned char type;
+ unsigned char value;
+ const char *name;
+};
+
+static const struct btn_code_map ally_btn_codes[] = {
+ { BTN_TYPE_NONE, 0x00, "NONE" },
+ /* Gamepad button codes */
+ { BTN_TYPE_PAD, 0x01, "BTN_SOUTH" },
+ { BTN_TYPE_PAD, 0x02, "BTN_EAST" },
+ { BTN_TYPE_PAD, 0x03, "BTN_NORTH" },
+ { BTN_TYPE_PAD, 0x04, "BTN_WEST" },
+ { BTN_TYPE_PAD, 0x05, "BTN_TL" },
+ { BTN_TYPE_PAD, 0x06, "BTN_TR" },
+ { BTN_TYPE_PAD, 0x07, "BTN_THUMBL" },
+ { BTN_TYPE_PAD, 0x08, "BTN_THUMBR" },
+ { BTN_TYPE_PAD, 0x09, "ABS_HAT0Y_NEG" },
+ { BTN_TYPE_PAD, 0x0A, "ABS_HAT0Y_POS" },
+ { BTN_TYPE_PAD, 0x0B, "ABS_HAT0X_NEG" },
+ { BTN_TYPE_PAD, 0x0C, "ABS_HAT0X_POS" },
+ { BTN_TYPE_PAD, 0x0D, "ABS_Z" },
+ { BTN_TYPE_PAD, 0x0E, "ABS_RZ" },
+ { BTN_TYPE_PAD, 0x11, "BTN_SELECT" },
+ { BTN_TYPE_PAD, 0x12, "BTN_START" },
+ { BTN_TYPE_PAD, 0x13, "BTN_MODE" },
+
+ /* Keyboard button codes */
+ { BTN_TYPE_KB, 0x8E, "FN_M2" },
+ { BTN_TYPE_KB, 0x8F, "FN_M1" },
+ { BTN_TYPE_KB, 0x76, "KEY_ESC" },
+ { BTN_TYPE_KB, 0x05, "KEY_F1" },
+ { BTN_TYPE_KB, 0x06, "KEY_F2" },
+ { BTN_TYPE_KB, 0x04, "KEY_F3" },
+ { BTN_TYPE_KB, 0x0C, "KEY_F4" },
+ { BTN_TYPE_KB, 0x03, "KEY_F5" },
+ { BTN_TYPE_KB, 0x0B, "KEY_F6" },
+ { BTN_TYPE_KB, 0x80, "KEY_F7" },
+ { BTN_TYPE_KB, 0x0A, "KEY_F8" },
+ { BTN_TYPE_KB, 0x01, "KEY_F9" },
+ { BTN_TYPE_KB, 0x09, "KEY_F10" },
+ { BTN_TYPE_KB, 0x78, "KEY_F11" },
+ { BTN_TYPE_KB, 0x07, "KEY_F12" },
+ { BTN_TYPE_KB, 0x08, "KEY_F13" },
+ { BTN_TYPE_KB, 0x10, "KEY_F14" },
+ { BTN_TYPE_KB, 0x18, "KEY_F15" },
+ { BTN_TYPE_KB, 0x0E, "KEY_GRAVE" }, /* backtick */
+ { BTN_TYPE_KB, 0x16, "KEY_1" },
+ { BTN_TYPE_KB, 0x1E, "KEY_2" },
+ { BTN_TYPE_KB, 0x26, "KEY_3" },
+ { BTN_TYPE_KB, 0x25, "KEY_4" },
+ { BTN_TYPE_KB, 0x2E, "KEY_5" },
+ { BTN_TYPE_KB, 0x36, "KEY_6" },
+ { BTN_TYPE_KB, 0x3D, "KEY_7" },
+ { BTN_TYPE_KB, 0x3E, "KEY_8" },
+ { BTN_TYPE_KB, 0x46, "KEY_9" },
+ { BTN_TYPE_KB, 0x45, "KEY_0" },
+ { BTN_TYPE_KB, 0x4E, "KEY_MINUS" }, /* hyphen */
+ { BTN_TYPE_KB, 0x55, "KEY_EQUAL" },
+ { BTN_TYPE_KB, 0x66, "KEY_BACKSPACE" },
+ { BTN_TYPE_KB, 0x0D, "KEY_TAB" },
+ { BTN_TYPE_KB, 0x15, "KEY_Q" },
+ { BTN_TYPE_KB, 0x1D, "KEY_W" },
+ { BTN_TYPE_KB, 0x24, "KEY_E" },
+ { BTN_TYPE_KB, 0x2D, "KEY_R" },
+ { BTN_TYPE_KB, 0x2C, "KEY_T" },
+ { BTN_TYPE_KB, 0x35, "KEY_Y" },
+ { BTN_TYPE_KB, 0x3C, "KEY_U" },
+ { BTN_TYPE_KB, 0x43, "KEY_I" },
+ { BTN_TYPE_KB, 0x44, "KEY_O" },
+ { BTN_TYPE_KB, 0x4D, "KEY_P" },
+ { BTN_TYPE_KB, 0x54, "KEY_LEFTBRACE" },
+ { BTN_TYPE_KB, 0x5B, "KEY_RIGHTBRACE" },
+ { BTN_TYPE_KB, 0x5D, "KEY_BACKSLASH" },
+ { BTN_TYPE_KB, 0x58, "KEY_CAPSLOCK" },
+ { BTN_TYPE_KB, 0x1C, "KEY_A" },
+ { BTN_TYPE_KB, 0x1B, "KEY_S" },
+ { BTN_TYPE_KB, 0x23, "KEY_D" },
+ { BTN_TYPE_KB, 0x2B, "KEY_F" },
+ { BTN_TYPE_KB, 0x34, "KEY_G" },
+ { BTN_TYPE_KB, 0x33, "KEY_H" },
+ { BTN_TYPE_KB, 0x3B, "KEY_J" },
+ { BTN_TYPE_KB, 0x42, "KEY_K" },
+ { BTN_TYPE_KB, 0x4B, "KEY_L" },
+ { BTN_TYPE_KB, 0x4C, "KEY_SEMICOLON" },
+ { BTN_TYPE_KB, 0x52, "KEY_APOSTROPHE" },
+ { BTN_TYPE_KB, 0x5A, "KEY_ENTER" },
+ { BTN_TYPE_KB, 0x88, "KEY_LEFTSHIFT" },
+ { BTN_TYPE_KB, 0x1A, "KEY_Z" },
+ { BTN_TYPE_KB, 0x22, "KEY_X" },
+ { BTN_TYPE_KB, 0x21, "KEY_C" },
+ { BTN_TYPE_KB, 0x2A, "KEY_V" },
+ { BTN_TYPE_KB, 0x32, "KEY_B" },
+ { BTN_TYPE_KB, 0x31, "KEY_N" },
+ { BTN_TYPE_KB, 0x3A, "KEY_M" },
+ { BTN_TYPE_KB, 0x41, "KEY_COMMA" },
+ { BTN_TYPE_KB, 0x49, "KEY_DOT" },
+ { BTN_TYPE_KB, 0x4A, "KEY_SLASH" },
+ { BTN_TYPE_KB, 0x89, "KEY_RIGHTSHIFT" },
+ { BTN_TYPE_KB, 0x82, "KEY_LEFTMETA" },
+ { BTN_TYPE_KB, 0x8A, "KEY_LEFTALT" },
+ { BTN_TYPE_KB, 0x29, "KEY_SPACE" },
+ { BTN_TYPE_KB, 0x8B, "KEY_RIGHTALT" },
+ { BTN_TYPE_KB, 0x84, "KEY_COMPOSE" },
+ { BTN_TYPE_KB, 0x8D, "KEY_RIGHTCTRL" },
+ { BTN_TYPE_KB, 0xC3, "KEY_SYSRQ" },
+ { BTN_TYPE_KB, 0x7E, "KEY_SCROLLLOCK" },
+ { BTN_TYPE_KB, 0x91, "KEY_PAUSE" },
+ { BTN_TYPE_KB, 0xC2, "KEY_INSERT" },
+ { BTN_TYPE_KB, 0x94, "KEY_HOME" },
+ { BTN_TYPE_KB, 0x96, "KEY_PAGEUP" },
+ { BTN_TYPE_KB, 0xC0, "KEY_DELETE" },
+ { BTN_TYPE_KB, 0x95, "KEY_END" },
+ { BTN_TYPE_KB, 0x97, "KEY_PAGEDOWN" },
+ { BTN_TYPE_KB, 0x98, "KEY_UP" },
+ { BTN_TYPE_KB, 0x99, "KEY_DOWN" },
+ { BTN_TYPE_KB, 0x9A, "KEY_LEFT" },
+ { BTN_TYPE_KB, 0x9B, "KEY_RIGHT" },
+
+ /* Numpad button codes */
+ { BTN_TYPE_KB, 0x77, "KEY_NUMLOCK" },
+ { BTN_TYPE_KB, 0x90, "KEY_KPSLASH" },
+ { BTN_TYPE_KB, 0x7C, "KEY_KPASTERISK" },
+ { BTN_TYPE_KB, 0x7B, "KEY_KPMINUS" },
+ { BTN_TYPE_KB, 0x70, "KEY_KP0" },
+ { BTN_TYPE_KB, 0x69, "KEY_KP1" },
+ { BTN_TYPE_KB, 0x72, "KEY_KP2" },
+ { BTN_TYPE_KB, 0x7A, "KEY_KP3" },
+ { BTN_TYPE_KB, 0x6B, "KEY_KP4" },
+ { BTN_TYPE_KB, 0x73, "KEY_KP5" },
+ { BTN_TYPE_KB, 0x74, "KEY_KP6" },
+ { BTN_TYPE_KB, 0x6C, "KEY_KP7" },
+ { BTN_TYPE_KB, 0x75, "KEY_KP8" },
+ { BTN_TYPE_KB, 0x7D, "KEY_KP9" },
+ { BTN_TYPE_KB, 0x79, "KEY_KPPLUS" },
+ { BTN_TYPE_KB, 0x81, "KEY_KPENTER" },
+ { BTN_TYPE_KB, 0x71, "KEY_KPDOT" },
+
+ /* Mouse button codes */
+ { BTN_TYPE_MOUSE, 0x01, "BTN_LEFT" },
+ { BTN_TYPE_MOUSE, 0x02, "BTN_RIGHT" },
+ { BTN_TYPE_MOUSE, 0x03, "BTN_MIDDLE" },
+ { BTN_TYPE_MOUSE, 0x04, "REL_WHEEL_HI_RES_UP" },
+ { BTN_TYPE_MOUSE, 0x05, "REL_WHEEL_HI_RES_DOWN" },
+
+ /* Media button codes */
+ { BTN_TYPE_MEDIA, 0x16, "MEDIA_SCREENSHOT" },
+ { BTN_TYPE_MEDIA, 0x19, "MEDIA_SHOW_KEYBOARD" },
+ { BTN_TYPE_MEDIA, 0x1C, "MEDIA_SHOW_DESKTOP" },
+ { BTN_TYPE_MEDIA, 0x1E, "MEDIA_START_RECORDING" },
+ { BTN_TYPE_MEDIA, 0x01, "MEDIA_MIC_OFF" },
+ { BTN_TYPE_MEDIA, 0x02, "MEDIA_VOL_DOWN" },
+ { BTN_TYPE_MEDIA, 0x03, "MEDIA_VOL_UP" },
+};
+
+/* Button pair indexes for mapping commands */
+enum btn_pair_index {
+ BTN_PAIR_DPAD_UPDOWN = 0x01,
+ BTN_PAIR_DPAD_LEFTRIGHT = 0x02,
+ BTN_PAIR_STICK_LR = 0x03,
+ BTN_PAIR_BUMPER_LR = 0x04,
+ BTN_PAIR_AB = 0x05,
+ BTN_PAIR_XY = 0x06,
+ BTN_PAIR_VIEW_MENU = 0x07,
+ BTN_PAIR_M1M2 = 0x08,
+ BTN_PAIR_TRIGGER_LR = 0x09,
+};
+
+struct button_map {
+ const struct btn_code_map *remap;
+ const struct btn_code_map *macro;
+};
+
+struct button_pair_map {
+ enum btn_pair_index pair_index;
+ struct button_map first;
+ struct button_map second;
+};
+
+/* Mappings the firmware is using. One set; a mode switch replaces it. */
+struct ally_button_mapping {
+ struct button_pair_map button_pairs[9];
};
static struct ally_prefs {
@@ -926,10 +1134,598 @@ static struct ally_prefs {
struct ally_turbo_config turbo;
struct ally_btn_sysfs_entry *button_entries;
+ struct ally_button_mapping button_mappings;
} ally_prefs = {
.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
};
+/**
+ * find_button_by_name() - Find a button code map by its name
+ * @name: name of the button code to look up
+ *
+ * Return: the matching button code map, or NULL if not found.
+ */
+static const struct btn_code_map *find_button_by_name(const char *name)
+{
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(ally_btn_codes); i++) {
+ if (!strcmp(ally_btn_codes[i].name, name))
+ return &ally_btn_codes[i];
+ }
+
+ return NULL;
+}
+
+static void ally_set_button_pair_mappings_default(struct ally_button_mapping *map)
+{
+ const struct btn_code_map *none_btn = find_button_by_name("NONE");
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(map->button_pairs); i++) {
+ map->button_pairs[i].pair_index = i + 1;
+ map->button_pairs[i].first.remap = none_btn;
+ map->button_pairs[i].first.macro = none_btn;
+ map->button_pairs[i].second.remap = none_btn;
+ map->button_pairs[i].second.macro = none_btn;
+ }
+}
+
+static void ally_set_default_gamepad_mapping(struct ally_button_mapping *map)
+{
+ ally_set_button_pair_mappings_default(map);
+
+ map->button_pairs[BTN_PAIR_AB - 1].first.remap =
+ find_button_by_name("BTN_SOUTH");
+ map->button_pairs[BTN_PAIR_AB - 1].second.remap =
+ find_button_by_name("BTN_EAST");
+
+ map->button_pairs[BTN_PAIR_XY - 1].first.remap =
+ find_button_by_name("BTN_NORTH");
+ map->button_pairs[BTN_PAIR_XY - 1].second.remap =
+ find_button_by_name("BTN_WEST");
+
+ map->button_pairs[BTN_PAIR_BUMPER_LR - 1].first.remap =
+ find_button_by_name("BTN_TL");
+ map->button_pairs[BTN_PAIR_BUMPER_LR - 1].second.remap =
+ find_button_by_name("BTN_TR");
+
+ map->button_pairs[BTN_PAIR_STICK_LR - 1].first.remap =
+ find_button_by_name("BTN_THUMBL");
+ map->button_pairs[BTN_PAIR_STICK_LR - 1].second.remap =
+ find_button_by_name("BTN_THUMBR");
+
+ map->button_pairs[BTN_PAIR_DPAD_UPDOWN - 1].first.remap =
+ find_button_by_name("ABS_HAT0Y_NEG");
+ map->button_pairs[BTN_PAIR_DPAD_UPDOWN - 1].second.remap =
+ find_button_by_name("ABS_HAT0Y_POS");
+
+ map->button_pairs[BTN_PAIR_DPAD_LEFTRIGHT - 1].first.remap =
+ find_button_by_name("ABS_HAT0X_NEG");
+ map->button_pairs[BTN_PAIR_DPAD_LEFTRIGHT - 1].second.remap =
+ find_button_by_name("ABS_HAT0X_POS");
+
+ map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].first.remap =
+ find_button_by_name("ABS_Z");
+ map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].second.remap =
+ find_button_by_name("ABS_RZ");
+
+ map->button_pairs[BTN_PAIR_VIEW_MENU - 1].first.remap =
+ find_button_by_name("BTN_SELECT");
+ map->button_pairs[BTN_PAIR_VIEW_MENU - 1].second.remap =
+ find_button_by_name("BTN_START");
+
+ map->button_pairs[BTN_PAIR_M1M2 - 1].first.remap =
+ find_button_by_name("FN_M2");
+ map->button_pairs[BTN_PAIR_M1M2 - 1].second.remap =
+ find_button_by_name("FN_M1");
+}
+
+static void ally_set_default_keyboard_mapping(struct ally_button_mapping *map)
+{
+ ally_set_button_pair_mappings_default(map);
+
+ /*
+ * Desktop mode defaults: keyboard and mouse mappings.
+ * Buttons not listed here (dpad, view, menu) remain NONE.
+ */
+
+ /* btn_a => KEY_SPACE, btn_b => KEY_E */
+ map->button_pairs[BTN_PAIR_AB - 1].first.remap =
+ find_button_by_name("KEY_SPACE");
+ map->button_pairs[BTN_PAIR_AB - 1].second.remap =
+ find_button_by_name("KEY_E");
+
+ /* btn_x => KEY_R, btn_y => KEY_F */
+ map->button_pairs[BTN_PAIR_XY - 1].first.remap =
+ find_button_by_name("KEY_R");
+ map->button_pairs[BTN_PAIR_XY - 1].second.remap =
+ find_button_by_name("KEY_F");
+
+ /* LB => wheel up, RB => wheel down */
+ map->button_pairs[BTN_PAIR_BUMPER_LR - 1].first.remap =
+ find_button_by_name("REL_WHEEL_HI_RES_UP");
+ map->button_pairs[BTN_PAIR_BUMPER_LR - 1].second.remap =
+ find_button_by_name("REL_WHEEL_HI_RES_DOWN");
+
+ /* left stick click => left click, right stick click => left shift */
+ map->button_pairs[BTN_PAIR_STICK_LR - 1].first.remap =
+ find_button_by_name("BTN_LEFT");
+ map->button_pairs[BTN_PAIR_STICK_LR - 1].second.remap =
+ find_button_by_name("KEY_LEFTSHIFT");
+
+ /* LT => left click, RT => right click */
+ map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].first.remap =
+ find_button_by_name("BTN_LEFT");
+ map->button_pairs[BTN_PAIR_TRIGGER_LR - 1].second.remap =
+ find_button_by_name("BTN_RIGHT");
+
+ /* M2 => FN_M2, M1 => FN_M1 */
+ map->button_pairs[BTN_PAIR_M1M2 - 1].first.remap =
+ find_button_by_name("FN_M2");
+ map->button_pairs[BTN_PAIR_M1M2 - 1].second.remap =
+ find_button_by_name("FN_M1");
+}
+
+/**
+ * ally_remap_code_valid() - Check if a remap target is valid for a button
+ * @btn: button to remap
+ * @gamepad_mode: gamepad mode to validate the remap against
+ * @code: remap target to check
+ * @is_macro: whether the target is assigned to the macro mapping
+ *
+ * M1 and M2 can be remapped to anything at any time.
+ * All other buttons may only be remapped to gamepad buttons (BTN_TYPE_PAD)
+ * when in gamepad mode, and to keyboard/mouse/media codes when in desktop
+ * (keyboard) mode. BTN_TYPE_NONE (unmapped) is always allowed.
+ * Macro mappings are executed by the MCU as input sequences on the virtual
+ * keyboard and are accepted regardless of the current gamepad mode.
+ *
+ * Return: true if the remap is valid, false otherwise.
+ */
+static bool ally_remap_code_valid(enum ally_button_id btn, u8 gamepad_mode,
+ const struct btn_code_map *code, bool is_macro)
+{
+ if (!code)
+ return false;
+
+ /* M1 and M2 can be remapped to everything */
+ if (btn == ALLY_BTN_M1 || btn == ALLY_BTN_M2)
+ return true;
+
+ /* NONE is always valid */
+ if (code->type == BTN_TYPE_NONE)
+ return true;
+
+ /* Macros are not restricted by the controller emulation mode */
+ if (is_macro)
+ return true;
+
+ if (gamepad_mode == ALLY_GAMEPAD_MODE_GAMEPAD)
+ return code->type == BTN_TYPE_PAD;
+
+ /* Desktop / keyboard mode: allow keyboard, mouse and media */
+ return code->type == BTN_TYPE_KB ||
+ code->type == BTN_TYPE_MOUSE ||
+ code->type == BTN_TYPE_MEDIA;
+}
+
+/**
+ * ally_encode_button_code() - Pack one button code into an MCU mapping slot
+ * @buf: command buffer to write
+ * @offset: byte offset of the slot in @buf
+ * @code: button code to encode
+ *
+ * The slot is %ASUS_ALLY_BTN_FUNCTIONS_LEN bytes. Byte 0 is the type. A pad
+ * value sits at byte 1, a keyboard value at byte 2, a media value at byte 3
+ * and a mouse value at byte 4.
+ */
+static void ally_encode_button_code(u8 *buf, unsigned int offset,
+ const struct btn_code_map *code)
+{
+ u8 bytes[ASUS_ALLY_BTN_FUNCTIONS_LEN] = {};
+
+ buf[offset] = code->type;
+ if (!code->type)
+ return;
+
+ bytes[0] = code->type;
+ switch (code->type) {
+ case BTN_TYPE_NONE:
+ break;
+ case BTN_TYPE_PAD:
+ bytes[1] = code->value;
+ break;
+ case BTN_TYPE_KB:
+ bytes[2] = code->value;
+ break;
+ case BTN_TYPE_MEDIA:
+ bytes[3] = code->value;
+ break;
+ case BTN_TYPE_MOUSE:
+ bytes[4] = code->value;
+ break;
+ }
+
+ memcpy(&buf[offset], bytes, sizeof(bytes));
+}
+
+/**
+ * ally_set_button_mapping() - Set the button mapping for a button pair
+ * @hdev: HID device
+ * @mapping: button pair mapping to send to the device
+ *
+ * Return: count of data transferred, negative if error.
+ */
+static int ally_set_button_mapping(struct hid_device *hdev,
+ struct button_pair_map *mapping)
+{
+ /*
+ * The MCU mapping block is four consecutive 11-byte entries
+ * starting at buf[5]: first remap 5-15, first macro 16-26,
+ * second remap 27-37, second macro 38-48. Each entry is one
+ * ally_encode_button_code() slot.
+ */
+ size_t buf_len;
+
+ if (!mapping || !mapping->first.remap || !mapping->first.macro ||
+ !mapping->second.remap || !mapping->second.macro)
+ return -EINVAL;
+
+ /*
+ * The mapping block spans buf[4]..buf[48]: give ally_alloc_cmd() its
+ * size so the fit check protects the fixed-offset writes below.
+ */
+ u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_MAPPING, NULL,
+ 1 + ASUS_ALLY_BTN_CODE_LEN,
+ &buf_len);
+ if (!buf)
+ return -ENOMEM;
+
+ /*
+ * This packet is slightly different from the other
+ * as before the packet length there is an extra byte
+ * which is the pair index.
+ */
+ buf[3] = mapping->pair_index;
+ buf[4] = ASUS_ALLY_BTN_CODE_LEN;
+
+ /* First remap, first macro, second remap, second macro. */
+ ally_encode_button_code(buf, 5, mapping->first.remap);
+ ally_encode_button_code(buf, 16, mapping->first.macro);
+ ally_encode_button_code(buf, 27, mapping->second.remap);
+ ally_encode_button_code(buf, 38, mapping->second.macro);
+
+ return ally_gamepad_send_packet(hdev, buf, buf_len);
+}
+
+/**
+ * ally_apply_button_mappings() - Push the recorded mappings of the current
+ * gamepad mode to the controller
+ * @hdev: HID device
+ *
+ * An MCU reset restores the firmware default mappings: push the recorded
+ * ones again so that the hardware keeps matching what userspace
+ * configured. The caller holds ally_prefs.lock, as the sysfs stores that
+ * update a single pair do.
+ */
+static void ally_apply_button_mappings(struct hid_device *hdev)
+{
+ struct ally_button_mapping *mode_map;
+ int i, ret;
+
+ mode_map = &ally_prefs.button_mappings;
+
+ for (i = 0; i < ARRAY_SIZE(mode_map->button_pairs); i++) {
+ mode_map->button_pairs[i].pair_index = i + 1;
+
+ ret = ally_set_button_mapping(hdev,
+ &mode_map->button_pairs[i]);
+ if (ret < 0) {
+ hid_warn(hdev,
+ "Failed to restore button mappings: %d\n",
+ ret);
+ return;
+ }
+ }
+}
+
+/**
+ * get_button_pair_info() - Get the pair index and position of a button
+ * @button_id: button to look up
+ * @pair_idx: output pointer for the pair index of the button
+ * @is_first: output pointer for the position of the button within the pair
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int get_button_pair_info(enum ally_button_id button_id,
+ enum btn_pair_index *pair_idx,
+ bool *is_first)
+{
+ switch (button_id) {
+ case ALLY_BTN_DU:
+ *pair_idx = BTN_PAIR_DPAD_UPDOWN;
+ *is_first = true;
+ break;
+ case ALLY_BTN_DD:
+ *pair_idx = BTN_PAIR_DPAD_UPDOWN;
+ *is_first = false;
+ break;
+ case ALLY_BTN_DL:
+ *pair_idx = BTN_PAIR_DPAD_LEFTRIGHT;
+ *is_first = true;
+ break;
+ case ALLY_BTN_DR:
+ *pair_idx = BTN_PAIR_DPAD_LEFTRIGHT;
+ *is_first = false;
+ break;
+ case ALLY_BTN_J0B:
+ *pair_idx = BTN_PAIR_STICK_LR;
+ *is_first = true;
+ break;
+ case ALLY_BTN_J1B:
+ *pair_idx = BTN_PAIR_STICK_LR;
+ *is_first = false;
+ break;
+ case ALLY_BTN_LB:
+ *pair_idx = BTN_PAIR_BUMPER_LR;
+ *is_first = true;
+ break;
+ case ALLY_BTN_RB:
+ *pair_idx = BTN_PAIR_BUMPER_LR;
+ *is_first = false;
+ break;
+ case ALLY_BTN_A:
+ *pair_idx = BTN_PAIR_AB;
+ *is_first = true;
+ break;
+ case ALLY_BTN_B:
+ *pair_idx = BTN_PAIR_AB;
+ *is_first = false;
+ break;
+ case ALLY_BTN_X:
+ *pair_idx = BTN_PAIR_XY;
+ *is_first = true;
+ break;
+ case ALLY_BTN_Y:
+ *pair_idx = BTN_PAIR_XY;
+ *is_first = false;
+ break;
+ case ALLY_BTN_VIEW:
+ *pair_idx = BTN_PAIR_VIEW_MENU;
+ *is_first = true;
+ break;
+ case ALLY_BTN_MENU:
+ *pair_idx = BTN_PAIR_VIEW_MENU;
+ *is_first = false;
+ break;
+ case ALLY_BTN_M1:
+ *pair_idx = BTN_PAIR_M1M2;
+ *is_first = false;
+ break;
+ case ALLY_BTN_M2:
+ *pair_idx = BTN_PAIR_M1M2;
+ *is_first = true;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static ssize_t button_remap_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct button_remap_attr *btn_attr = to_button_remap_attr(attr);
+ const struct btn_code_map *code;
+ enum ally_button_id button_id = btn_attr->button_id;
+ enum btn_pair_index pair_idx;
+ struct button_pair_map *pair;
+ struct button_map *btn_map;
+ bool is_first;
+ int ret;
+
+ ret = get_button_pair_info(button_id, &pair_idx, &is_first);
+ if (ret < 0)
+ return ret;
+
+ guard(mutex)(&ally_prefs.lock);
+
+ pair = &ally_prefs.button_mappings.button_pairs[pair_idx - 1];
+ btn_map = is_first ? &pair->first : &pair->second;
+ code = btn_attr->is_macro ? btn_map->macro : btn_map->remap;
+
+ /*
+ * The mappings are never NULL: the defaults, the mode-switch
+ * re-defaults and the store rollback all guarantee it. A miss
+ * means a broken default table, so fail loudly instead of
+ * dereferencing.
+ */
+ if (!code)
+ return -EIO;
+
+ return sysfs_emit(buf, "%s\n", code->name);
+}
+
+static ssize_t button_remap_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hid_device *hdev = to_hid_device(dev);
+ struct button_remap_attr *btn_attr = to_button_remap_attr(attr);
+ enum ally_button_id button_id = btn_attr->button_id;
+ struct ally_button_mapping *mode_map;
+ const struct btn_code_map *code;
+ const struct btn_code_map *old_code;
+ enum btn_pair_index pair_idx;
+ struct button_pair_map *pair;
+ struct button_map *btn_map;
+ char btn_name[32];
+ bool is_first;
+ int ret, i;
+
+ if (sscanf(buf, "%31s", btn_name) != 1)
+ return -EINVAL;
+
+ code = find_button_by_name(btn_name);
+ if (!code)
+ return -EINVAL;
+
+ ret = get_button_pair_info(button_id, &pair_idx, &is_first);
+ if (ret < 0)
+ return ret;
+
+ guard(mutex)(&ally_prefs.lock);
+
+ /*
+ * Validate the remap target while holding the lock: the
+ * gamepad mode can change concurrently, and the check must
+ * be done against the very mode the mapping is written to.
+ */
+ if (!ally_remap_code_valid(button_id, ally_prefs.gamepad_mode, code,
+ btn_attr->is_macro))
+ return -EINVAL;
+
+ /* Access the mapping for current gamepad mode */
+ pair = &ally_prefs.button_mappings.button_pairs[pair_idx - 1];
+ btn_map = is_first ? &pair->first : &pair->second;
+
+ if (btn_attr->is_macro) {
+ old_code = btn_map->macro;
+ btn_map->macro = code;
+ } else {
+ old_code = btn_map->remap;
+ btn_map->remap = code;
+ }
+
+ /* Update pair index */
+ pair->pair_index = pair_idx;
+
+ /*
+ * Send mapping to device with the caveat that first
+ * generation devices require individual button pair updates.
+ */
+ if (ally_prefs.is_ally_x) {
+ ret = ally_set_button_mapping(hdev, pair);
+ if (ret < 0)
+ goto err_restore;
+ } else {
+ mode_map = &ally_prefs.button_mappings;
+ for (i = 0; i < ARRAY_SIZE(mode_map->button_pairs); i++) {
+ mode_map->button_pairs[i].pair_index = i + 1;
+ ret = ally_set_button_mapping(hdev,
+ &mode_map->button_pairs[i]);
+ if (ret < 0) {
+ /*
+ * Pairs are pushed one at a time. This pair
+ * already reached the hardware when a later
+ * pair is the one that failed.
+ */
+ if (i > pair_idx - 1)
+ goto err_restore_hw;
+ goto err_restore;
+ }
+ }
+ }
+ sysfs_notify(&dev->kobj, ally_button_names[btn_attr->button_id],
+ btn_attr->is_macro ? "macro" : "remap");
+ return count;
+
+err_restore_hw:
+ if (btn_attr->is_macro)
+ btn_map->macro = old_code;
+ else
+ btn_map->remap = old_code;
+
+ /* Try to recover the mapping presented to the user. */
+ ally_set_button_mapping(hdev, pair);
+ goto err_out;
+
+err_restore:
+ if (btn_attr->is_macro)
+ btn_map->macro = old_code;
+ else
+ btn_map->remap = old_code;
+
+err_out:
+ hid_err(hdev, "Failed to set button mapping: %d\n", ret);
+ return ret;
+}
+
+/**
+ * button_remap_attr_create() - Create a button remap attribute
+ * @button_id: button the attribute is for
+ * @is_macro: whether the attribute controls the macro mapping
+ *
+ * Return: the newly created attribute, or NULL on allocation failure.
+ */
+static struct button_remap_attr *button_remap_attr_create(enum ally_button_id button_id,
+ bool is_macro)
+{
+ struct button_remap_attr *const attr = kzalloc_obj(*attr);
+
+ if (!attr)
+ return NULL;
+
+ attr->button_id = button_id;
+ attr->is_macro = is_macro;
+ sysfs_attr_init(&attr->dev_attr.attr);
+ attr->dev_attr.attr.name = is_macro ? "macro" : "remap";
+ attr->dev_attr.attr.mode = 0644;
+ attr->dev_attr.show = button_remap_show;
+ attr->dev_attr.store = button_remap_store;
+
+ return attr;
+}
+
+/**
+ * btn_remap_available_show() - Show the list of valid remap targets for a button
+ * @dev: device the attribute belongs to
+ * @attr: attribute being read
+ * @buf: buffer to write the list into
+ *
+ * M1/M2 list everything; other buttons list only the codes valid for the
+ * active mode (gamepad buttons in gamepad mode, keyboard / mouse / media
+ * in desktop mode).
+ *
+ * Return: number of characters written.
+ */
+static ssize_t btn_remap_available_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct ally_btn_sysfs_entry *const entry = container_of(attr,
+ struct ally_btn_sysfs_entry, attr_remap_available);
+ enum ally_button_id btn = entry->btn;
+ u8 mode;
+ ssize_t len = 0;
+ int i;
+
+ scoped_guard(mutex, &ally_prefs.lock)
+ mode = ally_prefs.gamepad_mode;
+
+ for (i = 0; i < ARRAY_SIZE(ally_btn_codes); i++) {
+ if (!ally_remap_code_valid(btn, mode, &ally_btn_codes[i], false))
+ continue;
+ len += sysfs_emit_at(buf, len, "%s ", ally_btn_codes[i].name);
+ }
+
+ if (len > 0)
+ buf[len - 1] = '\n';
+
+ return len;
+}
+
+static void ally_mapping_defaults(void)
+{
+ guard(mutex)(&ally_prefs.lock);
+
+ if (ally_prefs.initialized)
+ return;
+
+ ally_set_default_gamepad_mapping(&ally_prefs.button_mappings);
+}
+
/**
* ally_check_capability() - Check if a specific capability is supported
* @hdev: HID device to query
@@ -1532,12 +2328,35 @@ static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *a
/* The text index maps to the byte the MCU accepts. */
mode_byte = ally_gamepad_mode_value[mode];
- scoped_guard(mutex, &ally_prefs.lock) {
- ret = ally_set_gamepad_mode(hdev, mode_byte);
- if (ret < 0)
- return ret;
+ guard(mutex)(&ally_prefs.lock);
+
+ ret = ally_set_gamepad_mode(hdev, mode_byte);
+ if (ret < 0)
+ return ret;
- ally_prefs.gamepad_mode = mode_byte;
+ ally_prefs.gamepad_mode = mode_byte;
+
+ /*
+ * The MCU restores its own defaults for the new mode. Refresh the
+ * cache so a later read reports what the firmware is using, then
+ * wake anyone polling the remap, macro, and remap_available files.
+ */
+ if (mode_byte == ALLY_GAMEPAD_MODE_GAMEPAD)
+ ally_set_default_gamepad_mapping(&ally_prefs.button_mappings);
+ else if (mode_byte == ALLY_GAMEPAD_MODE_KEYBOARD)
+ ally_set_default_keyboard_mapping(&ally_prefs.button_mappings);
+
+ if (ally_prefs.button_entries) {
+ int i;
+
+ for (i = 0; i < ALLY_BTN_MAX; i++) {
+ sysfs_notify(&dev->kobj,
+ ally_prefs.button_entries[i].group.name, "remap");
+ sysfs_notify(&dev->kobj,
+ ally_prefs.button_entries[i].group.name, "macro");
+ sysfs_notify(&dev->kobj,
+ ally_prefs.button_entries[i].group.name, "remap_available");
+ }
}
hid_dbg(hdev, "Set gamepad mode to %s\n", ally_gamepad_mode_text[mode]);
@@ -2429,11 +3248,9 @@ static void ally_btn_turbo_init_attrs(struct ally_btn_sysfs_entry *entry)
static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_device *hdev)
{
+ const size_t entries_attr_index = ally_prefs.turbo_support ? 4 : 0;
int i, ret;
- if (!ally_prefs.turbo_support)
- return NULL;
-
struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
if (!entries)
return ERR_PTR(-ENOMEM);
@@ -2441,13 +3258,33 @@ static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_dev
for (i = 0; i < ALLY_BTN_MAX; i++) {
entries[i].btn = i;
- if (!ally_btn_param(i)) {
- hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
- ret = -EINVAL;
+ if (ally_prefs.turbo_support) {
+ if (!ally_btn_param(i)) {
+ hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
+ ret = -EINVAL;
+ goto ally_btn_err_cleanup;
+ }
+
+ ally_btn_turbo_init_attrs(&entries[i]);
+ }
+
+ entries[i].remap_attr = button_remap_attr_create(i, false);
+ entries[i].macro_attr = button_remap_attr_create(i, true);
+ if (!entries[i].remap_attr || !entries[i].macro_attr) {
+ ret = -ENOMEM;
goto ally_btn_err_cleanup;
}
- ally_btn_turbo_init_attrs(&entries[i]);
+ sysfs_attr_init(&entries[i].attr_remap_available.attr);
+ entries[i].attr_remap_available.attr.name = "remap_available";
+ entries[i].attr_remap_available.attr.mode = 0444;
+ entries[i].attr_remap_available.show = btn_remap_available_show;
+
+ entries[i].attrs[entries_attr_index + 0] = &entries[i].remap_attr->dev_attr.attr;
+ entries[i].attrs[entries_attr_index + 1] = &entries[i].macro_attr->dev_attr.attr;
+ entries[i].attrs[entries_attr_index + 2] = &entries[i].attr_remap_available.attr;
+ entries[i].attrs[entries_attr_index + 3] = NULL;
+
entries[i].group.name = ally_button_names[i];
entries[i].group.attrs = entries[i].attrs;
@@ -2465,6 +3302,11 @@ static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_dev
while (i--)
sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
+ for (i = 0; i < ALLY_BTN_MAX; i++) {
+ kfree(entries[i].remap_attr);
+ kfree(entries[i].macro_attr);
+ }
+
return ERR_PTR(ret);
}
@@ -2477,11 +3319,19 @@ static void ally_remove_button_attributes(struct hid_device *hdev)
if (!entries)
return;
- for (i = 0; i < ALLY_BTN_MAX; i++)
+ /*
+ * Unpublish first. A mode-store notify holds the prefs lock and must
+ * see either the live array or NULL.
+ */
+ ally_prefs.button_entries = NULL;
+
+ for (i = 0; i < ALLY_BTN_MAX; i++) {
sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
+ kfree(entries[i].remap_attr);
+ kfree(entries[i].macro_attr);
+ }
kfree(entries);
- ally_prefs.button_entries = NULL;
}
static int ally_prefs_setup(struct hid_device *hdev)
@@ -2497,6 +3347,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
ally_joystick_defaults();
ally_trigger_defaults();
ally_curve_defaults();
+ ally_mapping_defaults();
guard(mutex)(&ally_prefs.lock);
@@ -2534,13 +3385,11 @@ static int ally_prefs_setup(struct hid_device *hdev)
}
}
- if (ally_prefs.turbo_support) {
- entries = ally_create_button_attributes(hdev);
- if (IS_ERR(entries)) {
- ret = PTR_ERR(entries);
- hid_err(hdev, "Failed to create button attributes: %d\n", ret);
- goto err_cal_groups;
- }
+ entries = ally_create_button_attributes(hdev);
+ if (IS_ERR(entries)) {
+ ret = PTR_ERR(entries);
+ hid_err(hdev, "Failed to create button attributes: %d\n", ret);
+ goto err_cal_groups;
}
ally_prefs.button_entries = entries;
@@ -2934,6 +3783,9 @@ static int hid_asus_ally_init(struct hid_device *hdev)
hid_warn(hdev, "Failed to restore turbo parameters: %d\n", ret);
}
+ if (ally_prefs.initialized && ally_prefs.button_entries)
+ ally_apply_button_mappings(hdev);
+
return 0;
}
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 13/17] HID: asus: identify XG Mobile stations and bind the LED to the reported model
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (11 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 12/17] HID: asus: add support for btn remapping Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 14/17] HID: asus: expose XG Mobile identity and firmware information Denis Benato
` (3 subsequent siblings)
16 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The XG Mobile LED interface is currently bound to two USB product IDs
(0x1970, 0x1a9a). The stations identify themselves through the vendor
report 0x5E: after the usual "ASUS Tech.Inc." unlock, the 0xCE 0x01
query answers with a NUL terminated string of the form
MB:<mainboard>;SN:<serial>;PRO:<model>;PNUM:<part number>;
Run this query at probe time to initialize the LED interface only on
supported models (GC31, GC32 and GC33). The LED name is the model
followed by the bus identity (asus:xgm-<model>-<uniq>:led).
The identity query is tried three times, one second apart, so one
missed transfer does not leave the LED unregistered.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 234 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 232 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index b94cb8c50b28..215628393765 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -26,6 +26,7 @@
#include <linux/compiler_attributes.h>
#include <linux/container_of.h>
#include <linux/ctype.h>
+#include <linux/delay.h>
#include <linux/dmi.h>
#include <linux/err.h>
#include <linux/hid.h>
@@ -70,6 +71,29 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define FEATURE_KBD_LED_REPORT_ID1 0x5d
#define FEATURE_KBD_LED_REPORT_ID2 0x5e
+/* XG Mobile station protocol on FEATURE_KBD_LED_REPORT_ID2. */
+#define ASUS_XGM_CMD_QUERY 0xce
+#define ASUS_XGM_QUERY_IDENTITY 0x01
+#define ASUS_XGM_INFO_LEN 32
+
+/* Identity string fields, each "TAG:value;" inside the query answer. */
+#define ASUS_XGM_TAG_MAINBOARD "MB:"
+#define ASUS_XGM_TAG_SERIAL "SN:"
+#define ASUS_XGM_TAG_MODEL "PRO:"
+#define ASUS_XGM_TAG_PART "PNUM:"
+
+/*
+ * Every station answer echoes the report id, the opcode and the
+ * subcommand, then the payload.
+ */
+enum {
+ ASUS_XGM_RESP_REPORT_ID,
+ ASUS_XGM_RESP_OPCODE,
+ ASUS_XGM_RESP_SUBCMD,
+ ASUS_XGM_RESP_PAYLOAD,
+ ASUS_XGM_RESP_HDR_LEN = ASUS_XGM_RESP_PAYLOAD,
+};
+
#define ROG_ALLY_X_MIN_MCU 313
#define ROG_ALLY_MIN_MCU 319
@@ -207,6 +231,13 @@ struct asus_xgm_led {
struct hid_device *hdev;
};
+struct asus_xgm_info {
+ char mainboard[ASUS_XGM_INFO_LEN];
+ char serial[ASUS_XGM_INFO_LEN];
+ char model[ASUS_XGM_INFO_LEN];
+ char part_number[ASUS_XGM_INFO_LEN];
+};
+
struct asus_touchpad_info {
int max_x;
int max_y;
@@ -271,6 +302,7 @@ struct asus_drvdata {
struct asus_hid_listener listener;
bool fn_lock;
struct asus_xgm_led *xgm_led;
+ struct asus_xgm_info *xgm_info;
};
static int asus_report_battery(struct asus_drvdata *, u8 *, int);
@@ -4875,6 +4907,169 @@ static int asus_battery_probe(struct hid_device *hdev)
return ret;
}
+/**
+ * asus_xgm_query() - Run a query command against an XG Mobile station
+ * @hdev: HID device
+ * @subcmd: query sub-command
+ * @buf: buffer to store the answer in
+ * @buf_size: size of @buf
+ *
+ * Windows re-sends the "ASUS Tech.Inc." unlock before every command
+ * burst, so do the same.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_xgm_query(struct hid_device *hdev, u8 subcmd,
+ u8 *buf, size_t buf_size)
+{
+ const u8 unlock[] = {
+ FEATURE_KBD_LED_REPORT_ID2,
+ 'A', 'S', 'U', 'S', ' ', 'T', 'e', 'c', 'h', '.', 'I', 'n', 'c', '.'
+ };
+ const u8 cmd[] = { FEATURE_KBD_LED_REPORT_ID2, ASUS_XGM_CMD_QUERY, subcmd };
+ int ret;
+
+ ret = asus_kbd_set_report(hdev, unlock, sizeof(unlock));
+ if (ret < 0)
+ return ret;
+
+ ret = asus_kbd_set_report(hdev, cmd, sizeof(cmd));
+ if (ret < 0)
+ return ret;
+
+ return asus_kbd_get_report(hdev, FEATURE_KBD_LED_REPORT_ID2, buf, buf_size);
+}
+
+/**
+ * asus_xgm_info_assign() - Copy an identity field up to its ';'
+ * terminator
+ * @dst: destination buffer
+ * @dst_len: size of @dst, including the trailing NUL
+ * @src: start of the field inside the identity string
+ * @src_len: number of bytes available at @src
+ *
+ * Copies up to @dst_len - 1 bytes from @src, stopping at the first ';'
+ * field separator, and NUL terminates @dst.
+ */
+static void asus_xgm_info_assign(char *dst, size_t dst_len,
+ const char *src, size_t src_len)
+{
+ const char *sep = strnchr(src, src_len, ';');
+ size_t n = sep ? (size_t)(sep - src) : src_len;
+
+ if (n >= dst_len)
+ n = dst_len - 1;
+
+ memcpy(dst, src, n);
+ dst[n] = '\0';
+}
+
+/**
+ * asus_xgm_take_field() - Copy one tagged field out of the identity string
+ * @dst: destination buffer
+ * @dst_len: size of @dst, including the trailing NUL
+ * @identity: identity string returned by the station
+ * @identity_len: number of bytes available at @identity
+ * @tag: field tag, including the colon
+ *
+ * A tag that is absent leaves @dst untouched.
+ */
+static void asus_xgm_take_field(char *dst, size_t dst_len,
+ const char *identity, size_t identity_len,
+ const char *tag)
+{
+ size_t tag_len = strlen(tag);
+ const char *found = strnstr(identity, tag, identity_len);
+ size_t value_off;
+
+ if (!found)
+ return;
+
+ value_off = (found - identity) + tag_len;
+ if (value_off > identity_len)
+ return;
+
+ asus_xgm_info_assign(dst, dst_len, identity + value_off,
+ identity_len - value_off);
+}
+
+/**
+ * asus_xgm_get_identity() - Query the identity of an XG Mobile station
+ * @hdev: HID device
+ * @info: identity structure to fill in
+ *
+ * The identity query answers with a NUL terminated string of the form
+ * "MB:<mainboard>;SN:<serial>;PRO:<model>;PNUM:<part number>;", which is
+ * split into the fields of @info. The model field is mandatory: a station
+ * that does not report one is treated as absent.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_xgm_get_identity(struct hid_device *hdev,
+ struct asus_xgm_info *info)
+{
+ size_t len = asus_report_len(hdev, FEATURE_KBD_LED_REPORT_ID2);
+ const char *payload;
+ size_t payload_len;
+ int ret;
+
+ if (!len)
+ return -EINVAL;
+
+ u8 *buf __free(kfree) = kzalloc(len, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = asus_xgm_query(hdev, ASUS_XGM_QUERY_IDENTITY, buf, len);
+ if (ret < 0)
+ return ret;
+
+ if (len <= ASUS_XGM_RESP_HDR_LEN ||
+ buf[ASUS_XGM_RESP_OPCODE] != ASUS_XGM_CMD_QUERY ||
+ buf[ASUS_XGM_RESP_SUBCMD] != ASUS_XGM_QUERY_IDENTITY)
+ return -ENODEV;
+
+ payload = (const char *)buf + ASUS_XGM_RESP_PAYLOAD;
+ payload_len = strnlen(payload, len - ASUS_XGM_RESP_HDR_LEN);
+ if (!payload_len)
+ return -ENODEV;
+
+ asus_xgm_take_field(info->mainboard, sizeof(info->mainboard),
+ payload, payload_len, ASUS_XGM_TAG_MAINBOARD);
+ asus_xgm_take_field(info->serial, sizeof(info->serial),
+ payload, payload_len, ASUS_XGM_TAG_SERIAL);
+ asus_xgm_take_field(info->model, sizeof(info->model),
+ payload, payload_len, ASUS_XGM_TAG_MODEL);
+ asus_xgm_take_field(info->part_number, sizeof(info->part_number),
+ payload, payload_len, ASUS_XGM_TAG_PART);
+
+ if (!info->model[0])
+ return -ENODEV;
+
+ return 0;
+}
+
+static const char * const asus_xgm_models[] = { "GC31", "GC32", "GC33" };
+
+/**
+ * asus_xgm_is_supported_model() - Check that a station is supported
+ * @info: identity of the station
+ *
+ * The supported families are the GC31, GC32 and GC33 stations.
+ *
+ * Return: true if the reported model belongs to one of the supported
+ * families, false otherwise.
+ */
+static bool asus_xgm_is_supported_model(const struct asus_xgm_info *info)
+{
+ for (size_t i = 0; i < ARRAY_SIZE(asus_xgm_models); i++)
+ if (strncmp(info->model, asus_xgm_models[i],
+ strlen(asus_xgm_models[i])) == 0)
+ return true;
+
+ return false;
+}
+
static int asus_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value)
{
const u8 buf[] = { FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00 };
@@ -5160,14 +5355,49 @@ static void asus_initialize_reports(struct hid_device *hdev)
static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
{
+ struct asus_xgm_info *info;
const char *name;
- int ret;
+ int attempt, ret;
+
+ info = devm_kzalloc(&hdev->dev, sizeof(*info), GFP_KERNEL);
+ if (!info)
+ return -ENOMEM;
+
+ /*
+ * The station reports its identity through the query protocol: the
+ * reported model decides whether the LED interface is registered.
+ * The LED name is that model, then the bus identity. One missed
+ * transfer used to leave that LED unregistered, so try three times
+ * and wait a second between attempts.
+ */
+ for (attempt = 0; attempt < 3; attempt++) {
+ ret = asus_xgm_get_identity(hdev, info);
+ if (!ret)
+ break;
+ if (attempt < 2)
+ ssleep(1);
+ }
+ if (ret < 0) {
+ hid_warn(hdev, "Failed to query XG Mobile identity: %d\n", ret);
+ return 0;
+ }
+
+ hid_info(hdev, "XG Mobile: model %s, serial %s\n",
+ info->model, info->serial);
+
+ if (!asus_xgm_is_supported_model(info)) {
+ hid_warn(hdev, "Unsupported XG Mobile model %s\n", info->model);
+ return 0;
+ }
+
+ drvdata->xgm_info = info;
drvdata->xgm_led = devm_kzalloc(&hdev->dev, sizeof(*drvdata->xgm_led), GFP_KERNEL);
if (drvdata->xgm_led == NULL)
return -ENOMEM;
- name = devm_kasprintf(&hdev->dev, GFP_KERNEL, "asus:xgm-%s:led",
+ name = devm_kasprintf(&hdev->dev, GFP_KERNEL, "asus:xgm-%s-%s:led",
+ info->model,
strlen(hdev->uniq) ? hdev->uniq : dev_name(&hdev->dev));
if (name == NULL) {
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 14/17] HID: asus: expose XG Mobile identity and firmware information
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (12 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 13/17] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 15/17] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
` (2 subsequent siblings)
16 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The identity query also reports the mainboard part number, the serial
number and the part number of the station, and the 0xCE 0x03 query
answers with an 8-byte binary blob followed by "tag, length, data"
fields where GEN holds the station generation and VER the firmware
version.
Collect this information at probe time and expose it read-only on the
HID device:
/sys/bus/hid/devices/<bus>:<addr>.<intf>/xgm_model (PRO)
.../xgm_serial (SN)
.../xgm_mainboard (MB)
.../xgm_part_number (PNUM)
.../xgm_firmware_version (VER)
.../xgm_generation (GEN)
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 188 ++++++++++++++++++++++++++++++++++++++++-
1 file changed, 186 insertions(+), 2 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 215628393765..691303b1a13f 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -74,6 +74,7 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
/* XG Mobile station protocol on FEATURE_KBD_LED_REPORT_ID2. */
#define ASUS_XGM_CMD_QUERY 0xce
#define ASUS_XGM_QUERY_IDENTITY 0x01
+#define ASUS_XGM_QUERY_VERSION 0x03
#define ASUS_XGM_INFO_LEN 32
/* Identity string fields, each "TAG:value;" inside the query answer. */
@@ -81,6 +82,12 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_XGM_TAG_SERIAL "SN:"
#define ASUS_XGM_TAG_MODEL "PRO:"
#define ASUS_XGM_TAG_PART "PNUM:"
+#define ASUS_XGM_TAG_GENERATION "GEN"
+#define ASUS_XGM_TAG_FIRMWARE "VER"
+
+/* A version field is a 3-character tag, one length byte, then the data. */
+#define ASUS_XGM_TLV_TAG_LEN 3
+#define ASUS_XGM_TLV_HDR_LEN (ASUS_XGM_TLV_TAG_LEN + 1)
/*
* Every station answer echoes the report id, the opcode and the
@@ -236,6 +243,8 @@ struct asus_xgm_info {
char serial[ASUS_XGM_INFO_LEN];
char model[ASUS_XGM_INFO_LEN];
char part_number[ASUS_XGM_INFO_LEN];
+ char firmware_version[ASUS_XGM_INFO_LEN];
+ char generation[ASUS_XGM_INFO_LEN];
};
struct asus_touchpad_info {
@@ -5070,6 +5079,153 @@ static bool asus_xgm_is_supported_model(const struct asus_xgm_info *info)
return false;
}
+/**
+ * asus_xgm_tlv_str() - Extract a "tag, length, data" field
+ * @dst: destination buffer
+ * @dst_len: size of @dst, including the trailing NUL
+ * @buf: answer of a version query
+ * @len: size of @buf
+ * @tag: three-character tag to look for
+ *
+ * Return: 0 on success, -ENOENT if @tag is missing or its data does not
+ * fit in @dst.
+ */
+static int asus_xgm_tlv_str(char *dst, size_t dst_len, const u8 *buf,
+ size_t len, const char *tag)
+{
+ const u8 *p = buf + ASUS_XGM_RESP_PAYLOAD;
+ const u8 *end = buf + len;
+ u8 n;
+
+ /*
+ * The payload starts with a binary prefix and then the fields.
+ * The station pads between fields with 0xff, so the tag is sought
+ * at every byte rather than walked as a packed list.
+ */
+ while (p + ASUS_XGM_TLV_HDR_LEN <= end) {
+ if (!memcmp(p, tag, ASUS_XGM_TLV_TAG_LEN)) {
+ n = p[ASUS_XGM_TLV_TAG_LEN];
+
+ if (n >= dst_len ||
+ end - (p + ASUS_XGM_TLV_HDR_LEN) < n)
+ return -ENOENT;
+
+ memcpy(dst, p + ASUS_XGM_TLV_HDR_LEN, n);
+ dst[n] = '\0';
+ return 0;
+ }
+
+ p++;
+ }
+
+ return -ENOENT;
+}
+
+/**
+ * asus_xgm_get_version() - Query the firmware version of a station
+ * @hdev: HID device
+ * @info: identity structure whose generation and firmware_version fields
+ * get filled in
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+static int asus_xgm_get_version(struct hid_device *hdev,
+ struct asus_xgm_info *info)
+{
+ size_t len = asus_report_len(hdev, FEATURE_KBD_LED_REPORT_ID2);
+ int ret;
+
+ if (!len)
+ return -EINVAL;
+
+ u8 *buf __free(kfree) = kzalloc(len, GFP_KERNEL);
+ if (!buf)
+ return -ENOMEM;
+
+ ret = asus_xgm_query(hdev, ASUS_XGM_QUERY_VERSION, buf, len);
+ if (ret < 0)
+ return ret;
+
+ if (len <= ASUS_XGM_RESP_HDR_LEN ||
+ buf[ASUS_XGM_RESP_OPCODE] != ASUS_XGM_CMD_QUERY ||
+ buf[ASUS_XGM_RESP_SUBCMD] != ASUS_XGM_QUERY_VERSION)
+ return -ENODEV;
+
+ asus_xgm_tlv_str(info->generation, sizeof(info->generation),
+ buf, len, ASUS_XGM_TAG_GENERATION);
+ asus_xgm_tlv_str(info->firmware_version, sizeof(info->firmware_version),
+ buf, len, ASUS_XGM_TAG_FIRMWARE);
+
+ return 0;
+}
+
+/**
+ * asus_xgm_drvdata() - Get the drvdata behind an XG Mobile attribute
+ * @dev: HID device the attribute is attached to
+ *
+ * Return: the asus_drvdata of @dev.
+ */
+static struct asus_drvdata *asus_xgm_drvdata(struct device *dev)
+{
+ return hid_get_drvdata(to_hid_device(dev));
+}
+
+static ssize_t xgm_model_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->model);
+}
+static DEVICE_ATTR_RO(xgm_model);
+
+static ssize_t xgm_serial_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->serial);
+}
+static DEVICE_ATTR_RO(xgm_serial);
+
+static ssize_t xgm_mainboard_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->mainboard);
+}
+static DEVICE_ATTR_RO(xgm_mainboard);
+
+static ssize_t xgm_part_number_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->part_number);
+}
+static DEVICE_ATTR_RO(xgm_part_number);
+
+static ssize_t xgm_firmware_version_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->firmware_version);
+}
+static DEVICE_ATTR_RO(xgm_firmware_version);
+
+static ssize_t xgm_generation_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%s\n", asus_xgm_drvdata(dev)->xgm_info->generation);
+}
+static DEVICE_ATTR_RO(xgm_generation);
+
+static struct attribute *asus_xgm_attrs[] = {
+ &dev_attr_xgm_model.attr,
+ &dev_attr_xgm_serial.attr,
+ &dev_attr_xgm_mainboard.attr,
+ &dev_attr_xgm_part_number.attr,
+ &dev_attr_xgm_firmware_version.attr,
+ &dev_attr_xgm_generation.attr,
+ NULL
+};
+
+static const struct attribute_group asus_xgm_attr_group = {
+ .attrs = asus_xgm_attrs,
+};
+
static int asus_xgm_led_set(struct led_classdev *led_cdev, enum led_brightness value)
{
const u8 buf[] = { FEATURE_KBD_LED_REPORT_ID2, 0xC5, (value) ? 0x50 : 0x00 };
@@ -5392,9 +5548,29 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
drvdata->xgm_info = info;
+ ret = asus_xgm_get_version(hdev, info);
+ if (ret < 0)
+ hid_warn(hdev, "Failed to query XG Mobile version: %d\n", ret);
+ else
+ hid_info(hdev, "XG Mobile: firmware %s, generation %s\n",
+ info->firmware_version, info->generation);
+
+ /*
+ * The attribute group is not devm-managed: a creation failure is
+ * fatal and unwound through asus_xgm_init_err, while on the unbind
+ * path the removal is gated on xgm_info exactly like the creation.
+ */
+ ret = sysfs_create_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+ if (ret) {
+ hid_err(hdev, "Failed to create XG Mobile attributes: %d\n", ret);
+ goto asus_xgm_init_err;
+ }
+
drvdata->xgm_led = devm_kzalloc(&hdev->dev, sizeof(*drvdata->xgm_led), GFP_KERNEL);
- if (drvdata->xgm_led == NULL)
- return -ENOMEM;
+ if (!drvdata->xgm_led) {
+ ret = -ENOMEM;
+ goto asus_xgm_init_err;
+ }
name = devm_kasprintf(&hdev->dev, GFP_KERNEL, "asus:xgm-%s-%s:led",
info->model,
@@ -5427,6 +5603,8 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
return 0;
asus_xgm_init_err:
+ sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+ drvdata->xgm_info = NULL;
drvdata->xgm_led = NULL;
return ret;
}
@@ -5643,6 +5821,9 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
+ if (drvdata->xgm_info)
+ sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
@@ -5667,6 +5848,9 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
+ if (drvdata->xgm_info)
+ sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 15/17] HID: asus: document ROG XG Mobile sysfs attributes
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (13 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 14/17] HID: asus: expose XG Mobile identity and firmware information Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 16/17] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
2026-10-31 17:41 ` [PATCH v12 17/17] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato
16 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The driver publishes the station's model, serial, mainboard identifier,
part number, firmware version and hardware generation as read-only
sysfs files. Document each file: when it is created, what a read
returns, and when that read is an empty line.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
.../ABI/testing/sysfs-driver-hid-asus | 95 +++++++++++++++++++
1 file changed, 95 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-asus
diff --git a/Documentation/ABI/testing/sysfs-driver-hid-asus b/Documentation/ABI/testing/sysfs-driver-hid-asus
new file mode 100644
index 000000000000..30e0d17d4cec
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hid-asus
@@ -0,0 +1,95 @@
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_model
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Model of the connected ASUS ROG XG Mobile station, as
+ reported by the station. An example is "GC33Y".
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The value is one line and is never empty while the file
+ exists.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_serial
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Serial number of the connected ASUS ROG XG Mobile station,
+ as reported by the station.
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The read is an empty line when the station did not report a
+ serial number.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_mainboard
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Mainboard identifier of the connected ASUS ROG XG Mobile
+ station, as reported by the station.
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The read is an empty line when the station did not report a
+ mainboard identifier.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_part_number
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Part number of the connected ASUS ROG XG Mobile station, as
+ reported by the station.
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The read is an empty line when the station did not report a
+ part number.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_firmware_version
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Firmware version of the connected ASUS ROG XG Mobile
+ station, as reported by the station.
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The read is an empty line when the version query failed or
+ the station did not report a firmware version.
+
+ Access: Read only
+ Valid values: a single line of text
+
+What: /sys/bus/hid/drivers/asus/<dev>/xgm_generation
+Date: October 2026
+Contact: linux-input@vger.kernel.org
+Description:
+ Hardware generation of the connected ASUS ROG XG Mobile
+ station, as reported by the station.
+
+ The file is created only for a supported station, one whose
+ model name starts with GC31, GC32 or GC33, and only while that
+ station is bound.
+ The read is an empty line when the version query failed or
+ the station did not report a generation.
+
+ Access: Read only
+ Valid values: a single line of text
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 16/17] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (14 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 15/17] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 17/17] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato
16 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
The egpu_enable attribute used to take four values. The WMI method
returned 1 for every one of them, so that return never said which
activation had been used. The extra values stayed while the right
choice was still unknown. It is known now: 0x0001 is NVIDIA, 0x0101
is AMD and 0x0201 is Intel, and a wrong value must not be sent. The
tools that drive this attribute only write 1, so the AMD and Intel
values were unused unless someone requested them by hand.
Writing 0 turns the eGPU off and writing 1 turns it on. The value
sent for 1 is the one that matches the GPU vendor of the connected
station, or NVIDIA when no vendor has been reported.
hid-asus reads that vendor from the station version query and reports
it through asus-wmi. Removing the station stores 0, which restores
the default. One mutex serializes the report. The connector is
proprietary, so only one XG Mobile can be attached, and one vendor
word is the whole state. The armoury store reads it while holding
egpu_mutex, in the same section as the WMI call.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
drivers/hid/hid-asus.c | 38 ++++++++++++++-
drivers/platform/x86/asus-armoury.c | 57 +++++++++++++---------
drivers/platform/x86/asus-wmi.c | 43 ++++++++++++++++
include/linux/platform_data/x86/asus-wmi.h | 11 +++++
4 files changed, 125 insertions(+), 24 deletions(-)
diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 691303b1a13f..9431a620318e 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -89,6 +89,20 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
#define ASUS_XGM_TLV_TAG_LEN 3
#define ASUS_XGM_TLV_HDR_LEN (ASUS_XGM_TLV_TAG_LEN + 1)
+/*
+ * Four PCI IDs at the start of the version payload, high byte first:
+ * vendor, device, then the two subsystem IDs.
+ */
+enum {
+ ASUS_XGM_PCI_VENDOR,
+ ASUS_XGM_PCI_DEVICE,
+ ASUS_XGM_PCI_SUBSYS0,
+ ASUS_XGM_PCI_SUBSYS1,
+ ASUS_XGM_PCI_ID_COUNT,
+};
+
+#define ASUS_XGM_PCI_ID_LEN (ASUS_XGM_PCI_ID_COUNT * sizeof(u16))
+
/*
* Every station answer echoes the report id, the opcode and the
* subcommand, then the payload.
@@ -245,6 +259,7 @@ struct asus_xgm_info {
char part_number[ASUS_XGM_INFO_LEN];
char firmware_version[ASUS_XGM_INFO_LEN];
char generation[ASUS_XGM_INFO_LEN];
+ u16 pci_id[ASUS_XGM_PCI_ID_COUNT];
};
struct asus_touchpad_info {
@@ -5156,6 +5171,18 @@ static int asus_xgm_get_version(struct hid_device *hdev,
asus_xgm_tlv_str(info->firmware_version, sizeof(info->firmware_version),
buf, len, ASUS_XGM_TAG_FIRMWARE);
+ /*
+ * The IDs are stored high byte first. On a captured GC33Y the
+ * payload begins 10 de 27 17 21 fb 10 43: NVIDIA 0x10de, device
+ * 0x2717, 0x21fb and ASUS 0x1043.
+ */
+ if (len < ASUS_XGM_RESP_PAYLOAD + ASUS_XGM_PCI_ID_LEN)
+ return -ENODATA;
+
+ for (int i = 0; i < ASUS_XGM_PCI_ID_COUNT; i++)
+ info->pci_id[i] = get_unaligned_be16(buf + ASUS_XGM_RESP_PAYLOAD +
+ i * sizeof(u16));
+
return 0;
}
@@ -5555,6 +5582,8 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
hid_info(hdev, "XG Mobile: firmware %s, generation %s\n",
info->firmware_version, info->generation);
+ asus_wmi_egpu_set_vendor(info->pci_id[ASUS_XGM_PCI_VENDOR]);
+
/*
* The attribute group is not devm-managed: a creation failure is
* fatal and unwound through asus_xgm_init_err, while on the unbind
@@ -5603,6 +5632,7 @@ static int asus_xgm_init(struct hid_device *hdev, struct asus_drvdata *drvdata)
return 0;
asus_xgm_init_err:
+ asus_wmi_egpu_set_vendor(0);
sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
drvdata->xgm_info = NULL;
drvdata->xgm_led = NULL;
@@ -5821,8 +5851,10 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
- if (drvdata->xgm_info)
+ if (drvdata->xgm_info) {
+ asus_wmi_egpu_set_vendor(0);
sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+ }
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
@@ -5848,8 +5880,10 @@ static void asus_remove(struct hid_device *hdev)
if (drvdata->listener.brightness_set)
asus_hid_unregister_listener(&drvdata->listener);
- if (drvdata->xgm_info)
+ if (drvdata->xgm_info) {
+ asus_wmi_egpu_set_vendor(0);
sysfs_remove_group(&hdev->dev.kobj, &asus_xgm_attr_group);
+ }
if (drvdata->xgm_led)
devm_led_classdev_unregister(&hdev->dev, &drvdata->xgm_led->cdev);
diff --git a/drivers/platform/x86/asus-armoury.c b/drivers/platform/x86/asus-armoury.c
index fb382de5b57b..dcf2e761016d 100644
--- a/drivers/platform/x86/asus-armoury.c
+++ b/drivers/platform/x86/asus-armoury.c
@@ -86,6 +86,8 @@ struct asus_armoury_priv {
* sequences and dGPU connection status:
* do not allow concurrent changes or changes
* before a reboot if dGPU got disabled.
+ * The activation section reads the reported GPU vendor here.
+ * That word is covered by egpu_vendor_lock in asus-wmi.
*/
struct mutex egpu_mutex;
@@ -538,13 +540,28 @@ static ssize_t dgpu_disable_current_value_store(struct kobject *kobj,
ASUS_WMI_SHOW_INT(dgpu_disable_current_value, ASUS_WMI_DEVID_DGPU);
ASUS_ATTR_GROUP_BOOL(dgpu_disable, "dgpu_disable", "Disable the dGPU");
-/* Values map for eGPU activation requests. */
-static u32 egpu_status_map[] = {
- [0] = 0x00000000U,
- [1] = 0x00000001U,
- [2] = 0x00000101U,
- [3] = 0x00000201U,
-};
+/*
+ * egpu_default_enable_value() - Activation value for the connected station
+ *
+ * The eGPU switch activation value selects the vendor of the external
+ * GPU: 0x0001 activates NVIDIA, 0x0101 AMD and 0x0201 Intel. Pick the
+ * value matching the GPU vendor reported for the connected station,
+ * and fall back to NVIDIA when no vendor has been reported. The caller
+ * holds egpu_mutex. asus_wmi_egpu_vendor() takes egpu_vendor_lock.
+ *
+ * Return: the activation value for the reported GPU vendor
+ */
+static u32 egpu_default_enable_value(void)
+{
+ switch (asus_wmi_egpu_vendor()) {
+ case PCI_VENDOR_ID_ATI:
+ return 0x0101;
+ case PCI_VENDOR_ID_INTEL:
+ return 0x0201;
+ default:
+ return 0x0001;
+ }
+}
/*
* armoury_pci_rescan() - Performs a PCI rescan
@@ -578,11 +595,17 @@ static ssize_t egpu_enable_current_value_store(struct kobject *kobj, struct kobj
if (err)
return err;
- if (requested >= ARRAY_SIZE(egpu_status_map))
+ if (requested > 1)
return -EINVAL;
- enable = egpu_status_map[requested];
scoped_guard(mutex, &asus_armoury.egpu_mutex) {
+ /*
+ * 0 deactivates the eGPU. 1 activates it with the value for
+ * the GPU reported for the connected station. Read that
+ * vendor in this section.
+ */
+ enable = requested ? egpu_default_enable_value() : 0;
+
/* Ensure the eGPU is connected before attempting to activate it. */
if (enable) {
err = armoury_get_devstate(NULL, &result, ASUS_WMI_DEVID_EGPU_CONNECTED);
@@ -661,7 +684,7 @@ static ssize_t egpu_enable_current_value_store(struct kobject *kobj, struct kobj
static ssize_t egpu_enable_current_value_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf)
{
- int i, err;
+ int err;
u32 status;
scoped_guard(mutex, &asus_armoury.egpu_mutex) {
@@ -670,20 +693,10 @@ static ssize_t egpu_enable_current_value_show(struct kobject *kobj, struct kobj_
return err;
}
- for (i = 0; i < ARRAY_SIZE(egpu_status_map); i++) {
- if (egpu_status_map[i] == status)
- return sysfs_emit(buf, "%u\n", i);
- }
-
- return -EIO;
+ return sysfs_emit(buf, "%u\n", status != 0);
}
-static ssize_t egpu_enable_possible_values_show(struct kobject *kobj, struct kobj_attribute *attr,
- char *buf)
-{
- return armoury_attr_enum_list(buf, ARRAY_SIZE(egpu_status_map));
-}
-ASUS_ATTR_GROUP_ENUM(egpu_enable, "egpu_enable", "Enable the eGPU (also disables dGPU)");
+ASUS_ATTR_GROUP_BOOL(egpu_enable, "egpu_enable", "Enable the eGPU (also disables dGPU)");
/* Device memory available to APU */
diff --git a/drivers/platform/x86/asus-wmi.c b/drivers/platform/x86/asus-wmi.c
index a65090429ca7..16836e7ee3dc 100644
--- a/drivers/platform/x86/asus-wmi.c
+++ b/drivers/platform/x86/asus-wmi.c
@@ -881,6 +881,49 @@ static DEVICE_ATTR_RW(dgpu_disable);
#endif /* IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS) */
/* eGPU ********************************************************************/
+
+/*
+ * One XG Mobile can be attached: the connector is proprietary, so
+ * this word is the whole report. 0 means the station is gone and
+ * the next activation uses the default value. egpu_vendor_lock
+ * covers the word. The armoury store holds egpu_mutex and then
+ * calls the getter, which takes this lock. The setter does not
+ * take egpu_mutex.
+ */
+static DEFINE_MUTEX(egpu_vendor_lock);
+static u16 egpu_gpu_vendor;
+
+/**
+ * asus_wmi_egpu_set_vendor() - Report the vendor of the external GPU
+ * @vendor: PCI vendor ID, or 0 when the station is removed
+ *
+ * Pass 0 when the station is removed. That restores the default
+ * activation value. egpu_vendor_lock serializes this with
+ * asus_wmi_egpu_vendor().
+ */
+void asus_wmi_egpu_set_vendor(u16 vendor)
+{
+ guard(mutex)(&egpu_vendor_lock);
+ egpu_gpu_vendor = vendor;
+}
+EXPORT_SYMBOL_NS_GPL(asus_wmi_egpu_set_vendor, "ASUS_WMI");
+
+/**
+ * asus_wmi_egpu_vendor() - Vendor of the external GPU
+ *
+ * Takes egpu_vendor_lock for the copy and releases it before
+ * returning. The armoury store calls this while holding egpu_mutex.
+ *
+ * Return: the PCI vendor ID last reported with
+ * asus_wmi_egpu_set_vendor(), or 0 when no station is known.
+ */
+u16 asus_wmi_egpu_vendor(void)
+{
+ guard(mutex)(&egpu_vendor_lock);
+ return egpu_gpu_vendor;
+}
+EXPORT_SYMBOL_NS_GPL(asus_wmi_egpu_vendor, "ASUS_WMI");
+
#if IS_ENABLED(CONFIG_ASUS_WMI_DEPRECATED_ATTRS)
static ssize_t egpu_enable_show(struct device *dev,
struct device_attribute *attr, char *buf)
diff --git a/include/linux/platform_data/x86/asus-wmi.h b/include/linux/platform_data/x86/asus-wmi.h
index b5ed8c83ace1..a53b2828bb1d 100644
--- a/include/linux/platform_data/x86/asus-wmi.h
+++ b/include/linux/platform_data/x86/asus-wmi.h
@@ -204,6 +204,8 @@ int asus_hid_register_listener(struct asus_hid_listener *cdev);
void asus_hid_unregister_listener(struct asus_hid_listener *cdev);
int asus_hid_event(enum asus_hid_event event);
bool asus_wmi_custom_fan_curve_is_enabled(void);
+void asus_wmi_egpu_set_vendor(u16 vendor);
+u16 asus_wmi_egpu_vendor(void);
#else
static inline void set_ally_mcu_hack(enum asus_ally_mcu_hack status)
{
@@ -239,6 +241,15 @@ static inline bool asus_wmi_custom_fan_curve_is_enabled(void)
{
return false;
}
+
+static inline void asus_wmi_egpu_set_vendor(u16 vendor)
+{
+}
+
+static inline u16 asus_wmi_egpu_vendor(void)
+{
+ return 0;
+}
#endif
#endif /* __PLATFORM_DATA_X86_ASUS_WMI_H */
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* [PATCH v12 17/17] MAINTAINERS: add maintainer for the ASUS HID driver
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
` (15 preceding siblings ...)
2026-10-31 17:41 ` [PATCH v12 16/17] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
@ 2026-10-31 17:41 ` Denis Benato
16 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 17:41 UTC (permalink / raw)
To: linux-kernel
Cc: platform-driver-x86, linux-input, kernel, Benjamin Tissoires,
Jiri Kosina, Luke D . Jones, Mateusz Schwartz, Denis Benato,
Jonathan LoBue, Khamunetri Clark, Derek J. Clark,
Joshua Leivenzon, Nathan Chancellor, Rebecca Mara Müller,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen,
Denis Benato
I already maintain the ASUS drivers in platform/x86, know these
devices well, have contributed in reviewing and bugfixing and have a
lot more work pending so I volunteer on helping making this driver
better: give hid-asus.c and its sysfs ABI file a dedicated entry
and list me as its maintainer.
Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
MAINTAINERS | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 4a69da715c58..3dbf491046b9 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4135,6 +4135,13 @@ L: linux-hwmon@vger.kernel.org
S: Maintained
F: drivers/hwmon/asus-ec-sensors.c
+ASUS HID DRIVER
+M: Denis Benato <denis.benato@linux.dev>
+L: linux-input@vger.kernel.org
+S: Maintained
+F: Documentation/ABI/testing/sysfs-driver-hid-asus
+F: drivers/hid/hid-asus.c
+
ASUS NOTEBOOKS AND EEEPC ACPI/WMI EXTRAS DRIVERS
M: Corentin Chary <corentin.chary@gmail.com>
M: Luke D. Jones <luke@ljones.dev>
--
2.47.3
^ permalink raw reply related [flat|nested] 29+ messages in thread
* Re: [PATCH v12 07/17] HID: asus: add joysticks anti-deadzone configuration
2026-10-31 17:41 ` [PATCH v12 07/17] HID: asus: add joysticks anti-deadzone configuration Denis Benato
@ 2026-10-31 17:51 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-31 17:51 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] HID: asus: ABBA deadlock between ally_prefs.lock and kernfs_drain()
--
commit 69043ee7052574919d9b6d50e4a364916a41871f
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add joysticks anti-deadzone configuration
This commit adds sysfs attributes to configure the joystick anti-deadzone
parameter on ROG Ally devices and restores the configured values during
device initialization.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 5cf7258bc4273..9bfdde560834d 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> @@ -1321,11 +1324,152 @@ static struct device_attribute dev_attr_right_joystick_outer_threshold =
> static struct device_attribute dev_attr_right_joystick_outer_threshold_range =
> __ATTR(outer_threshold_range, 0444, right_joystick_outer_threshold_range_show, NULL);
>
[ ... ]
> +static ssize_t ally_anti_deadzone_show(struct device *dev, char *buf,
> + enum ally_joystick which)
> +{
> + guard(mutex)(&ally_prefs.lock);
> +
> + if (!ally_prefs.anti_deadzone_support) {
> + hid_dbg(to_hid_device(dev), "Anti-deadzone not supported on this device\n");
> + return -EOPNOTSUPP;
> + }
> +
> + return sysfs_emit(buf, "%u\n", *ally_anti_deadzones[which]);
> +}
> +
> +static ssize_t ally_anti_deadzone_store(struct device *dev, const char *buf,
> + size_t count, enum ally_joystick which,
> + const char *group)
> +{
> + u8 value, dz[ARRAY_SIZE(ally_anti_deadzones)];
> + struct hid_device *hdev = to_hid_device(dev);
> + unsigned int i;
> + int ret;
> +
> + ret = kstrtou8(buf, 10, &value);
> + if (ret || value > 100)
> + return -EINVAL;
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
> + if (!ally_prefs.anti_deadzone_support) {
> + hid_dbg(hdev, "Anti-deadzone not supported on this device\n");
> + return -EOPNOTSUPP;
> + }
> +
> + for (i = 0; i < ARRAY_SIZE(dz); i++)
> + dz[i] = *ally_anti_deadzones[i];
> + dz[which] = value;
> +
> + ret = ally_set_anti_deadzone(hdev, dz[ALLY_LEFT_JOYSTICK],
> + dz[ALLY_RIGHT_JOYSTICK]);
> + if (ret)
> + return ret;
> +
> + *ally_anti_deadzones[which] = value;
> + }
> +
> + sysfs_notify(&dev->kobj, group, "anti_deadzone");
> +
> + return count;
> +}
[ ... ]
> @@ -1602,7 +1748,7 @@ static int ally_prefs_setup(struct hid_device *hdev)
> }
>
> /* Skip the calibration groups when the capability is missing. */
> - if (ally_prefs.user_cal_support) {
> + if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support) {
> int cal_i;
>
> for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
> @@ -1643,8 +1789,7 @@ static void ally_sysfs_remove(struct hid_device *hdev)
> for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
> sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
>
> - if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
> - ally_prefs.resp_curve_support) {
> + if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support) {
> for (i = 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++)
> sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]);
> }
[Severity: High]
Can this cause an ABBA deadlock between ally_prefs.lock and the kernfs active
reference when userspace accesses the anti_deadzone sysfs attribute
concurrently with device disconnect, unbind, or setup rollback?
In drivers/hid/hid-asus.c, when userspace reads or writes the anti_deadzone
sysfs attribute, kernfs_fop_read_iter() or kernfs_fop_write_iter() acquires
the kernfs active reference via kernfs_get_active() before invoking
ally_anti_deadzone_show() or ally_anti_deadzone_store(), both of which then
take ally_prefs.lock:
kernfs_fop_read_iter() / kernfs_fop_write_iter()
kernfs_get_active() [acquires kn->active]
ally_anti_deadzone_show() / ally_anti_deadzone_store()
mutex_lock(&ally_prefs.lock) [waits for ally_prefs.lock]
Concurrently, ally_sysfs_remove() (and the error rollback path in
ally_prefs_setup()) holds ally_prefs.lock via guard(mutex)(&ally_prefs.lock)
while calling sysfs_remove_group() on ally_cal_attr_groups:
ally_sysfs_remove() / ally_prefs_setup()
guard(mutex)(&ally_prefs.lock) [holds ally_prefs.lock]
sysfs_remove_group()
__kernfs_remove()
kernfs_drain() [waits for kn->active to drain]
Wouldn't this lock inversion deadlock if a thread enters
ally_anti_deadzone_show() or ally_anti_deadzone_store() just as
ally_sysfs_remove() or ally_prefs_setup() removes the attribute groups under
ally_prefs.lock?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261031174130.208240-1-denis.benato@linux.dev?part=7
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v12 06/17] HID: asus: add triggers inner and outer range configuration
2026-10-31 17:41 ` [PATCH v12 06/17] HID: asus: add triggers " Denis Benato
@ 2026-10-31 17:54 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-31 17:54 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] HID: asus: AB-BA deadlock between ally_prefs.lock and kernfs active refs
--
commit e95ac74bc1842538166d7189f1bc71f71eb61de4
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add triggers inner and outer range configuration
This commit adds sysfs attributes to configure the inner and outer trigger
ranges for ROG Ally devices, along with restoring those trigger ranges on
device initialization.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 50b0e60405958..5cf7258bc4273 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> @@ -1342,9 +1351,201 @@ static const struct attribute_group ally_right_joystick_axis_group = {
> .attrs = right_joystick_axis_attrs,
> };
>
[ ... ]
> +static ssize_t ally_trigger_limit_show(char *buf, enum ally_trigger_limit which)
> +{
> + guard(mutex)(&ally_prefs.lock);
> +
> + if (!ally_prefs.user_cal_support)
> + return -EOPNOTSUPP;
> +
> + return sysfs_emit(buf, "%u\n", *ally_trigger_limits[which]);
> +}
> +
> +static ssize_t ally_trigger_limit_store(struct device *dev, const char *buf,
> + size_t count,
> + enum ally_trigger_limit which,
> + int lo, int hi,
> + const char *group, const char *name)
> +{
> + u8 value, v[ARRAY_SIZE(ally_trigger_limits)];
> + struct hid_device *hdev = to_hid_device(dev);
> + size_t i;
> + int ret;
> +
> + ret = kstrtou8(buf, 10, &value);
> + if (ret || value < lo || value > hi)
> + return -EINVAL;
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
> + if (!ally_prefs.user_cal_support)
> + return -EOPNOTSUPP;
> +
> + for (i = 0; i < ARRAY_SIZE(v); i++)
> + v[i] = *ally_trigger_limits[i];
> + v[which] = value;
> +
> + ret = ally_set_trigger_ranges(hdev, v[ALLY_LEFT_MIN], v[ALLY_LEFT_MAX],
> + v[ALLY_RIGHT_MIN], v[ALLY_RIGHT_MAX]);
> + if (ret)
> + return ret;
> +
> + *ally_trigger_limits[which] = value;
> + }
> +
> + sysfs_notify(&dev->kobj, group, name);
> +
> + return count;
> +}
[ ... ]
> +static const struct attribute_group ally_left_trigger_group = {
> + .name = "left_trigger",
> + .attrs = left_trigger_attrs,
> +};
> +
> +static const struct attribute_group ally_right_trigger_group = {
> + .name = "right_trigger",
> + .attrs = right_trigger_attrs,
> +};
> static const struct attribute_group *const ally_cal_attr_groups[] = {
> &ally_left_joystick_axis_group,
> &ally_right_joystick_axis_group,
> + &ally_left_trigger_group,
> + &ally_right_trigger_group,
> };
[Severity: High]
Can registering ally_left_trigger_group and ally_right_trigger_group in
ally_cal_attr_groups[] cause an AB-BA deadlock between ally_prefs.lock and
the kernfs active references on the trigger limit sysfs attributes?
When userspace reads or writes range_lower_limit or range_upper_limit,
kernfs_fop_read_iter() or kernfs_fop_write_iter() acquires the attribute's
active reference (kn->active) before calling ally_trigger_limit_show() or
ally_trigger_limit_store(), which then acquires ally_prefs.lock:
CPU 0:
kernfs_fop_read_iter() / kernfs_fop_write_iter()
kernfs_get_active() [holds kn->active]
ally_trigger_limit_show() / ally_trigger_limit_store()
mutex_lock(&ally_prefs.lock)
Meanwhile, during device removal in ally_sysfs_remove() (or error rollback
in ally_prefs_setup()), ally_prefs.lock is acquired before calling
sysfs_remove_group() on ally_cal_attr_groups[]:
drivers/hid/hid-asus.c:ally_sysfs_remove() {
...
guard(mutex)(&ally_prefs.lock);
...
if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
ally_prefs.resp_curve_support) {
for (i = 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++)
sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]);
}
}
sysfs_remove_group() calls __kernfs_remove() -> kernfs_drain(), which waits
for kn->active to drain while holding ally_prefs.lock:
CPU 1:
ally_sysfs_remove() / ally_prefs_setup()
mutex_lock(&ally_prefs.lock)
sysfs_remove_group()
__kernfs_remove()
kernfs_drain() [waits for kn->active]
If a sysfs read or write on the new trigger attributes runs concurrently
with device disconnect, unbind, or setup rollback, wouldn't CPU 0 and CPU 1
deadlock waiting on each other?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261031174130.208240-1-denis.benato@linux.dev?part=6
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v12 11/17] HID: asus: add support for turbo buttons
2026-10-31 17:41 ` [PATCH v12 11/17] HID: asus: add support for turbo buttons Denis Benato
@ 2026-10-31 17:54 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-31 17:54 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] HID: asus: ABBA deadlock between ally_prefs.lock and sysfs removal
- [High] HID: asus: UAF of ally_prefs.button_entries on second device removal
- [Low] HID: asus: mix of cleanup helpers and goto in ally_prefs_setup() and helper
- [Low] HID: asus: malformed kernel-doc comment on ally_set_turbo_params()
- [Low] HID: asus: per-call stack table and manual copy in ally_btn_param() and setter
--
commit d9c271786b208c5b04b58afd3cb388dfc9868ef1
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add support for turbo buttons
This commit adds support for configuring ROG Ally turbo button parameters
by exposing turbo_period and toggle_period sysfs attributes for each
gamepad button and restoring the settings during device initialization.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 3e58c5ca0cbd8..d20e433af5740 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
> @@ -812,6 +812,80 @@ struct ally_joystick_resp_curve {
[ ... ]
> +/* Payload order. Not the same order as enum ally_button_id. */
> +struct ally_turbo_config {
> + struct ally_btn_turbo_params btn_du;
> + struct ally_btn_turbo_params btn_dd;
> + struct ally_btn_turbo_params btn_dl;
> + struct ally_btn_turbo_params btn_dr;
> + struct ally_btn_turbo_params btn_j0b;
> + struct ally_btn_turbo_params btn_j1b;
> + struct ally_btn_turbo_params btn_lb;
> + struct ally_btn_turbo_params btn_rb;
> + struct ally_btn_turbo_params btn_a;
> + struct ally_btn_turbo_params btn_b;
> + struct ally_btn_turbo_params btn_x;
> + struct ally_btn_turbo_params btn_y;
> + struct ally_btn_turbo_params btn_view;
> + struct ally_btn_turbo_params btn_menu;
> + struct ally_btn_turbo_params btn_m2;
> + struct ally_btn_turbo_params btn_m1;
> +};
[ ... ]
> @@ -2120,9 +2197,297 @@ static const struct attribute_group ally_attr_groups[] = {
> },
> };
>
> +static struct ally_btn_turbo_params *ally_btn_param(enum ally_button_id btn)
> +{
> + struct ally_btn_turbo_params *table[ALLY_BTN_MAX] = {
> + [ALLY_BTN_A] = &ally_prefs.turbo.btn_a,
> + [ALLY_BTN_B] = &ally_prefs.turbo.btn_b,
> + [ALLY_BTN_X] = &ally_prefs.turbo.btn_x,
> + [ALLY_BTN_Y] = &ally_prefs.turbo.btn_y,
> + [ALLY_BTN_LB] = &ally_prefs.turbo.btn_lb,
> + [ALLY_BTN_RB] = &ally_prefs.turbo.btn_rb,
> + [ALLY_BTN_DU] = &ally_prefs.turbo.btn_du,
> + [ALLY_BTN_DD] = &ally_prefs.turbo.btn_dd,
> + [ALLY_BTN_DL] = &ally_prefs.turbo.btn_dl,
> + [ALLY_BTN_DR] = &ally_prefs.turbo.btn_dr,
> + [ALLY_BTN_J0B] = &ally_prefs.turbo.btn_j0b,
> + [ALLY_BTN_J1B] = &ally_prefs.turbo.btn_j1b,
> + [ALLY_BTN_MENU] = &ally_prefs.turbo.btn_menu,
> + [ALLY_BTN_VIEW] = &ally_prefs.turbo.btn_view,
> + [ALLY_BTN_M1] = &ally_prefs.turbo.btn_m1,
> + [ALLY_BTN_M2] = &ally_prefs.turbo.btn_m2,
> + };
> +
> + if (btn >= ALLY_BTN_MAX)
> + return NULL;
> +
> + return table[btn];
> +}
> +
> +/** ally_set_turbo_params() - Set the turbo parameters
> + *
> + * Caller holds ally_prefs.lock.
> + *
> + * @hdev: The HID device to send the packet to.
> + * Return: 0 on success, negative error code on failure.
> + */
[Severity: Low]
This isn't a bug, but in drivers/hid/hid-asus.c above ally_set_turbo_params(),
the kernel-doc comment has the function identifier on the opening /** line
and places the locking note before the @hdev parameter description.
Could the opening /** be placed on its own line and the description text
moved after the parameter list so scripts/kernel-doc can parse it cleanly?
> +static int ally_set_turbo_params(struct hid_device *hdev)
> +{
> + struct ally_turbo_config *turbo = &ally_prefs.turbo;
> + const u8 payload[] = {
> + turbo->btn_du.turbo, turbo->btn_du.toggle,
> + turbo->btn_dd.turbo, turbo->btn_dd.toggle,
> + turbo->btn_dl.turbo, turbo->btn_dl.toggle,
> + turbo->btn_dr.turbo, turbo->btn_dr.toggle,
> + turbo->btn_j0b.turbo, turbo->btn_j0b.toggle,
> + turbo->btn_j1b.turbo, turbo->btn_j1b.toggle,
> + turbo->btn_lb.turbo, turbo->btn_lb.toggle,
> + turbo->btn_rb.turbo, turbo->btn_rb.toggle,
> + turbo->btn_a.turbo, turbo->btn_a.toggle,
> + turbo->btn_b.turbo, turbo->btn_b.toggle,
> + turbo->btn_x.turbo, turbo->btn_x.toggle,
> + turbo->btn_y.turbo, turbo->btn_y.toggle,
> + turbo->btn_view.turbo, turbo->btn_view.toggle,
> + turbo->btn_menu.turbo, turbo->btn_menu.toggle,
> + turbo->btn_m2.turbo, turbo->btn_m2.toggle,
> + turbo->btn_m1.turbo, turbo->btn_m1.toggle,
> + };
[Severity: Low]
This isn't a bug, but in drivers/hid/hid-asus.c, ally_btn_param() rebuilds
table[ALLY_BTN_MAX] on the stack on every call instead of marking it static
const (like ally_trigger_limits, ally_anti_deadzones, and ally_resp_curves
earlier in the file), and ally_set_turbo_params() manually copies all 32
bytes into payload[] even though struct ally_turbo_config is already laid
out in hardware payload order.
Could table[] in ally_btn_param() be marked static const (or replaced with
an indexed array), and could struct ally_turbo_config be marked __packed and
passed directly to ally_alloc_cmd()?
[ ... ]
> +static ssize_t btn_turbo_period_show(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + struct ally_btn_sysfs_entry *entry;
> + struct ally_btn_turbo_params *params;
> +
> + entry = container_of(attr, struct ally_btn_sysfs_entry, attr_turbo_period);
> +
> + guard(mutex)(&ally_prefs.lock);
> +
> + if (!ally_prefs.turbo_support)
> + return -EOPNOTSUPP;
> +
> + params = ally_btn_param(entry->btn);
> + if (!params)
> + return -ENODEV;
> +
> + return sysfs_emit(buf, "%hhu\n", params->turbo);
> +}
> +
> +static ssize_t btn_turbo_period_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct ally_btn_sysfs_entry *entry;
> + struct hid_device *hdev = to_hid_device(dev);
> + struct ally_btn_turbo_params *params;
> + u8 old_value, value;
> + int ret;
> +
> + entry = container_of(attr, struct ally_btn_sysfs_entry, attr_turbo_period);
> +
> + ret = kstrtou8(buf, 10, &value);
> + if (ret || value > ALLY_TURBO_PERIOD_MAX)
> + return ret ? ret : -EINVAL;
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
[ ... ]
> +static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_device *hdev)
> +{
> + int i, ret;
> +
> + if (!ally_prefs.turbo_support)
> + return NULL;
> +
> + struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
> + if (!entries)
> + return ERR_PTR(-ENOMEM);
> +
> + for (i = 0; i < ALLY_BTN_MAX; i++) {
> + entries[i].btn = i;
> +
> + if (!ally_btn_param(i)) {
> + hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
> + ret = -EINVAL;
> + goto ally_btn_err_cleanup;
> + }
> +
> + ally_btn_turbo_init_attrs(&entries[i]);
> + entries[i].group.name = ally_button_names[i];
> + entries[i].group.attrs = entries[i].attrs;
> +
> + ret = sysfs_create_group(&hdev->dev.kobj, &entries[i].group);
> + if (ret < 0) {
> + hid_err(hdev, "Failed to create sysfs group for %s: %d\n",
> + ally_button_names[i], ret);
> + goto ally_btn_err_cleanup;
> + }
> + }
> +
> + return no_free_ptr(entries);
> +
> +ally_btn_err_cleanup:
> + while (i--)
> + sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
> +
> + return ERR_PTR(ret);
> +}
> +
> +static void ally_remove_button_attributes(struct hid_device *hdev)
> +{
> + struct ally_btn_sysfs_entry *entries;
> + int i;
> +
> + entries = ally_prefs.button_entries;
> + if (!entries)
> + return;
> +
> + for (i = 0; i < ALLY_BTN_MAX; i++)
> + sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
> +
> + kfree(entries);
> + ally_prefs.button_entries = NULL;
> +}
> +
> static int ally_prefs_setup(struct hid_device *hdev)
> {
> - int ret, i;
> + struct ally_btn_sysfs_entry *entries = NULL;
> + int ret, i, cal_n = 0;
>
> ret = ally_detect_capabilities(hdev);
> if (ret)
> @@ -2158,27 +2523,38 @@ static int ally_prefs_setup(struct hid_device *hdev)
> /* Skip the calibration groups when the capability is missing. */
> if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
> ally_prefs.resp_curve_support) {
> - int cal_i;
> -
> - for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
> + for (; cal_n < ARRAY_SIZE(ally_cal_attr_groups); cal_n++) {
> ret = sysfs_create_group(&hdev->dev.kobj,
> - ally_cal_attr_groups[cal_i]);
> + ally_cal_attr_groups[cal_n]);
> if (ret) {
> hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
> - ally_cal_attr_groups[cal_i]->name, ret);
> - while (cal_i--)
> - sysfs_remove_group(&hdev->dev.kobj,
> - ally_cal_attr_groups[cal_i]);
> - for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
> - sysfs_remove_group(&hdev->dev.kobj,
> - &ally_attr_groups[i]);
> - return ret;
> + ally_cal_attr_groups[cal_n]->name, ret);
> + goto err_cal_groups;
> }
> }
> }
>
> + if (ally_prefs.turbo_support) {
> + entries = ally_create_button_attributes(hdev);
> + if (IS_ERR(entries)) {
> + ret = PTR_ERR(entries);
> + hid_err(hdev, "Failed to create button attributes: %d\n", ret);
> + goto err_cal_groups;
> + }
> + }
> +
> + ally_prefs.button_entries = entries;
> ally_prefs.sysfs_live = true;
> return 0;
> +
> +err_cal_groups:
> + while (cal_n--)
> + sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[cal_n]);
> +
> + for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
> + sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
> +
> + return ret;
> }
[Severity: Low]
This isn't a bug, but in drivers/hid/hid-asus.c,
ally_create_button_attributes() mixes __free(kfree) on entries with goto
ally_btn_err_cleanup, and ally_prefs_setup() mixes
guard(mutex)(&ally_prefs.lock) with goto err_cal_groups.
Since include/linux/cleanup.h advises against mixing scope-based cleanup
helpers and goto error labels within the same function, could these
functions use either pure goto-based cleanup or pure scope-based cleanup?
> static void ally_sysfs_remove(struct hid_device *hdev)
> @@ -2195,6 +2571,8 @@ static void ally_sysfs_remove(struct hid_device *hdev)
>
> ally_prefs.sysfs_live = false;
>
> + ally_remove_button_attributes(hdev);
[Severity: High]
Can this trigger an ABBA deadlock between ally_prefs.lock and kernfs active
node draining?
In drivers/hid/hid-asus.c, when userspace reads or writes any of the turbo
button sysfs attributes (btn_turbo_period_show(), btn_turbo_period_store(),
btn_toggle_period_show(), or btn_toggle_period_store()), kernfs holds an
active reference on the attribute kernfs_node and the callback then acquires
ally_prefs.lock:
kernfs_fop_read_iter() / kernfs_fop_write_iter()
kernfs_get_active_of() /* acquires kn->active */
btn_turbo_period_show() / btn_turbo_period_store()
guard(mutex)(&ally_prefs.lock)
Conversely, during device removal in ally_sysfs_remove() (and on the error
unwind paths in ally_prefs_setup() and ally_create_button_attributes()),
ally_prefs.lock is held while calling ally_remove_button_attributes() and
sysfs_remove_group(), which calls kernfs_drain() to wait synchronously for
kn->active to drain:
ally_sysfs_remove()
guard(mutex)(&ally_prefs.lock)
ally_remove_button_attributes()
sysfs_remove_group()
kernfs_remove_by_name_ns()
__kernfs_remove()
kernfs_drain() /* waits on kn->active */
If a sysfs read or write is waiting to acquire ally_prefs.lock while
ally_sysfs_remove() or ally_prefs_setup() calls sysfs_remove_group() under
ally_prefs.lock, won't both tasks deadlock waiting on each other?
[Severity: High]
Can removing a second HID_ALLY_INTF_CFG_IN device cause a use-after-free of
ally_prefs.button_entries?
In drivers/hid/hid-asus.c, when the first device (hdev1) probes,
ally_prefs_setup(hdev1) allocates ally_prefs.button_entries via
ally_create_button_attributes(hdev1), registers the attribute groups (which
embed struct device_attribute attr_turbo_period and attr_toggle_period) on
hdev1->dev.kobj, and sets ally_prefs.sysfs_live = true.
If a second matching HID_ALLY_INTF_CFG_IN interface (hdev2) is probed,
ally_prefs_setup(hdev2) first calls ally_detect_capabilities(hdev2)
(overwriting ally_prefs.turbo_support and other global capability flags),
and then sees ally_prefs.sysfs_live == true and returns 0 without
registering sysfs groups on hdev2->dev.kobj:
ally_prefs_setup(hdev2)
ally_detect_capabilities(hdev2)
...
guard(mutex)(&ally_prefs.lock);
...
if (ally_prefs.sysfs_live)
return 0;
When hdev2 is subsequently unbound, asus_remove(hdev2) calls
ally_sysfs_remove(hdev2) -> ally_remove_button_attributes(hdev2):
ally_remove_button_attributes(hdev2)
entries = ally_prefs.button_entries;
for (i = 0; i < ALLY_BTN_MAX; i++)
sysfs_remove_group(&hdev2->dev.kobj, &entries[i].group);
kfree(entries);
ally_prefs.button_entries = NULL;
Because ally_prefs does not track which hid_device owns the live sysfs
groups, sysfs_remove_group() looks for the groups on hdev2->dev.kobj (leaving
them registered on hdev1->dev.kobj) and kfree(entries) frees the backing
struct ally_btn_sysfs_entry memory while hdev1's sysfs attributes are still
active. Wouldn't any subsequent sysfs access on hdev1 (or unbinding hdev1)
dereference freed memory?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261031174130.208240-1-denis.benato@linux.dev?part=11
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v12 05/17] HID: asus: add joysticks inner and outer range configuration
2026-10-31 17:41 ` [PATCH v12 05/17] HID: asus: add joysticks inner and outer range configuration Denis Benato
@ 2026-10-31 17:57 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-31 17:57 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] HID: asus: ABBA deadlock holding ally_prefs.lock in ally_sysfs_remove()
- [Low] HID: asus: inconsistent threshold defines and callback duplication
--
commit 21c1f6aa8c8abefa0a006966f1d360a594341b9e
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add joysticks inner and outer range configuration
This commit adds sysfs attributes under left_joystick_axis and
right_joystick_axis on ROG Ally devices to configure and restore the inner
and outer thresholds for both joysticks.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index fe2008f9d51d1..50b0e60405958 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
> @@ -98,6 +98,9 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
>
> #define ASUS_HID_INPUT_KBD_REPORT_ID 0x01
>
> +#define ASUS_ALLY_OUTER_THRESHOLD_MIN 70
> +#define ASUS_ALLY_OUTER_THRESHOLD_MAX 100
> +
[ ... ]
> @@ -826,7 +834,6 @@ static int ally_detect_capabilities(struct hid_device *hdev)
> return 0;
> }
>
> -/* Caller holds ally_prefs.lock. The packet is sent on @hdev. */
> static int ally_set_xbox_controller(struct hid_device *hdev, bool enabled)
> {
> const u8 payload[] = { enabled ?
> @@ -1042,6 +1049,304 @@ static struct attribute *ally_right_vibration_attrs[] = {
[ ... ]
> +static ssize_t left_joystick_inner_threshold_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + u8 value;
> + int ret;
> +
> + ret = kstrtou8(buf, 10, &value);
> + if (ret || value > 50)
> + return -EINVAL;
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
> + if (!ally_prefs.user_cal_support)
> + return -EOPNOTSUPP;
> +
> + ret = ally_set_joystick_thresholds(hdev, value,
> + ally_prefs.left_outer_threshold,
> + ally_prefs.right_deadzone,
> + ally_prefs.right_outer_threshold);
> + if (ret)
> + return ret;
> +
> + ally_prefs.left_deadzone = value;
> + }
> +
> + sysfs_notify(&dev->kobj, "left_joystick_axis", "inner_threshold");
> +
> + return count;
> +}
> +
> +static ssize_t left_joystick_inner_threshold_range_show(struct device *dev,
> + struct device_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "0 50\n");
> +}
> +
> +static ssize_t left_joystick_outer_threshold_show(struct device *dev,
> + struct device_attribute *attr,
> + char *buf)
> +{
> + guard(mutex)(&ally_prefs.lock);
> +
> + if (!ally_prefs.user_cal_support)
> + return -EOPNOTSUPP;
> +
> + return sysfs_emit(buf, "%u\n", ally_prefs.left_outer_threshold);
> +}
> +
> +static ssize_t left_joystick_outer_threshold_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + u8 value;
> + int ret;
> +
> + ret = kstrtou8(buf, 10, &value);
> + if (ret || value < 70 || value > 100)
> + return -EINVAL;
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
> + if (!ally_prefs.user_cal_support)
> + return -EOPNOTSUPP;
> +
> + ret = ally_set_joystick_thresholds(hdev, ally_prefs.left_deadzone,
> + value, ally_prefs.right_deadzone,
> + ally_prefs.right_outer_threshold);
> + if (ret)
> + return ret;
> +
> + ally_prefs.left_outer_threshold = value;
> + }
> +
> + sysfs_notify(&dev->kobj, "left_joystick_axis", "outer_threshold");
> +
> + return count;
> +}
> +
> +static ssize_t left_joystick_outer_threshold_range_show(struct device *dev,
> + struct device_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "70 100\n");
> +}
[ ... ]
> +static ssize_t right_joystick_inner_threshold_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + u8 value;
> + int ret;
> +
> + ret = kstrtou8(buf, 10, &value);
> + if (ret || value > 50)
> + return -EINVAL;
[ ... ]
> +static ssize_t right_joystick_inner_threshold_range_show(struct device *dev,
> + struct device_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "0 50\n");
> +}
[ ... ]
> +static ssize_t right_joystick_outer_threshold_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + u8 value;
> + int ret;
> +
> + ret = kstrtou8(buf, 10, &value);
> + if (ret || value < ASUS_ALLY_OUTER_THRESHOLD_MIN || value > ASUS_ALLY_OUTER_THRESHOLD_MAX)
> + return -EINVAL;
[ ... ]
> +static ssize_t right_joystick_outer_threshold_range_show(struct device *dev,
> + struct device_attribute *attr,
> + char *buf)
> +{
> + return sysfs_emit(buf, "%u %u\n", ASUS_ALLY_OUTER_THRESHOLD_MIN,
> + ASUS_ALLY_OUTER_THRESHOLD_MAX);
> +}
[Severity: Low]
As reported by Ilpo Järvinen on v11 [1], this isn't a bug, but
ASUS_ALLY_OUTER_THRESHOLD_MIN and ASUS_ALLY_OUTER_THRESHOLD_MAX are only
used in right_joystick_outer_threshold_store() and
right_joystick_outer_threshold_range_show(), while
left_joystick_outer_threshold_store() and
left_joystick_outer_threshold_range_show() still use literal 70 and 100.
Similarly, the inner threshold upper bound of 50 is hardcoded in
left_joystick_inner_threshold_store(),
left_joystick_inner_threshold_range_show(),
right_joystick_inner_threshold_store(), and
right_joystick_inner_threshold_range_show(), and the left and right range
show callbacks are identical.
Could these threshold bounds all use shared defines, share the range show
callbacks between left and right joysticks, and keep the locking comment
above ally_set_xbox_controller()?
[ ... ]
> @@ -1093,6 +1399,27 @@ static int ally_prefs_setup(struct hid_device *hdev)
> }
> }
>
> + /* Skip the calibration groups when the capability is missing. */
> + if (ally_prefs.user_cal_support) {
> + int cal_i;
> +
> + for (cal_i = 0; cal_i < ARRAY_SIZE(ally_cal_attr_groups); cal_i++) {
> + ret = sysfs_create_group(&hdev->dev.kobj,
> + ally_cal_attr_groups[cal_i]);
> + if (ret) {
> + hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
> + ally_cal_attr_groups[cal_i]->name, ret);
> + while (cal_i--)
> + sysfs_remove_group(&hdev->dev.kobj,
> + ally_cal_attr_groups[cal_i]);
> + for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
> + sysfs_remove_group(&hdev->dev.kobj,
> + &ally_attr_groups[i]);
> + return ret;
> + }
> + }
> + }
> +
> ally_prefs.sysfs_live = true;
> return 0;
> }
> @@ -1104,15 +1431,21 @@ static void ally_sysfs_remove(struct hid_device *hdev)
> if (ally_get_endpoint_address(hdev) != HID_ALLY_INTF_CFG_IN)
> return;
>
> - scoped_guard(mutex, &ally_prefs.lock) {
> - if (!ally_prefs.sysfs_live)
> - return;
> + guard(mutex)(&ally_prefs.lock);
>
> - ally_prefs.sysfs_live = false;
> - }
> + if (!ally_prefs.sysfs_live)
> + return;
> +
> + ally_prefs.sysfs_live = false;
>
> for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
> sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
> +
> + if (ally_prefs.user_cal_support || ally_prefs.anti_deadzone_support ||
> + ally_prefs.resp_curve_support) {
> + for (i = 0; i < ARRAY_SIZE(ally_cal_attr_groups); i++)
> + sysfs_remove_group(&hdev->dev.kobj, ally_cal_attr_groups[i]);
> + }
> }
[Severity: High]
Can changing scoped_guard(mutex, &ally_prefs.lock) to
guard(mutex)(&ally_prefs.lock) in ally_sysfs_remove() (and calling
sysfs_remove_group() under ally_prefs.lock in the error path of
ally_prefs_setup()) cause an ABBA deadlock with concurrent sysfs reads or
writes?
When userspace reads or writes an Ally sysfs attribute such as
left_joystick_inner_threshold_show() or
left_joystick_inner_threshold_store(), kernfs acquires the active reference
on the kernfs_node before invoking the callback, which then locks
ally_prefs.lock:
kernfs_fop_read_iter() / kernfs_fop_write_iter()
kernfs_get_active()
left_joystick_inner_threshold_show()
mutex_lock(&ally_prefs.lock)
When ally_sysfs_remove() or the error rollback in ally_prefs_setup() runs
concurrently, it holds ally_prefs.lock while calling sysfs_remove_group(),
which waits in kernfs_drain() for all active references on those attributes
to finish:
ally_sysfs_remove()
mutex_lock(&ally_prefs.lock)
sysfs_remove_group()
__kernfs_remove()
kernfs_drain()
Should ally_prefs.lock be released before calling sysfs_remove_group()?
[1] https://lore.kernel.org/r/2bf5c5f8-185e-c322-dd76-ec182514c93c@linux.intel.com
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261031174130.208240-1-denis.benato@linux.dev?part=5
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v12 03/17] HID: asus: add gamepad configuration
2026-10-31 17:41 ` [PATCH v12 03/17] HID: asus: add gamepad configuration Denis Benato
@ 2026-10-31 17:59 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-31 17:59 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] HID: asus: per-device sysfs group tracked by global sysfs_live flag
--
commit 7492a3880439a824fa483d73553323c7d5fdab75
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add gamepad configuration
This commit adds ROG Ally gamepad capability detection, initializes the
controller state, and exposes the xbox_controller sysfs attribute. It also
updates hid_asus_ally_reset_resume() to wait for the gamepad MCU to become
ready.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 552d3b25a1d10..5f5393ea92b7e 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> @@ -729,6 +736,224 @@ static int ally_get_endpoint_address(struct hid_device *hdev)
> return ep->desc.bEndpointAddress;
> }
>
> +/*
> + * Settings that must survive the controller powering off and coming back.
> + * No device or input pointers live here: every transfer uses the interface
> + * that is bound right now.
> + *
> + * One set for the machine is by design. The Ally controller is
> + * part of the PC, so two ROG Ally HID devices are not bound at
> + * once. The only way to see a second one is to emulate it with
> + * uhid.
> + */
> +static struct ally_prefs {
> + /* Guards every field below, including across the feature report. */
> + struct mutex lock;
> +
> + bool initialized;
> + bool sysfs_live;
[ ... ]
> +static int ally_prefs_setup(struct hid_device *hdev)
> +{
> + int ret, i;
> +
> + ret = ally_detect_capabilities(hdev);
> + if (ret)
> + return ret;
> +
> + guard(mutex)(&ally_prefs.lock);
> +
> + if (!ally_prefs.initialized) {
> + ally_prefs.gamepad_mode = 0x01;
> + ally_prefs.xbox_controller_enabled = true;
> + ally_prefs.initialized = true;
> + }
> +
> + if (ally_prefs.sysfs_live)
> + return 0;
> +
> + for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++) {
> + ret = sysfs_create_group(&hdev->dev.kobj, &ally_attr_groups[i]);
> + if (ret) {
> + hid_err(hdev, "Failed to create Ally sysfs group '%s': %d\n",
> + ally_attr_groups[i].name ?: "", ret);
> + while (i--)
> + sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
> + return ret;
> + }
> + }
> +
> + ally_prefs.sysfs_live = true;
> + return 0;
> +}
> +
> +static void ally_sysfs_remove(struct hid_device *hdev)
> +{
> + int i;
> +
> + if (ally_get_endpoint_address(hdev) != HID_ALLY_INTF_CFG_IN)
> + return;
> +
> + scoped_guard(mutex, &ally_prefs.lock) {
> + if (!ally_prefs.sysfs_live)
> + return;
> +
> + ally_prefs.sysfs_live = false;
> + }
> +
> + for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
> + sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
> +}
[Severity: Medium]
Can tracking per-device sysfs registration with the global
ally_prefs.sysfs_live boolean leave a dangling sysfs group on a device or
skip creating it on a newly probed one?
In hid_asus_ally_probe(), ally_prefs_setup() attaches ally_attr_groups to the
per-device &hdev->dev.kobj and sets ally_prefs.sysfs_live = true on a first
HID_ALLY_INTF_CFG_IN device (hdev1).
If a second HID_ALLY_INTF_CFG_IN device (hdev2, for example via USB
emulation) is probed:
1. If ally_gamepad_check_ready(hdev2) fails in hid_asus_ally_probe(),
asus_probe() logs the error and still succeeds with drvdata->ally left as
NULL:
if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
ally = hid_asus_ally_probe(hdev);
if (IS_ERR(ally))
hid_err(hdev, "Failed to initialize ROG Ally HID extensions: %ld\n",
PTR_ERR(ally));
else
/* Readers take this with smp_load_acquire(). */
smp_store_release(&drvdata->ally, ally);
}
2. If ally_gamepad_check_ready(hdev2) succeeds, ally_prefs_setup(hdev2)
overwrites the global capability flags via ally_detect_capabilities(hdev2)
and then returns 0 early because ally_prefs.sysfs_live is already true,
without creating the sysfs group on &hdev2->dev.kobj.
When hdev2 is later unbound or removed, asus_remove(hdev2) unconditionally
calls ally_sysfs_remove(hdev2) without checking drvdata->ally or whether
hdev2 owns the registered sysfs group:
if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
ally_quiesce(drvdata->ally);
ally_sysfs_remove(hdev);
}
Because hdev2 has endpoint HID_ALLY_INTF_CFG_IN and ally_prefs.sysfs_live is
true, ally_sysfs_remove(hdev2) clears ally_prefs.sysfs_live = false and calls
sysfs_remove_group() on &hdev2->dev.kobj where the group was never created.
When hdev1 is later unbound or removed, ally_sysfs_remove(hdev1) sees
ally_prefs.sysfs_live == false and returns early without removing the sysfs
group from &hdev1->dev.kobj. Similarly, if hdev1 removal races with hdev2
probing before ally_sysfs_remove(hdev1) clears ally_prefs.sysfs_live,
ally_prefs_setup(hdev2) sees ally_prefs.sysfs_live == true and skips creating
the sysfs group on hdev2.
Should sysfs group ownership be tracked per device (for instance on struct
ally_intf) rather than with a global boolean?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261031174130.208240-1-denis.benato@linux.dev?part=3
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v12 12/17] HID: asus: add support for btn remapping
2026-10-31 17:41 ` [PATCH v12 12/17] HID: asus: add support for btn remapping Denis Benato
@ 2026-10-31 18:00 ` sashiko-bot
2026-10-31 19:06 ` Denis Benato
0 siblings, 1 reply; 29+ messages in thread
From: sashiko-bot @ 2026-10-31 18:00 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] HID: asus: ABBA deadlock between ally_prefs.lock and sysfs active refs
- [Low] HID: asus: mixed cleanup helpers and goto in button_remap_store()
- [Low] HID: asus: commit message refers to remap_index instead of remap_available
--
commit eab19113fde5452095dfc729f53ab98ff0fc3365
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add support for btn remapping
This commit adds button and macro remapping support for ASUS ROG Ally
devices via per-button sysfs attributes (remap, macro, and
remap_available), validating mappings against the active gamepad mode and
restoring or refreshing the cached mappings across mode switches and MCU
resets.
> HID: asus: add support for btn remapping
>
> Add support for buttons remapping: the ability to assign different
> button events to each one of the physical button on the device.
>
> Each button exposes a remap_index sysfs attribute listing the valid
> remap target names for the current gamepad mode: M1 and M2 can be
> remapped to anything at any time, while other buttons may only be
> remapped to gamepad codes in gamepad mode and keyboard/mouse/media
> codes in desktop mode.
>
> When the gamepad mode is changed the firmware automatically restores
> its internal defaults, so the software cache is refreshed to reflect
> the hardware state.
>
> Co-developed-by: Luke Jones <luke@ljones.dev>
> Signed-off-by: Luke Jones <luke@ljones.dev>
> Signed-off-by: Denis Benato <denis.benato@linux.dev>
> Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
[Severity: Low]
This isn't a bug, but the commit message refers to a remap_index sysfs
attribute, whereas ally_create_button_attributes() in drivers/hid/hid-asus.c
registers this attribute as remap_available alongside the remap and macro
attributes created in button_remap_attr_create(). Should the commit message
be updated to mention remap, macro, and remap_available instead of
remap_index?
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index d20e433af5740..b94cb8c50b28b 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> +static ssize_t button_remap_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct hid_device *hdev = to_hid_device(dev);
> + struct button_remap_attr *btn_attr = to_button_remap_attr(attr);
> + enum ally_button_id button_id = btn_attr->button_id;
> + struct ally_button_mapping *mode_map;
> + const struct btn_code_map *code;
> + const struct btn_code_map *old_code;
> + enum btn_pair_index pair_idx;
> + struct button_pair_map *pair;
> + struct button_map *btn_map;
> + char btn_name[32];
> + bool is_first;
> + int ret, i;
> +
> + if (sscanf(buf, "%31s", btn_name) != 1)
> + return -EINVAL;
> +
> + code = find_button_by_name(btn_name);
> + if (!code)
> + return -EINVAL;
> +
> + ret = get_button_pair_info(button_id, &pair_idx, &is_first);
> + if (ret < 0)
> + return ret;
> +
> + guard(mutex)(&ally_prefs.lock);
> +
> + /*
> + * Validate the remap target while holding the lock: the
> + * gamepad mode can change concurrently, and the check must
> + * be done against the very mode the mapping is written to.
> + */
> + if (!ally_remap_code_valid(button_id, ally_prefs.gamepad_mode, code,
> + btn_attr->is_macro))
> + return -EINVAL;
> +
> + /* Access the mapping for current gamepad mode */
> + pair = &ally_prefs.button_mappings.button_pairs[pair_idx - 1];
> + btn_map = is_first ? &pair->first : &pair->second;
> +
> + if (btn_attr->is_macro) {
> + old_code = btn_map->macro;
> + btn_map->macro = code;
> + } else {
> + old_code = btn_map->remap;
> + btn_map->remap = code;
> + }
> +
> + /* Update pair index */
> + pair->pair_index = pair_idx;
> +
> + /*
> + * Send mapping to device with the caveat that first
> + * generation devices require individual button pair updates.
> + */
> + if (ally_prefs.is_ally_x) {
> + ret = ally_set_button_mapping(hdev, pair);
> + if (ret < 0)
> + goto err_restore;
> + } else {
> + mode_map = &ally_prefs.button_mappings;
> + for (i = 0; i < ARRAY_SIZE(mode_map->button_pairs); i++) {
> + mode_map->button_pairs[i].pair_index = i + 1;
> + ret = ally_set_button_mapping(hdev,
> + &mode_map->button_pairs[i]);
> + if (ret < 0) {
> + /*
> + * Pairs are pushed one at a time. This pair
> + * already reached the hardware when a later
> + * pair is the one that failed.
> + */
> + if (i > pair_idx - 1)
> + goto err_restore_hw;
> + goto err_restore;
> + }
> + }
> + }
> + sysfs_notify(&dev->kobj, ally_button_names[btn_attr->button_id],
> + btn_attr->is_macro ? "macro" : "remap");
> + return count;
> +
> +err_restore_hw:
> + if (btn_attr->is_macro)
> + btn_map->macro = old_code;
> + else
> + btn_map->remap = old_code;
> +
> + /* Try to recover the mapping presented to the user. */
> + ally_set_button_mapping(hdev, pair);
> + goto err_out;
> +
> +err_restore:
> + if (btn_attr->is_macro)
> + btn_map->macro = old_code;
> + else
> + btn_map->remap = old_code;
> +
> +err_out:
> + hid_err(hdev, "Failed to set button mapping: %d\n", ret);
> + return ret;
> +}
[Severity: Low]
This isn't a bug, but button_remap_store() in drivers/hid/hid-asus.c mixes
scope-based cleanup via guard(mutex)(&ally_prefs.lock) with goto-based error
labels (err_restore_hw, err_restore, and err_out), and
ally_create_button_attributes() similarly mixes __free(kfree) on entries
with goto ally_btn_err_cleanup. Could these functions avoid mixing cleanup
helpers and goto error paths in the same routine?
[ ... ]
> @@ -2429,11 +3248,9 @@ static void ally_btn_turbo_init_attrs(struct ally_btn_sysfs_entry *entry)
>
> static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_device *hdev)
> {
> + const size_t entries_attr_index = ally_prefs.turbo_support ? 4 : 0;
> int i, ret;
>
> - if (!ally_prefs.turbo_support)
> - return NULL;
> -
> struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
> if (!entries)
> return ERR_PTR(-ENOMEM);
> @@ -2441,13 +3258,33 @@ static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_dev
> for (i = 0; i < ALLY_BTN_MAX; i++) {
> entries[i].btn = i;
>
> - if (!ally_btn_param(i)) {
> - hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
> - ret = -EINVAL;
> + if (ally_prefs.turbo_support) {
> + if (!ally_btn_param(i)) {
> + hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
> + ret = -EINVAL;
> + goto ally_btn_err_cleanup;
> + }
> +
> + ally_btn_turbo_init_attrs(&entries[i]);
> + }
> +
> + entries[i].remap_attr = button_remap_attr_create(i, false);
> + entries[i].macro_attr = button_remap_attr_create(i, true);
> + if (!entries[i].remap_attr || !entries[i].macro_attr) {
> + ret = -ENOMEM;
> goto ally_btn_err_cleanup;
> }
>
> - ally_btn_turbo_init_attrs(&entries[i]);
> + sysfs_attr_init(&entries[i].attr_remap_available.attr);
> + entries[i].attr_remap_available.attr.name = "remap_available";
> + entries[i].attr_remap_available.attr.mode = 0444;
> + entries[i].attr_remap_available.show = btn_remap_available_show;
> +
> + entries[i].attrs[entries_attr_index + 0] = &entries[i].remap_attr->dev_attr.attr;
> + entries[i].attrs[entries_attr_index + 1] = &entries[i].macro_attr->dev_attr.attr;
> + entries[i].attrs[entries_attr_index + 2] = &entries[i].attr_remap_available.attr;
> + entries[i].attrs[entries_attr_index + 3] = NULL;
> +
> entries[i].group.name = ally_button_names[i];
> entries[i].group.attrs = entries[i].attrs;
>
> @@ -2465,6 +3302,11 @@ static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_dev
> while (i--)
> sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
>
> + for (i = 0; i < ALLY_BTN_MAX; i++) {
> + kfree(entries[i].remap_attr);
> + kfree(entries[i].macro_attr);
> + }
> +
> return ERR_PTR(ret);
> }
>
> @@ -2477,11 +3319,19 @@ static void ally_remove_button_attributes(struct hid_device *hdev)
> if (!entries)
> return;
>
> - for (i = 0; i < ALLY_BTN_MAX; i++)
> + /*
> + * Unpublish first. A mode-store notify holds the prefs lock and must
> + * see either the live array or NULL.
> + */
> + ally_prefs.button_entries = NULL;
> +
> + for (i = 0; i < ALLY_BTN_MAX; i++) {
> sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
> + kfree(entries[i].remap_attr);
> + kfree(entries[i].macro_attr);
> + }
>
> kfree(entries);
> - ally_prefs.button_entries = NULL;
> }
[Severity: High]
Can this cause a lock ordering inversion and deadlock between
ally_prefs.lock and the sysfs kernfs active references on the button and
gamepad_mode attributes?
When userspace reads or writes the remap, macro, remap_available, or
gamepad_mode sysfs attributes, kernfs acquires an active reference on the
kernfs node before invoking button_remap_show(), button_remap_store(),
btn_remap_available_show(), or gamepad_mode_store(), which then acquire
ally_prefs.lock:
kernfs_fop_read_iter() / kernfs_fop_write_iter()
kernfs_get_active() /* acquires kn->active */
button_remap_show() / button_remap_store() /
btn_remap_available_show() / gamepad_mode_store()
guard(mutex)(&ally_prefs.lock); /* acquires ally_prefs.lock */
In the opposite direction, during device removal or error rollback in
ally_prefs_setup(), ally_prefs.lock is held while calling
sysfs_remove_group(), which drains active kernfs references:
ally_sysfs_remove() / ally_prefs_setup()
guard(mutex)(&ally_prefs.lock); /* acquires ally_prefs.lock */
ally_remove_button_attributes() / ally_create_button_attributes()
sysfs_remove_group()
__kernfs_remove()
kernfs_drain() /* waits for kn->active */
If a sysfs read or write is waiting to acquire ally_prefs.lock while
ally_sysfs_remove() or ally_prefs_setup() holds ally_prefs.lock and enters
kernfs_drain(), won't both threads block on each other?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261031174130.208240-1-denis.benato@linux.dev?part=12
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor
2026-10-31 17:41 ` [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor Denis Benato
@ 2026-10-31 18:04 ` sashiko-bot
2026-10-31 18:08 ` Denis Benato
2026-10-31 18:30 ` Rebecca Mara Müller
1 sibling, 1 reply; 29+ messages in thread
From: sashiko-bot @ 2026-10-31 18:04 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] HID: asus: wrong report ID in mcu_request_version() breaks MCU check
--
commit 95aa403a07769e0a4dbd2dee41abe2766cd2fe36
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: size feature reports from the HID report descriptor
This commit derives feature report transfer lengths from the HID report
descriptor via asus_report_len() instead of using hardcoded sizes across
asus_kbd_set_report(), asus_kbd_get_report(), mcu_request_version(), and
asus_xgm_led_set().
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index f6aa1be09ad53..327bb4ab1a0d6 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> @@ -900,10 +992,15 @@ static int mcu_parse_version_string(const u8 *response, size_t response_size)
>
> static int mcu_request_version(struct hid_device *hdev)
> {
> - u8 *response __free(kfree) = kzalloc(ROG_ALLY_REPORT_SIZE, GFP_KERNEL);
> - const u8 request[] = { 0x5a, 0x05, 0x03, 0x31, 0x00, 0x20 };
> + size_t hw_len = asus_report_len(hdev, FEATURE_REPORT_ID);
> + const u8 request[] = { FEATURE_KBD_REPORT_ID, 0x05, 0x03, 0x31, 0x00, 0x20 };
> int ret;
>
> + if (!hw_len)
> + return -EINVAL;
> +
> + u8 *response __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
> +
> if (!response)
> return -ENOMEM;
>
> @@ -911,17 +1008,16 @@ static int mcu_request_version(struct hid_device *hdev)
> if (ret < 0)
> return ret;
>
> - ret = hid_hw_raw_request(hdev, FEATURE_REPORT_ID, response,
> - ROG_ALLY_REPORT_SIZE, HID_FEATURE_REPORT,
> - HID_REQ_GET_REPORT);
> + ret = hid_hw_raw_request(hdev, FEATURE_REPORT_ID, response, hw_len,
> + HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
> if (ret < 0)
> return ret;
[Severity: High]
Does asus_report_len(hdev, FEATURE_REPORT_ID) return 0 here on ROG Ally and
ROG Ally X devices?
In drivers/hid/hid-asus.c, FEATURE_REPORT_ID (0x0d) is the I2C touchpad
multitouch report ID used by asus_start_multitouch(), whereas the ROG Ally
N-Key vendor HID interface declares feature report 0x5a
(FEATURE_KBD_REPORT_ID) in its HID report descriptor (and as documented above
mcu_parse_version_string(), the MCU response payload starts with "5a 05 03 31
00 1a 13").
Previously, mcu_request_version() passed a hardcoded ROG_ALLY_REPORT_SIZE (64)
to hid_hw_raw_request(), which succeeded because usbhid_get_raw_report() does
not look up the report ID in hdev->report_enum[HID_FEATURE_REPORT].
With this change, when probing USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY or
USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X via:
asus_probe()
-> asus_kbd_register_leds()
-> validate_mcu_fw_version()
-> mcu_request_version()
looking up FEATURE_REPORT_ID (0x0d) in hdev->report_enum[HID_FEATURE_REPORT]
via asus_report_len() will not find a report and will return 0, causing
mcu_request_version() to return -EINVAL. validate_mcu_fw_version() then
silently aborts without checking the MCU version or configuring MCU powersave:
version = mcu_request_version(hdev);
if (version < 0)
return;
Should mcu_request_version() look up FEATURE_KBD_REPORT_ID instead of
FEATURE_REPORT_ID when sizing the buffer with asus_report_len()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261031174130.208240-1-denis.benato@linux.dev?part=1
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor
2026-10-31 18:04 ` sashiko-bot
@ 2026-10-31 18:08 ` Denis Benato
0 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 18:08 UTC (permalink / raw)
To: sashiko-reviews; +Cc: linux-input, dmitry.torokhov
On 10/31/26 19:04, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [High] HID: asus: wrong report ID in mcu_request_version() breaks MCU check
> --
>
> commit 95aa403a07769e0a4dbd2dee41abe2766cd2fe36
> Author: Denis Benato <denis.benato@linux.dev>
>
> HID: asus: size feature reports from the HID report descriptor
>
> This commit derives feature report transfer lengths from the HID report
> descriptor via asus_report_len() instead of using hardcoded sizes across
> asus_kbd_set_report(), asus_kbd_get_report(), mcu_request_version(), and
> asus_xgm_led_set().
>
>> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
>> index f6aa1be09ad53..327bb4ab1a0d6 100644
>> --- a/drivers/hid/hid-asus.c
>> +++ b/drivers/hid/hid-asus.c
> [ ... ]
>> @@ -900,10 +992,15 @@ static int mcu_parse_version_string(const u8 *response, size_t response_size)
>>
>> static int mcu_request_version(struct hid_device *hdev)
>> {
>> - u8 *response __free(kfree) = kzalloc(ROG_ALLY_REPORT_SIZE, GFP_KERNEL);
>> - const u8 request[] = { 0x5a, 0x05, 0x03, 0x31, 0x00, 0x20 };
>> + size_t hw_len = asus_report_len(hdev, FEATURE_REPORT_ID);
>> + const u8 request[] = { FEATURE_KBD_REPORT_ID, 0x05, 0x03, 0x31, 0x00, 0x20 };
>> int ret;
>>
>> + if (!hw_len)
>> + return -EINVAL;
>> +
>> + u8 *response __free(kfree) = kzalloc(hw_len, GFP_KERNEL);
>> +
>> if (!response)
>> return -ENOMEM;
>>
>> @@ -911,17 +1008,16 @@ static int mcu_request_version(struct hid_device *hdev)
>> if (ret < 0)
>> return ret;
>>
>> - ret = hid_hw_raw_request(hdev, FEATURE_REPORT_ID, response,
>> - ROG_ALLY_REPORT_SIZE, HID_FEATURE_REPORT,
>> - HID_REQ_GET_REPORT);
>> + ret = hid_hw_raw_request(hdev, FEATURE_REPORT_ID, response, hw_len,
>> + HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
>> if (ret < 0)
>> return ret;
> [Severity: High]
> Does asus_report_len(hdev, FEATURE_REPORT_ID) return 0 here on ROG Ally and
> ROG Ally X devices?
>
> In drivers/hid/hid-asus.c, FEATURE_REPORT_ID (0x0d) is the I2C touchpad
> multitouch report ID used by asus_start_multitouch(), whereas the ROG Ally
> N-Key vendor HID interface declares feature report 0x5a
> (FEATURE_KBD_REPORT_ID) in its HID report descriptor (and as documented above
> mcu_parse_version_string(), the MCU response payload starts with "5a 05 03 31
> 00 1a 13").
>
> Previously, mcu_request_version() passed a hardcoded ROG_ALLY_REPORT_SIZE (64)
> to hid_hw_raw_request(), which succeeded because usbhid_get_raw_report() does
> not look up the report ID in hdev->report_enum[HID_FEATURE_REPORT].
>
> With this change, when probing USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY or
> USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X via:
>
> asus_probe()
> -> asus_kbd_register_leds()
> -> validate_mcu_fw_version()
> -> mcu_request_version()
>
> looking up FEATURE_REPORT_ID (0x0d) in hdev->report_enum[HID_FEATURE_REPORT]
> via asus_report_len() will not find a report and will return 0, causing
> mcu_request_version() to return -EINVAL. validate_mcu_fw_version() then
> silently aborts without checking the MCU version or configuring MCU powersave:
>
> version = mcu_request_version(hdev);
> if (version < 0)
> return;
>
> Should mcu_request_version() look up FEATURE_KBD_REPORT_ID instead of
> FEATURE_REPORT_ID when sizing the buffer with asus_report_len()?
No, this was reported by my sashiko-bot too and attempting to follow the suggestion
has lead to driver failing and I had to use what's in there now.
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds
2026-10-31 17:41 ` [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-10-31 18:11 ` sashiko-bot
0 siblings, 0 replies; 29+ messages in thread
From: sashiko-bot @ 2026-10-31 18:11 UTC (permalink / raw)
To: Denis Benato; +Cc: linux-input, dmitry.torokhov
Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] HID: asus: stuck vendor keys in ally_vendor_keys() on button release
- [High] HID: asus: use-after-free in asus_resume() during ally_intf teardown
- [High] HID: asus: UAF and lifecycle inversion in ally_quiesce() and ally_free()
- [Medium] HID: asus: ally_dev_get_report() sizes 0x0D read from 0x5A report length
- [Medium] HID: asus: duplicate gamepad input_dev registered due to mapping report 0x0B
- [Low] HID: asus: pre-setting mapped keybits in asus_input_mapping() unmaps usages
--
commit 6c3f0919161f970328ba824fb01ea511f2881f2b
Author: Denis Benato <denis.benato@linux.dev>
HID: asus: add support for ROG Ally handhelds
This commit adds core HID support for ASUS ROG Ally handheld devices,
including device readiness initialization, Ally X gamepad input handling,
vendor button handling, and Ctrl+Alt+Del long-press filtering.
> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 327bb4ab1a0d6..552d3b25a1d10 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c
[ ... ]
> @@ -239,12 +286,761 @@ static const struct asus_touchpad_info medion_e1239t_tp = {
[ ... ]
> +static void ally_quiesce(struct ally_intf *intf)
> +{
> + if (!intf)
> + return;
> +
> + cancel_delayed_work_sync(&intf->resume_work);
> +}
> +
> +static void ally_free(struct ally_intf *intf)
> +{
> + if (!intf)
> + return;
> +
> + if (intf->gamepad)
> + input_unregister_device(intf->gamepad);
> + kfree(intf);
> +}
[Severity: High]
In asus_remove() and the err_stop_hw path of asus_probe(), ally_quiesce() is
called before hid_hw_stop(hdev), while ally_free() is called after
hid_hw_stop(hdev) without clearing drvdata->ally:
if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
ally_quiesce(drvdata->ally);
asus_worker_stop(drvdata->worker);
hid_hw_stop(hdev);
if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD)
ally_free(drvdata->ally);
Could this teardown sequence cause a use-after-free on intf and intf->input,
as well as invert the hid_hw_open() / hid_hw_close() lifetime relative to
hid_hw_stop()?
First, intf->gamepad is registered with ally_x_input_open() and
ally_x_input_close(), which call hid_hw_open(intf->hdev) and
hid_hw_close(intf->hdev). Because intf->gamepad is only unregistered in
ally_free() after hid_hw_stop(hdev) has already stopped the low-level
transport, an open intf->gamepad device will call hid_hw_close() after
hid_hw_stop(), or userspace can open intf->gamepad and call hid_hw_open()
after hid_hw_stop() has completed.
Second, hid_device_remove() leaves hdev->driver set until after asus_remove()
returns, and hid_bus_type does not lock or runtime-resume the parent USB
interface during driver release. If a runtime resume occurs after
ally_quiesce() runs cancel_delayed_work_sync(&intf->resume_work) (for example
via usbhid_open() -> usb_autopm_get_interface() when intf->gamepad is opened,
or via activity on another interface of the composite USB device),
asus_resume() will still see the non-NULL drvdata->ally pointer and call
schedule_delayed_work(&intf->resume_work, ...). That re-queues resume_work
right before hid_hw_stop() frees intf->input and ally_free() frees intf, or
dereferences intf after kfree(intf).
Should intf->gamepad be unregistered before hid_hw_stop(hdev), and should
resume_work be disabled with disable_delayed_work_sync(&intf->resume_work)
(along with clearing drvdata->ally) before stopping the hardware and freeing
intf?
[ ... ]
> +static bool ally_vendor_keys(struct input_dev *input, const u8 *data, int size)
> +{
> + if (size < 2 || data[0] != FEATURE_KBD_REPORT_ID)
> + return false;
> +
> + if (data[1] != 0x38 && data[1] != 0xA6 && data[1] != 0xA7 &&
> + data[1] != 0xA8)
> + return false;
> +
> + if (!input)
> + return true;
> +
> + input_report_key(input, KEY_PROG1, data[1] == 0x38);
> + input_report_key(input, KEY_F16, data[1] == 0xA6);
> + input_report_key(input, KEY_F17, data[1] == 0xA7);
> + input_report_key(input, KEY_F18, data[1] == 0xA8);
> + input_report_key(input, KEY_F19, data[1] == 0x38);
> + input_sync(input);
> + return true;
> +}
[Severity: High]
When one of the ROG Ally vendor buttons (0x38, 0xA6, 0xA7, or 0xA8) is
pressed and then released, will the key remain stuck down and autorepeat
indefinitely?
On button press, the device sends report FEATURE_KBD_REPORT_ID (0x5A) with
data[1] set to the button code. ally_vendor_keys() reports the key press on
input and returns true, which causes asus_raw_event() to return -1. Because
asus_raw_event() returns -1, __hid_input_report() returns before
hid_report_raw_event() runs, so hid-core never records the pressed usage in
field->value[0] (and KEY_F19 is not mapped by hid-core at all).
When the button is released, the device sends report 0x5A with
data[1] == 0x00. ally_vendor_keys() checks whether data[1] is 0x38, 0xA6,
0xA7, or 0xA8, so it returns false on the release report before reaching the
input_report_key(..., 0) calls. The release report then falls through to
hid_report_raw_event() -> hid_input_array_field(), which sees the previous
field->value[0] still at 0 and emits no key release event either.
[ ... ]
> +static int ally_dev_set_report(struct hid_device *hdev, const u8 *buf, size_t len)
> +{
> + return asus_kbd_set_report(hdev, buf, len);
> +}
> +
> +static int ally_dev_get_report(struct hid_device *hdev, u8 *out, size_t len)
> +{
> + return hid_hw_raw_request(hdev, HID_ALLY_GET_REPORT_ID, out, len,
> + HID_FEATURE_REPORT, HID_REQ_GET_REPORT);
> +}
> +
> +/**
> + * ally_gamepad_send_packet() - Send a raw packet to the gamepad device
> + * @hdev: HID device
> + * @buf: buffer containing the packet data
> + * @len: length of data to send
> + *
> + * Return: count of data transferred, negative if error.
> + */
> +static int ally_gamepad_send_packet(struct hid_device *hdev, const u8 *buf, size_t len)
> +{
> + return ally_dev_set_report(hdev, buf, len);
> +}
> +
> +/**
> + * ally_gamepad_send_receive_packet() - Send a packet and receive the response
> + * @hdev: HID device
> + * @buf: buffer containing the packet data to send and receive response in
> + * @len: length of buffer
> + *
> + * Return: 0 on success, negative errno on failure.
> + */
> +static int ally_gamepad_send_receive_packet(struct hid_device *hdev,
> + u8 *buf, size_t len)
> +{
> + int ret;
> +
> + ret = ally_dev_set_report(hdev, buf, len);
> + if (ret >= 0) {
> + memset(buf, 0, len);
> + ret = ally_dev_get_report(hdev, buf, len);
> + }
> +
> + return ret;
> +}
[Severity: Medium]
As reported by Sashiko on v11 [1], ally_alloc_cmd() sizes its buffer from
FEATURE_KBD_REPORT_ID (0x5A), and ally_gamepad_send_receive_packet() passes
that length to ally_dev_get_report() to read HID_ALLY_GET_REPORT_ID (0x0D)
directly via hid_hw_raw_request().
Why does ally_dev_get_report() call hid_hw_raw_request() directly with the
caller's buffer size instead of using asus_kbd_get_report(hdev,
HID_ALLY_GET_REPORT_ID, out, len), which sizes the transfer from the report
descriptor for HID_ALLY_GET_REPORT_ID and returns 0 on success as documented
by ally_gamepad_send_receive_packet()?
[ ... ]
> @@ -512,6 +1308,21 @@ static int asus_raw_event(struct hid_device *hdev,
> if (drvdata->quirks & QUIRK_MEDION_E1239T)
> return asus_e1239t_event(drvdata, data, size);
>
> + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
> + struct ally_intf *intf;
> +
> + /*
> + * Return -1 to suppress further processing by the generic HID
> + * input parser for reports we fully handle. The interface is
> + * published with smp_store_release() only after its fields
> + * are set. Letting report 0x0B reach the default parser
> + * creates a generic gamepad and Steam Input overlaps.
> + */
> + intf = smp_load_acquire(&drvdata->ally); /* fields are published */
> + if (hid_asus_ally_raw_event(hdev, intf, data, size))
> + return -1;
> + }
[Severity: Medium]
The comment here notes that letting report 0x0B (HID_ALLY_X_INPUT_REPORT)
reach the default parser creates a generic gamepad that overlaps with Steam
Input. However, returning -1 from asus_raw_event() only stops runtime events
from being processed after the device is already registered.
During asus_probe(), hid_hw_start(hdev, HID_CONNECT_DEFAULT) runs
hidinput_connect() -> hidinput_configure_usages() -> asus_input_mapping()
before hid_asus_ally_probe() is called. Because asus_input_mapping() returns 0
for the standard Generic Desktop and Button usages in report 0x0B, doesn't
hidinput_connect() still map report 0x0B and register a duplicate, dead
generic gamepad input_dev alongside the custom intf->gamepad device registered
by ally_x_setup_input()?
Should asus_input_mapping() return -1 when QUIRK_ROG_ALLY_XPAD is set and
field->report->id == HID_ALLY_X_INPUT_REPORT so that hid-input does not map
report 0x0B at all?
[ ... ]
> @@ -1415,6 +2129,25 @@ static int asus_input_mapping(struct hid_device *hdev,
> return -1;
> }
>
> + /*
> + * Declare the Ally vendor keys before this input is registered.
> + * KEY_F19 is the second event of the ROG key (usage 0x38).
> + */
> + if (drvdata->quirks & QUIRK_ROG_ALLY_XPAD) {
> + switch (usage->hid & HID_USAGE) {
> + case 0x38:
> + case 0xa6:
> + case 0xa7:
> + case 0xa8:
> + input_set_capability(hi->input, EV_KEY, KEY_PROG1);
> + input_set_capability(hi->input, EV_KEY, KEY_F16);
> + input_set_capability(hi->input, EV_KEY, KEY_F17);
> + input_set_capability(hi->input, EV_KEY, KEY_F18);
> + input_set_capability(hi->input, EV_KEY, KEY_F19);
> + break;
> + }
> + }
> +
> set_bit(EV_REP, hi->input->evbit);
> return 1;
> }
[Severity: Low]
In asus_input_mapping(), usages 0x38, 0xa6, 0xa7, and 0xa8 first call
asus_map_key_clear(), which calls hid_map_usage_clear() to set usage->code
and clear that bit in hi->input->keybit. Calling input_set_capability()
immediately afterwards sets KEY_PROG1, KEY_F16, KEY_F17, and KEY_F18 in
hi->input->keybit before asus_input_mapping() returns 1.
When asus_input_mapping() returns 1 to hidinput_configure_usage() in
drivers/hid/hid-input.c:
while (usage->code <= max && test_and_set_bit(usage->code, bit)) {
if (device->quirks & HID_QUIRK_INCREMENT_USAGE_ON_DUPLICATE) {
usage->code = find_next_zero_bit(bit,
max + 1,
usage->code);
} else {
device->status |= HID_STAT_DUP_DETECTED;
goto ignore;
}
}
test_and_set_bit(usage->code, bit) sees the bit already set, flags
HID_STAT_DUP_DETECTED, and jumps to ignore:, which zeroes usage->type and
usage->code. If hid_asus_ally_probe() fails or returns NULL on an interface
and the device falls back to generic HID handling, those vendor usages end up
unmapped.
Would it be better to only call input_set_capability(hi->input, EV_KEY,
KEY_F19) for the extra unmapped key here, and let hidinput_configure_usage()
set the mapped usage bits itself?
[1] https://lore.kernel.org/r/20261025235232.129945-16-denis.benato@linux.dev
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261031174130.208240-1-denis.benato@linux.dev?part=2
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor
2026-10-31 17:41 ` [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor Denis Benato
2026-10-31 18:04 ` sashiko-bot
@ 2026-10-31 18:30 ` Rebecca Mara Müller
1 sibling, 0 replies; 29+ messages in thread
From: Rebecca Mara Müller @ 2026-10-31 18:30 UTC (permalink / raw)
To: Denis Benato
Cc: Paolo Pisati, linux-kernel, linux-input, platform-driver-x86,
kernel, Benjamin Tissoires, Jiri Kosina, Luke D . Jones,
Mateusz Schwartz, Denis Benato, Jonathan LoBue, Khamunetri Clark,
Derek J . Clark, Joshua Leivenzon, Nathan Chancellor,
Ivan Levchenko, Corentin Chary, Hans de Goede, Ilpo Järvinen
Hi Denis,
as asked in the cover letter, I repeated the Zenbook Duo test on v12.
Everything that worked with v11 still works.
Setup as for v11: hid.git for-next (hid-asus.c f6aa1be09ad5), your
v12 01-12, then Paolo's v4 1-4 and 6-7, leaving out 5/7, which this
patch replaces. Your patches applied cleanly, and Paolo's went on top
of the reworked 01/17 without conflicts (4/7 with the same trivial
fixup as last time). Built out-of-tree with W=1 against a 7.2.6-based
distribution kernel, no warnings from hid-asus.c. UX8407AA, my
userspace workaround stopped.
- Docked (USB, 0b05:1cd7), captured with usbmon while reloading the
module and pressing the backlight key: 5 SET and 2 GET, every one of
them report 0x5a on interface 4 with wLength 16, all completed with
status 0. It is the same sequence as with v11: the handshake and its
read-back, the 0x05 function query and its GET, the initial
brightness and two changes. The backlight key and Fn+F (platform
profile) work.
- Bluetooth (0b05:1cd8): the backlight key and Fn+F work, no errors
in the log.
- After docking again: the backlight key and Fn+F still work.
Tested-by: Rebecca Mara Müller <rebecca.mara@posteo.de> # UX8407AA
One small thing I noticed while applying: 06/17 adds 14 lines that
contain only a single space, and git am warns about trailing
whitespace. checkpatch will probably flag them too.
Thanks,
Rebecca
^ permalink raw reply [flat|nested] 29+ messages in thread
* Re: [PATCH v12 12/17] HID: asus: add support for btn remapping
2026-10-31 18:00 ` sashiko-bot
@ 2026-10-31 19:06 ` Denis Benato
0 siblings, 0 replies; 29+ messages in thread
From: Denis Benato @ 2026-10-31 19:06 UTC (permalink / raw)
To: sashiko-reviews; +Cc: linux-input, dmitry.torokhov
On 10/31/26 19:00, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
> - [High] HID: asus: ABBA deadlock between ally_prefs.lock and sysfs active refs
> - [Low] HID: asus: mixed cleanup helpers and goto in button_remap_store()
> - [Low] HID: asus: commit message refers to remap_index instead of remap_available
> --
>
> commit eab19113fde5452095dfc729f53ab98ff0fc3365
> Author: Denis Benato <denis.benato@linux.dev>
>
> HID: asus: add support for btn remapping
>
> This commit adds button and macro remapping support for ASUS ROG Ally
> devices via per-button sysfs attributes (remap, macro, and
> remap_available), validating mappings against the active gamepad mode and
> restoring or refreshing the cached mappings across mode switches and MCU
> resets.
>
>> HID: asus: add support for btn remapping
>>
>> Add support for buttons remapping: the ability to assign different
>> button events to each one of the physical button on the device.
>>
>> Each button exposes a remap_index sysfs attribute listing the valid
>> remap target names for the current gamepad mode: M1 and M2 can be
>> remapped to anything at any time, while other buttons may only be
>> remapped to gamepad codes in gamepad mode and keyboard/mouse/media
>> codes in desktop mode.
>>
>> When the gamepad mode is changed the firmware automatically restores
>> its internal defaults, so the software cache is refreshed to reflect
>> the hardware state.
>>
>> Co-developed-by: Luke Jones <luke@ljones.dev>
>> Signed-off-by: Luke Jones <luke@ljones.dev>
>> Signed-off-by: Denis Benato <denis.benato@linux.dev>
>> Signed-off-by: Jonathan LoBue <jlobue10@gmail.com>
> [Severity: Low]
> This isn't a bug, but the commit message refers to a remap_index sysfs
> attribute, whereas ally_create_button_attributes() in drivers/hid/hid-asus.c
> registers this attribute as remap_available alongside the remap and macro
> attributes created in button_remap_attr_create(). Should the commit message
> be updated to mention remap, macro, and remap_available instead of
> remap_index?
Not good. And now that I think about it I also forgot the documentation entry.
>> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
>> index d20e433af5740..b94cb8c50b28b 100644
>> --- a/drivers/hid/hid-asus.c
>> +++ b/drivers/hid/hid-asus.c
> [ ... ]
>
>> +static ssize_t button_remap_store(struct device *dev,
>> + struct device_attribute *attr,
>> + const char *buf, size_t count)
>> +{
>> + struct hid_device *hdev = to_hid_device(dev);
>> + struct button_remap_attr *btn_attr = to_button_remap_attr(attr);
>> + enum ally_button_id button_id = btn_attr->button_id;
>> + struct ally_button_mapping *mode_map;
>> + const struct btn_code_map *code;
>> + const struct btn_code_map *old_code;
>> + enum btn_pair_index pair_idx;
>> + struct button_pair_map *pair;
>> + struct button_map *btn_map;
>> + char btn_name[32];
>> + bool is_first;
>> + int ret, i;
>> +
>> + if (sscanf(buf, "%31s", btn_name) != 1)
>> + return -EINVAL;
>> +
>> + code = find_button_by_name(btn_name);
>> + if (!code)
>> + return -EINVAL;
>> +
>> + ret = get_button_pair_info(button_id, &pair_idx, &is_first);
>> + if (ret < 0)
>> + return ret;
>> +
>> + guard(mutex)(&ally_prefs.lock);
>> +
>> + /*
>> + * Validate the remap target while holding the lock: the
>> + * gamepad mode can change concurrently, and the check must
>> + * be done against the very mode the mapping is written to.
>> + */
>> + if (!ally_remap_code_valid(button_id, ally_prefs.gamepad_mode, code,
>> + btn_attr->is_macro))
>> + return -EINVAL;
>> +
>> + /* Access the mapping for current gamepad mode */
>> + pair = &ally_prefs.button_mappings.button_pairs[pair_idx - 1];
>> + btn_map = is_first ? &pair->first : &pair->second;
>> +
>> + if (btn_attr->is_macro) {
>> + old_code = btn_map->macro;
>> + btn_map->macro = code;
>> + } else {
>> + old_code = btn_map->remap;
>> + btn_map->remap = code;
>> + }
>> +
>> + /* Update pair index */
>> + pair->pair_index = pair_idx;
>> +
>> + /*
>> + * Send mapping to device with the caveat that first
>> + * generation devices require individual button pair updates.
>> + */
>> + if (ally_prefs.is_ally_x) {
>> + ret = ally_set_button_mapping(hdev, pair);
>> + if (ret < 0)
>> + goto err_restore;
>> + } else {
>> + mode_map = &ally_prefs.button_mappings;
>> + for (i = 0; i < ARRAY_SIZE(mode_map->button_pairs); i++) {
>> + mode_map->button_pairs[i].pair_index = i + 1;
>> + ret = ally_set_button_mapping(hdev,
>> + &mode_map->button_pairs[i]);
>> + if (ret < 0) {
>> + /*
>> + * Pairs are pushed one at a time. This pair
>> + * already reached the hardware when a later
>> + * pair is the one that failed.
>> + */
>> + if (i > pair_idx - 1)
>> + goto err_restore_hw;
>> + goto err_restore;
>> + }
>> + }
>> + }
>> + sysfs_notify(&dev->kobj, ally_button_names[btn_attr->button_id],
>> + btn_attr->is_macro ? "macro" : "remap");
>> + return count;
>> +
>> +err_restore_hw:
>> + if (btn_attr->is_macro)
>> + btn_map->macro = old_code;
>> + else
>> + btn_map->remap = old_code;
>> +
>> + /* Try to recover the mapping presented to the user. */
>> + ally_set_button_mapping(hdev, pair);
>> + goto err_out;
>> +
>> +err_restore:
>> + if (btn_attr->is_macro)
>> + btn_map->macro = old_code;
>> + else
>> + btn_map->remap = old_code;
>> +
>> +err_out:
>> + hid_err(hdev, "Failed to set button mapping: %d\n", ret);
>> + return ret;
>> +}
> [Severity: Low]
> This isn't a bug, but button_remap_store() in drivers/hid/hid-asus.c mixes
> scope-based cleanup via guard(mutex)(&ally_prefs.lock) with goto-based error
> labels (err_restore_hw, err_restore, and err_out), and
> ally_create_button_attributes() similarly mixes __free(kfree) on entries
> with goto ally_btn_err_cleanup. Could these functions avoid mixing cleanup
> helpers and goto error paths in the same routine?
>
> [ ... ]
>
>> @@ -2429,11 +3248,9 @@ static void ally_btn_turbo_init_attrs(struct ally_btn_sysfs_entry *entry)
>>
>> static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_device *hdev)
>> {
>> + const size_t entries_attr_index = ally_prefs.turbo_support ? 4 : 0;
>> int i, ret;
>>
>> - if (!ally_prefs.turbo_support)
>> - return NULL;
>> -
>> struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
>> if (!entries)
>> return ERR_PTR(-ENOMEM);
>> @@ -2441,13 +3258,33 @@ static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_dev
>> for (i = 0; i < ALLY_BTN_MAX; i++) {
>> entries[i].btn = i;
>>
>> - if (!ally_btn_param(i)) {
>> - hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
>> - ret = -EINVAL;
>> + if (ally_prefs.turbo_support) {
>> + if (!ally_btn_param(i)) {
>> + hid_err(hdev, "Invalid button id %d for turbo attributes\n", i);
>> + ret = -EINVAL;
>> + goto ally_btn_err_cleanup;
>> + }
>> +
>> + ally_btn_turbo_init_attrs(&entries[i]);
>> + }
>> +
>> + entries[i].remap_attr = button_remap_attr_create(i, false);
>> + entries[i].macro_attr = button_remap_attr_create(i, true);
>> + if (!entries[i].remap_attr || !entries[i].macro_attr) {
>> + ret = -ENOMEM;
>> goto ally_btn_err_cleanup;
>> }
>>
>> - ally_btn_turbo_init_attrs(&entries[i]);
>> + sysfs_attr_init(&entries[i].attr_remap_available.attr);
>> + entries[i].attr_remap_available.attr.name = "remap_available";
>> + entries[i].attr_remap_available.attr.mode = 0444;
>> + entries[i].attr_remap_available.show = btn_remap_available_show;
>> +
>> + entries[i].attrs[entries_attr_index + 0] = &entries[i].remap_attr->dev_attr.attr;
>> + entries[i].attrs[entries_attr_index + 1] = &entries[i].macro_attr->dev_attr.attr;
>> + entries[i].attrs[entries_attr_index + 2] = &entries[i].attr_remap_available.attr;
>> + entries[i].attrs[entries_attr_index + 3] = NULL;
>> +
>> entries[i].group.name = ally_button_names[i];
>> entries[i].group.attrs = entries[i].attrs;
>>
>> @@ -2465,6 +3302,11 @@ static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_dev
>> while (i--)
>> sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
>>
>> + for (i = 0; i < ALLY_BTN_MAX; i++) {
>> + kfree(entries[i].remap_attr);
>> + kfree(entries[i].macro_attr);
>> + }
>> +
>> return ERR_PTR(ret);
>> }
>>
>> @@ -2477,11 +3319,19 @@ static void ally_remove_button_attributes(struct hid_device *hdev)
>> if (!entries)
>> return;
>>
>> - for (i = 0; i < ALLY_BTN_MAX; i++)
>> + /*
>> + * Unpublish first. A mode-store notify holds the prefs lock and must
>> + * see either the live array or NULL.
>> + */
>> + ally_prefs.button_entries = NULL;
>> +
>> + for (i = 0; i < ALLY_BTN_MAX; i++) {
>> sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
>> + kfree(entries[i].remap_attr);
>> + kfree(entries[i].macro_attr);
>> + }
>>
>> kfree(entries);
>> - ally_prefs.button_entries = NULL;
>> }
> [Severity: High]
> Can this cause a lock ordering inversion and deadlock between
> ally_prefs.lock and the sysfs kernfs active references on the button and
> gamepad_mode attributes?
>
> When userspace reads or writes the remap, macro, remap_available, or
> gamepad_mode sysfs attributes, kernfs acquires an active reference on the
> kernfs node before invoking button_remap_show(), button_remap_store(),
> btn_remap_available_show(), or gamepad_mode_store(), which then acquire
> ally_prefs.lock:
>
> kernfs_fop_read_iter() / kernfs_fop_write_iter()
> kernfs_get_active() /* acquires kn->active */
> button_remap_show() / button_remap_store() /
> btn_remap_available_show() / gamepad_mode_store()
> guard(mutex)(&ally_prefs.lock); /* acquires ally_prefs.lock */
>
> In the opposite direction, during device removal or error rollback in
> ally_prefs_setup(), ally_prefs.lock is held while calling
> sysfs_remove_group(), which drains active kernfs references:
>
> ally_sysfs_remove() / ally_prefs_setup()
> guard(mutex)(&ally_prefs.lock); /* acquires ally_prefs.lock */
> ally_remove_button_attributes() / ally_create_button_attributes()
> sysfs_remove_group()
> __kernfs_remove()
> kernfs_drain() /* waits for kn->active */
>
> If a sysfs read or write is waiting to acquire ally_prefs.lock while
> ally_sysfs_remove() or ally_prefs_setup() holds ally_prefs.lock and enters
> kernfs_drain(), won't both threads block on each other?
>
^ permalink raw reply [flat|nested] 29+ messages in thread
end of thread, other threads:[~2026-10-31 19:06 UTC | newest]
Thread overview: 29+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-31 17:41 [PATCH v12 00/17] HID: asus: add support for ROG Ally handhelds Denis Benato
2026-10-31 17:41 ` [PATCH v12 01/17] HID: asus: size feature reports from the HID report descriptor Denis Benato
2026-10-31 18:04 ` sashiko-bot
2026-10-31 18:08 ` Denis Benato
2026-10-31 18:30 ` Rebecca Mara Müller
2026-10-31 17:41 ` [PATCH v12 02/17] HID: asus: add support for ROG Ally handhelds Denis Benato
2026-10-31 18:11 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 03/17] HID: asus: add gamepad configuration Denis Benato
2026-10-31 17:59 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 04/17] HID: asus: add vibration strength configuration Denis Benato
2026-10-31 17:41 ` [PATCH v12 05/17] HID: asus: add joysticks inner and outer range configuration Denis Benato
2026-10-31 17:57 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 06/17] HID: asus: add triggers " Denis Benato
2026-10-31 17:54 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 07/17] HID: asus: add joysticks anti-deadzone configuration Denis Benato
2026-10-31 17:51 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 08/17] HID: asus: add support for response curve Denis Benato
2026-10-31 17:41 ` [PATCH v12 09/17] HID: asus: add support to force feedback Denis Benato
2026-10-31 17:41 ` [PATCH v12 10/17] HID: asus: add support for gamepad mode Denis Benato
2026-10-31 17:41 ` [PATCH v12 11/17] HID: asus: add support for turbo buttons Denis Benato
2026-10-31 17:54 ` sashiko-bot
2026-10-31 17:41 ` [PATCH v12 12/17] HID: asus: add support for btn remapping Denis Benato
2026-10-31 18:00 ` sashiko-bot
2026-10-31 19:06 ` Denis Benato
2026-10-31 17:41 ` [PATCH v12 13/17] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
2026-10-31 17:41 ` [PATCH v12 14/17] HID: asus: expose XG Mobile identity and firmware information Denis Benato
2026-10-31 17:41 ` [PATCH v12 15/17] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
2026-10-31 17:41 ` [PATCH v12 16/17] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
2026-10-31 17:41 ` [PATCH v12 17/17] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.