Linux Input/HID development
 help / color / mirror / Atom feed
* [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds
@ 2026-11-04  1:39 Denis Benato
  2026-11-04  1:39 ` [PATCH v13 01/18] HID: asus: size feature reports from the HID report descriptor Denis Benato
                   ` (17 more replies)
  0 siblings, 18 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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/
Link v12: https://lore.kernel.org/all/20261031174130.208240-1-denis.benato@linux.dev/

------------------------------------------------------------------------------------------------

The 13th version is mostly structural work based on the v12 comments, plus one
new patch: the ROG Ally sysfs attributes are now documented in
Documentation/ABI/testing, placed right after the commit that creates them and
before MAINTAINERS... I had it by v12 but forgot to send it... I even answered
the bot pointing to it.

Limited structural changes: mainly ctrl+alt+del handling and AC/CC presses
going to hid core to make things easier to follow and spare some lines.

With usage of macros to compress duplicate code I managed to reduce lines of
code by more than a thousand while also increasing the number of (hopefully)
useful comments and kernel-doc.

The rest is some [Low] feedbacks from sashiko I decided was worth doing.

When I sent v12 I had changed gamepad_mode_index to gamepad_mode_available
to follow bot suggestion: it was a mistake that broke the simmetry with
other handhelds so I switched back to _index.

Also from my local review I was stupid enough to convert a bunch of
scoped_guard to a guard and I introduced a possible deadlock... So
I have reworked that and now sashiko should (hopefully) be happy
and not find other synchronization issues.

This time I used glm-5.3 with sashiko and it is flagging the cross-ally
talk of ally_prefs as Medium, but it is actually intended to be that
way and is explained in the code why it is made the way it is.

The previous sashiko-bot on 01/18 run flagged a feature report misalignment,
I said I wouldn't have changed that, but I did because I misunderstood what
it was telling me: it is now that will flag an issue about query the length
of report 0x5a and sending something else: attempting to fix that will break
the driver.

Hopefully this is the last revision.. I hope I haven't missed anything
else and there are no more weird issues with the mutex.

------------------------------------------------------------------------------------------------

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

- v13
  - HID: asus: size feature reports from the HID report descriptor
    - mcu_request_version() names the keyboard report FEATURE_KBD_REPORT_ID
    - Tested-by added in commit message
  - HID: asus: add support for ROG Ally handhelds
    - Drop the linux/types.h include added in v12
    - KEY_F19 dropped: it was a leftover that went unnoticed
    - ally_vendor_keys() consumes only the vendor codes (0x38, 0xA6, 0xA7
      and 0xA8): the 0x00 report that releases the keys reaches hid-core,
      which can only release the usages it saw pressed.
    - Updated the kernel-doc for the AC and CC short and long press codes
    - handle_ctrl_alt_del() requires the four bytes it reads
    - ally_quiesce() disables the resume work instead of only cancelling
      it, so a concurrent asus_resume() cannot re-queue after the cancel
    - New hid_asus_ally_terminate(): code compression
    - asus_raw_event() declares the intf pointer once at the top instead
      of inside the size check
  - HID: asus: add gamepad configuration
    - ally_check_capability() returns int and ally_detect_capabilities()
      propagates failures through an error label
    - ally_set_xbox_controller() has a kernel-doc comment and tests
      xbox_controller_support before allocating the command buffer
    - ally_prefs_setup() takes the ally_intf: sysfs_live is
      intf->sysfs_live now, and ally_sysfs_remove() takes the interface
      and returns early without one
    - The sysfs groups are created and removed without ally_prefs.lock:
      this prevents a deadlock
  - HID: asus: add vibration strength configuration
    - The store is bounded by ASUS_ALLY_VIBRATION_INTENSITY_MIN/MAX
      instead of a hard-coded 100, and the range files print those macros
      instead of "0 100"
    - The four per-side callbacks collapse into
      ally_vibration_intensity_show() and ally_vibration_intensity_store()
      over an enum ally_vibration and the ALLY_VIBRATION_FUNCS generator:
      the store snapshots both values under the lock and sends them
      together
  - HID: asus: add joysticks inner and outer range configuration
    - ally_set_joystick_thresholds() has a kernel-doc comment
    - The four threshold callbacks collapse into ally_threshold_show()
      and ally_threshold_store() and the ALLY_THRESHOLD_FUNCS generator,
      bounded by the new ASUS_ALLY_INNER_THRESHOLD_MIN/MAX and the
      existing outer macros, which the range files print too
    - The calibration groups are created when any of user_cal_support,
      anti_deadzone_support or resp_curve_support is set, read under the
      mutex: v12 grew that gate one capability at a time in the following
      commits
    - A failure while creating them unwinds through the new
      err_cal_groups label, and intf->cal_sysfs_live records whether they
      exist
    - ally_sysfs_remove() removes the calibration groups based on
      intf->cal_sysfs_live instead of re-reading the capability flags
  - HID: asus: add triggers inner and outer range configuration
    - ally_set_trigger_ranges() has a kernel-doc comment
    - The generated block does not carry trailing spaces on its blank
      lines anymore
  - HID: asus: add joysticks anti-deadzone configuration
    - The store is bounded by ASUS_ALLY_ANTI_DEADZONE_MIN/MAX instead of
      a hard-coded 100, and the range file prints those macros instead of
      "0 100"
    - The calibration gate for this commit is gone: the joystick commit
      already covers anti_deadzone_support
  - HID: asus: add support for response curve
    - The curve structs are defined before struct ally_prefs, and the
      "one set by design" comment moved onto ally_prefs: it now also says
      the settings survive the controller powering off and coming back,
      that no device or input pointers live there, and that a second
      device emulated with uhid would be rejected by the driver
    - The calibration gate for this commit is gone for the same reason
  - HID: asus: add support to force feedback
    - ally_ff_quiesce() runs from hid_asus_ally_terminate() instead of
      from the asus_probe() error path and asus_remove()
  - HID: asus: add support for gamepad mode
    - gamepad_mode_available is renamed back to gamepad_mode_index to
      align with other handheld drivers, and the commit message says so
  - HID: asus: add support for turbo buttons
    - button_entries lives in struct ally_intf instead of ally_prefs
    - ally_create_button_attributes() reads turbo_support under
      ally_prefs.lock instead of unlocked
    - ally_set_turbo_params() sends the ally_turbo_config struct itself
      as the payload instead of a hand-built initializer list, and its
      kernel-doc gained the @hdev and Context sections
    - The turbo and toggle period callbacks collapse into
      ally_btn_period_show() and ally_btn_period_store() over an enum
      ally_btn_period and the ALLY_BTN_PERIOD_FUNCS generator, and the
      range files print the real bounds instead of a hard-coded "0 255"
    - The button attributes are created without the turbo_support gate:
      the next commit decides where the turbo attributes go, and
      intf->button_entries is published under the mutex together with
      cal_sysfs_live and sysfs_live
    - ally_sysfs_remove() unpublishes intf->button_entries under the
      mutex and removes the groups outside of it, so a mode-store notify
      holding the lock sees either the live array or NULL
  - HID: asus: add support for btn remapping
    - The attribute listing the valid remap targets is named
      remap_available: v12 called it remap_index, and the commit message
      follows
    - ally_create_button_attributes() no longer returns NULL when turbo
      is unsupported: entries_attr_index is computed under the lock and
      the turbo attributes are appended only when supported
    - gamepad_mode_store() notifies remap, macro and remap_available
      outside ally_prefs.lock over the static ally_button_names[] instead
      of iterating the button entries under the lock: the mode update and
      the default-mapping refresh stay under it
    - ally_prefs_setup() applies the recorded mappings whenever
      initialized: the button_entries test is gone since the entries
      always exist
  - HID: asus: document ROG Ally sysfs attributes
    - New patch

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

Denis Benato (18):
  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
  HID: asus: document ROG Ally 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         |  484 ++
 MAINTAINERS                                   |    7 +
 drivers/hid/Kconfig                           |    1 +
 drivers/hid/hid-asus.c                        | 4373 ++++++++++++++++-
 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, 4862 insertions(+), 114 deletions(-)
 create mode 100644 Documentation/ABI/testing/sysfs-driver-hid-asus

-- 
2.47.3


^ permalink raw reply	[flat|nested] 25+ messages in thread

* [PATCH v13 01/18] HID: asus: size feature reports from the HID report descriptor
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (16 subsequent siblings)
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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.

Tested-by: Rebecca Mara Müller <rebecca.mara@posteo.de> # UX8407AA
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..46643022b68d 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_KBD_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] 25+ messages in thread

* [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
  2026-11-04  1:39 ` [PATCH v13 01/18] HID: asus: size feature reports from the HID report descriptor Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  2:07   ` sashiko-bot
  2026-11-04  1:39 ` [PATCH v13 03/18] HID: asus: add gamepad configuration Denis Benato
                   ` (15 subsequent siblings)
  17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 | 1008 ++++++++++++++++++++++++++++++++++++----
 1 file changed, 909 insertions(+), 99 deletions(-)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 46643022b68d..2a3e8c399f0a 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -22,16 +22,23 @@
 
 #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/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 +46,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 +66,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 +194,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 +212,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 +285,816 @@ 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_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;
+
+	/*
+	 * Disable so a concurrent asus_resume() cannot re-queue after the
+	 * cancel. drvdata->ally is cleared before this runs.
+	 */
+	disable_delayed_work_sync(&intf->resume_work);
+}
+
+static void ally_unregister_gamepad(struct ally_intf *intf)
+{
+	if (!intf || !intf->gamepad)
+		return;
+
+	input_unregister_device(intf->gamepad);
+	intf->gamepad = NULL;
+}
+
+static void ally_free(struct ally_intf *intf)
+{
+	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 < 4 || 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
+ *
+ * Handles report 0x5A. A vendor key is reported down while @data[1] matches
+ * its code (0x38 is the ROG key, KEY_PROG1; 0xA6, 0xA7 and 0xA8 are KEY_F16,
+ * KEY_F17 and KEY_F18). @data[1] == 0 releases every known vendor key. Any
+ * other code is ignored here and left to hid-core and the later filters. The
+ * same decode is used for every Ally interface.
+ *
+ * Return: true if @data[1] is one of the vendor codes (0x38, 0xA6, 0xA7,
+ * 0xA8), in which case the report is consumed. false otherwise, including
+ * the @data[1] == 0 report that releases the vendor keys, which must still
+ * reach hid-core.
+ */
+static bool ally_vendor_keys(struct input_dev *input, const u8 *data, int size)
+{
+	bool is_vendor;
+
+	if (size < 2 || data[0] != FEATURE_KBD_REPORT_ID)
+		return false;
+
+	is_vendor = data[1] == 0x38 || data[1] == 0xA6 ||
+		    data[1] == 0xA7 || data[1] == 0xA8;
+
+	/* Neither a vendor press nor a release: leave it to the other handlers. */
+	if (!is_vendor && data[1] != 0)
+		return false;
+
+	if (input) {
+		/* AC short press */
+		input_report_key(input, KEY_PROG1, data[1] == 0x38);
+
+		/* CC short press & release */
+		input_report_key(input, KEY_F16,   data[1] == 0xA6);
+
+		/* AC long press */
+		input_report_key(input, KEY_F17,   data[1] == 0xA7);
+
+		/* AC long release */
+		input_report_key(input, KEY_F18,   data[1] == 0xA8);
+
+		input_sync(input);
+	}
+
+	/*
+	 * Only the vendor codes are consumed. They are reported on this
+	 * interface's evdev above, so hid-core never sees them pressed. Note
+	 * that 0xA8 ("AC long release") is itself a usage the firmware sends,
+	 * not the report that clears one. Everything else, including the 0x00
+	 * report that clears a held usage, must still reach hid-core: it can
+	 * only release the usages it saw pressed if it sees the report that
+	 * clears them.
+	 */
+	return is_vendor;
+}
+
+/**
+ * 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);
+}
+
+static struct ally_intf *hid_asus_ally_terminate(struct asus_drvdata *drvdata)
+{
+	/*
+	 * Pairs with smp_store_release() in asus_probe(): ensures the
+	 * ally_intf fields set before publication (input, gamepad,
+	 * resume_work) are visible before we quiesce and free it.
+	 */
+	struct ally_intf *ally = smp_load_acquire(&drvdata->ally);
+
+	if (!ally)
+		return NULL;
+
+	/* Cut resume off before draining work or freeing intf. */
+	smp_store_release(&drvdata->ally, NULL);
+
+	ally_quiesce(ally);
+	ally_unregister_gamepad(ally);
+
+	return ally;
+}
+
 /*
  * Send events to asus-wmi driver for handling special keys
  */
@@ -496,6 +1346,7 @@ static int asus_raw_event(struct hid_device *hdev,
 		struct hid_report *report, u8 *data, int size)
 {
 	struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+	struct ally_intf *intf;
 	int ret;
 
 	if (size < 2) {
@@ -512,6 +1363,19 @@ 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) {
+		/*
+		 * 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 +1445,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 +2161,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 */
@@ -1569,6 +2336,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 +2347,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 +2364,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 +2380,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 +2498,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,6 +2548,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 = hid_asus_ally_terminate(drvdata);
+		asus_worker_stop(drvdata->worker);
+		hid_hw_stop(hdev);
+		ally_free(ally);
+		return ret;
+	}
+
 	asus_worker_stop(drvdata->worker);
 	hid_hw_stop(hdev);
 	return ret;
@@ -1763,6 +2564,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
 static void asus_remove(struct hid_device *hdev)
 {
 	struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
+	struct ally_intf *ally;
 
 	if (drvdata->listener.brightness_set)
 		asus_hid_unregister_listener(&drvdata->listener);
@@ -1770,6 +2572,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 = hid_asus_ally_terminate(drvdata);
+		asus_worker_stop(drvdata->worker);
+		hid_hw_stop(hdev);
+		ally_free(ally);
+		return;
+	}
+
 	asus_worker_stop(drvdata->worker);
 	hid_hw_stop(hdev);
 }
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v13 03/18] HID: asus: add gamepad configuration
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
  2026-11-04  1:39 ` [PATCH v13 01/18] HID: asus: size feature reports from the HID report descriptor Denis Benato
  2026-11-04  1:39 ` [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:59   ` sashiko-bot
  2026-11-04  1:39 ` [PATCH v13 04/18] HID: asus: add vibration strength configuration Denis Benato
                   ` (14 subsequent siblings)
  17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 | 312 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 311 insertions(+), 1 deletion(-)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 2a3e8c399f0a..8277d7108bb9 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>
@@ -36,6 +37,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>
@@ -77,6 +79,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
@@ -203,6 +210,13 @@ struct ally_intf {
 	u8 cad_state;
 	unsigned long cad_last_event_time;
 	struct delayed_work resume_work;
+	/*
+	 * Sysfs groups hang off this interface's hdev. Numbers live in
+	 * ally_prefs so a re-probe can restore them; ownership of the
+	 * attributes is per device.
+	 */
+	bool sysfs_live;
+	bool cal_sysfs_live;
 };
 
 struct asus_drvdata {
@@ -762,6 +776,281 @@ 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, but that would get rejected by the driver meaning that
+ * this struct can be used on a per-machine basis.
+ */
+static struct ally_prefs {
+	/* Guards every field below, including across the feature report. */
+	struct mutex lock;
+
+	bool initialized;
+
+	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: 1 if the capability is supported, 0 if not, negative on error.
+ */
+static int 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 -ENOMEM;
+	}
+
+	if (buf_len < 5) {
+		hid_err(hdev, "Capability check response too short.\n");
+		return -EINVAL;
+	}
+
+	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 ret;
+	}
+
+	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)
+{
+	int ret;
+
+	guard(mutex)(&ally_prefs.lock);
+
+	ally_prefs.is_ally_x = (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X);
+
+	ret = ally_check_capability(hdev, CMD_CHECK_XBOX_SUPPORT);
+	if (ret < 0)
+		goto ally_detect_capabilities_err;
+	ally_prefs.xbox_controller_support = ret;
+
+
+	ret = ally_check_capability(hdev, CMD_CHECK_USER_CAL_SUPPORT);
+	if (ret < 0)
+		goto ally_detect_capabilities_err;
+	ally_prefs.user_cal_support = ret;
+
+	ret = ally_check_capability(hdev, CMD_CHECK_TURBO_SUPPORT);
+	if (ret < 0)
+		goto ally_detect_capabilities_err;
+	ally_prefs.turbo_support = ret;
+
+	ret = ally_check_capability(hdev, CMD_CHECK_RESP_CURVE_SUPPORT);
+	if (ret < 0)
+		goto ally_detect_capabilities_err;
+	ally_prefs.resp_curve_support = ret;
+
+	ret = ally_check_capability(hdev, CMD_CHECK_DIR_TO_BTN_SUPPORT);
+	if (ret < 0)
+		goto ally_detect_capabilities_err;
+	ally_prefs.dir_to_btn_support = ret;
+
+	ret = ally_check_capability(hdev, CMD_CHECK_GYRO_TO_JOYSTICK);
+	if (ret < 0)
+		goto ally_detect_capabilities_err;
+	ally_prefs.gyro_support = ret;
+
+	ret = ally_check_capability(hdev, CMD_CHECK_ANTI_DEADZONE);
+	if (ret < 0)
+		goto ally_detect_capabilities_err;
+	ally_prefs.anti_deadzone_support = ret;
+
+	return 0;
+ally_detect_capabilities_err:
+	hid_err(hdev, "Failed to detect capabilities: %d\n", ret);
+	return ret;
+}
+
+/**
+ * ally_set_xbox_controller() - Enable or disable Xbox controller mode
+ * @hdev: HID device on which the command packet is sent
+ * @enabled: true to enable Xbox controller mode, false to disable it
+ *
+ * Builds a %CMD_SET_XBOX_CONTROLLER packet and sends it to the device. On
+ * success, the new state is stored in ally_prefs.xbox_controller_enabled.
+ *
+ * Context: Caller must hold ally_prefs.lock.
+ *
+ * Return: 0 on success, -%EOPNOTSUPP if Xbox controller mode is not
+ * supported, -%ENOMEM if the command buffer allocation fails, or a negative
+ * errno from ally_gamepad_send_packet() if sending the packet fails.
+ */
+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;
+
+	if (!ally_prefs.xbox_controller_support)
+		return -EOPNOTSUPP;
+
+	u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_XBOX_CONTROLLER,
+					       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 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, struct ally_intf *intf)
+{
+	int ret, i;
+
+	ret = ally_detect_capabilities(hdev);
+	if (ret)
+		return ret;
+
+	scoped_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 (intf->sysfs_live)
+			return 0;
+	}
+
+	/*
+	 * Create and remove sysfs groups without ally_prefs.lock. Attribute
+	 * methods take that lock while holding a kernfs active reference;
+	 * removing a group drains those references, so locking here would
+	 * invert the order and deadlock.
+	 */
+	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;
+		}
+	}
+
+	scoped_guard(mutex, &ally_prefs.lock) {
+		intf->sysfs_live = true;
+	}
+
+	return 0;
+}
+
+static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
+{
+	int i;
+
+	if (!intf)
+		return;
+
+	if (ally_get_endpoint_address(hdev) != HID_ALLY_INTF_CFG_IN)
+		return;
+
+	scoped_guard(mutex, &ally_prefs.lock) {
+		if (!intf->sysfs_live)
+			return;
+
+		intf->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;
@@ -944,6 +1233,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;
 }
 
@@ -1038,6 +1335,12 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
 			return ERR_PTR(ret);
 		}
 
+		ret = ally_prefs_setup(hdev, intf);
+		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:
@@ -1058,7 +1361,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
@@ -1071,6 +1374,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);
 }
 
@@ -1091,6 +1400,7 @@ static struct ally_intf *hid_asus_ally_terminate(struct asus_drvdata *drvdata)
 
 	ally_quiesce(ally);
 	ally_unregister_gamepad(ally);
+	ally_sysfs_remove(drvdata->hdev, ally);
 
 	return ally;
 }
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v13 04/18] HID: asus: add vibration strength configuration
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (2 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 03/18] HID: asus: add gamepad configuration Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration Denis Benato
                   ` (13 subsequent siblings)
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 | 171 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 171 insertions(+)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 8277d7108bb9..459061391c37 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -97,6 +97,9 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
 
 #define ASUS_HID_INPUT_KBD_REPORT_ID 0x01
 
+#define ASUS_ALLY_VIBRATION_INTENSITY_MIN 0
+#define ASUS_ALLY_VIBRATION_INTENSITY_MAX 100
+
 /* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
 #define ASUS_SPURIOUS_CODE_0XEA 0xea
 #define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -804,6 +807,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),
 };
@@ -976,6 +982,149 @@ 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;
+}
+
+/* Indices into ally_vibration[], in the order ally_set_vibration_intensity() takes them. */
+enum ally_vibration {
+	ALLY_VIBRATION_LEFT,
+	ALLY_VIBRATION_RIGHT,
+};
+
+static u8 *const ally_vibration[] = {
+	[ALLY_VIBRATION_LEFT]  = &ally_prefs.vibration_intensity_left,
+	[ALLY_VIBRATION_RIGHT] = &ally_prefs.vibration_intensity_right,
+};
+
+static ssize_t ally_vibration_intensity_show(char *buf, enum ally_vibration which)
+{
+	guard(mutex)(&ally_prefs.lock);
+
+	return sysfs_emit(buf, "%u\n", *ally_vibration[which]);
+}
+
+static ssize_t ally_vibration_intensity_store(struct device *dev, const char *buf,
+					      size_t count, enum ally_vibration which,
+					      const char *group)
+{
+	u8 value, v[ARRAY_SIZE(ally_vibration)];
+	struct hid_device *hdev = to_hid_device(dev);
+	size_t i;
+	int ret;
+
+	ret = kstrtou8(buf, 10, &value);
+	if (ret || value > ASUS_ALLY_VIBRATION_INTENSITY_MAX)
+		return -EINVAL;
+
+	scoped_guard(mutex, &ally_prefs.lock) {
+		for (i = 0; i < ARRAY_SIZE(v); i++)
+			v[i] = *ally_vibration[i];
+		v[which] = value;
+
+		ret = ally_set_vibration_intensity(hdev,
+						   v[ALLY_VIBRATION_LEFT],
+						   v[ALLY_VIBRATION_RIGHT]);
+		if (ret < 0)
+			return ret;
+
+		*ally_vibration[which] = value;
+	}
+
+	sysfs_notify(&dev->kobj, group, "intensity");
+
+	return count;
+}
+
+/* Generates <side>_vibration_intensity_{show,store,range_show}. */
+#define ALLY_VIBRATION_FUNCS(side, SIDE)						\
+static ssize_t side##_vibration_intensity_show(struct device *dev,			\
+					       struct device_attribute *attr,		\
+					       char *buf)				\
+{											\
+	return ally_vibration_intensity_show(buf, ALLY_VIBRATION_##SIDE);		\
+}											\
+											\
+static ssize_t side##_vibration_intensity_store(struct device *dev,			\
+						struct device_attribute *attr,		\
+						const char *buf, size_t count)		\
+{											\
+	return ally_vibration_intensity_store(dev, buf, count, ALLY_VIBRATION_##SIDE,	\
+					      #side "_vibration");			\
+}											\
+											\
+static ssize_t side##_vibration_intensity_range_show(struct device *dev,		\
+						     struct device_attribute *attr,	\
+						     char *buf)				\
+{											\
+	return sysfs_emit(buf, "%u %u\n", ASUS_ALLY_VIBRATION_INTENSITY_MIN,		\
+			  ASUS_ALLY_VIBRATION_INTENSITY_MAX);				\
+}
+
+ALLY_VIBRATION_FUNCS(left,  LEFT)
+ALLY_VIBRATION_FUNCS(right, RIGHT)
+
+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
@@ -985,6 +1134,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, struct ally_intf *intf)
@@ -995,6 +1152,8 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
 	if (ret)
 		return ret;
 
+	ally_vibration_defaults();
+
 	scoped_guard(mutex, &ally_prefs.lock) {
 		if (!ally_prefs.initialized) {
 			ally_prefs.gamepad_mode = 0x01;
@@ -1241,6 +1400,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] 25+ messages in thread

* [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (3 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 04/18] HID: asus: add vibration strength configuration Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:53   ` sashiko-bot
  2026-11-04  1:39 ` [PATCH v13 06/18] HID: asus: add triggers " Denis Benato
                   ` (12 subsequent siblings)
  17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 | 266 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 265 insertions(+), 1 deletion(-)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 459061391c37..a89ce83ae93f 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -100,6 +100,12 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
 #define ASUS_ALLY_VIBRATION_INTENSITY_MIN 0
 #define ASUS_ALLY_VIBRATION_INTENSITY_MAX 100
 
+#define ASUS_ALLY_OUTER_THRESHOLD_MIN 70
+#define ASUS_ALLY_OUTER_THRESHOLD_MAX 100
+
+#define ASUS_ALLY_INNER_THRESHOLD_MIN 0
+#define ASUS_ALLY_INNER_THRESHOLD_MAX 50
+
 /* Spurious HID codes sent by QUIRK_ROG_NKEY_KEYBOARD devices */
 #define ASUS_SPURIOUS_CODE_0XEA 0xea
 #define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -810,6 +816,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),
 };
@@ -1125,6 +1136,212 @@ static struct attribute *ally_right_vibration_attrs[] = {
 	NULL
 };
 
+/**
+ * ally_set_joystick_thresholds() - Set the joystick inner and outer thresholds
+ * @hdev: HID device the command packet is sent on
+ * @left_it: left joystick inner threshold (deadzone)
+ * @left_ot: left joystick outer threshold
+ * @right_it: right joystick inner threshold (deadzone)
+ * @right_ot: right joystick outer threshold
+ *
+ * Context: Caller holds ally_prefs.lock.
+ * Return: 0 on success, -ENOMEM if the command buffer cannot be allocated,
+ * or a negative errno if sending the packet fails.
+ */
+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;
+}
+
+/* Indices into ally_thresholds[], in the order the MCU packet takes them. */
+enum ally_threshold {
+	ALLY_LEFT_INNER,
+	ALLY_LEFT_OUTER,
+	ALLY_RIGHT_INNER,
+	ALLY_RIGHT_OUTER,
+};
+
+static u8 *const ally_thresholds[] = {
+	[ALLY_LEFT_INNER]  = &ally_prefs.left_deadzone,
+	[ALLY_LEFT_OUTER]  = &ally_prefs.left_outer_threshold,
+	[ALLY_RIGHT_INNER] = &ally_prefs.right_deadzone,
+	[ALLY_RIGHT_OUTER] = &ally_prefs.right_outer_threshold,
+};
+
+static ssize_t ally_threshold_show(char *buf, enum ally_threshold which)
+{
+	guard(mutex)(&ally_prefs.lock);
+
+	if (!ally_prefs.user_cal_support)
+		return -EOPNOTSUPP;
+
+	return sysfs_emit(buf, "%u\n", *ally_thresholds[which]);
+}
+
+static ssize_t ally_threshold_store(struct device *dev, const char *buf,
+				    size_t count, enum ally_threshold which,
+				    int lo, int hi,
+				    const char *group, const char *name)
+{
+	u8 value, v[ARRAY_SIZE(ally_thresholds)];
+	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_thresholds[i];
+		v[which] = value;
+
+		ret = ally_set_joystick_thresholds(hdev,
+						   v[ALLY_LEFT_INNER],
+						   v[ALLY_LEFT_OUTER],
+						   v[ALLY_RIGHT_INNER],
+						   v[ALLY_RIGHT_OUTER]);
+		if (ret)
+			return ret;
+
+		*ally_thresholds[which] = value;
+	}
+
+	sysfs_notify(&dev->kobj, group, name);
+
+	return count;
+}
+
+/* Generates <side>_joystick_<kind>_threshold_{show,store,range_show}. */
+#define ALLY_THRESHOLD_FUNCS(side, SIDE, kind, KIND)						\
+static ssize_t side##_joystick_##kind##_threshold_show(struct device *dev,			\
+						       struct device_attribute *attr,		\
+						       char *buf)				\
+{												\
+	return ally_threshold_show(buf, ALLY_##SIDE##_##KIND);					\
+}												\
+												\
+static ssize_t side##_joystick_##kind##_threshold_store(struct device *dev,			\
+							struct device_attribute *attr,		\
+							const char *buf, size_t count)		\
+{												\
+	return ally_threshold_store(dev, buf, count, ALLY_##SIDE##_##KIND,			\
+				    ASUS_ALLY_##KIND##_THRESHOLD_MIN,				\
+				    ASUS_ALLY_##KIND##_THRESHOLD_MAX,				\
+				    #side "_joystick_axis", #kind "_threshold");		\
+}												\
+												\
+static ssize_t side##_joystick_##kind##_threshold_range_show(struct device *dev,		\
+							     struct device_attribute *attr,	\
+							     char *buf)				\
+{												\
+	return sysfs_emit(buf, "%u %u\n", ASUS_ALLY_##KIND##_THRESHOLD_MIN,			\
+			  ASUS_ALLY_##KIND##_THRESHOLD_MAX);					\
+}
+
+ALLY_THRESHOLD_FUNCS(left,  LEFT,  inner, INNER)
+ALLY_THRESHOLD_FUNCS(left,  LEFT,  outer, OUTER)
+ALLY_THRESHOLD_FUNCS(right, RIGHT, inner, INNER)
+ALLY_THRESHOLD_FUNCS(right, RIGHT, outer, OUTER)
+
+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
@@ -1146,13 +1363,15 @@ static const struct attribute_group ally_attr_groups[] = {
 
 static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
 {
-	int ret, i;
+	bool cal_live = false;
+	int ret, i, cal_n = 0;
 
 	ret = ally_detect_capabilities(hdev);
 	if (ret)
 		return ret;
 
 	ally_vibration_defaults();
+	ally_joystick_defaults();
 
 	scoped_guard(mutex, &ally_prefs.lock) {
 		if (!ally_prefs.initialized) {
@@ -1182,15 +1401,44 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
 		}
 	}
 
+	scoped_guard(mutex, &ally_prefs.lock)
+		cal_live = ally_prefs.user_cal_support ||
+			ally_prefs.anti_deadzone_support ||
+			ally_prefs.resp_curve_support;
+
+	/* Skip the calibration groups when the capability is missing. */
+	if (cal_live) {
+		for (; cal_n < ARRAY_SIZE(ally_cal_attr_groups); cal_n++) {
+			ret = sysfs_create_group(&hdev->dev.kobj,
+						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_n]->name, ret);
+				goto err_cal_groups;
+			}
+		}
+	}
+
 	scoped_guard(mutex, &ally_prefs.lock) {
+		intf->cal_sysfs_live = cal_live;
 		intf->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, struct ally_intf *intf)
 {
+	bool cal_live;
 	int i;
 
 	if (!intf)
@@ -1204,10 +1452,17 @@ static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
 			return;
 
 		intf->sysfs_live = false;
+		cal_live = intf->cal_sysfs_live;
+		intf->cal_sysfs_live = false;
 	}
 
 	for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
 		sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
+
+	if (cal_live) {
+		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 {
@@ -1412,6 +1667,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] 25+ messages in thread

* [PATCH v13 06/18] HID: asus: add triggers inner and outer range configuration
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (4 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 07/18] HID: asus: add joysticks anti-deadzone configuration Denis Benato
                   ` (11 subsequent siblings)
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 | 225 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 225 insertions(+)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index a89ce83ae93f..8c49e9bdad50 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -106,6 +106,11 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
 #define ASUS_ALLY_INNER_THRESHOLD_MIN 0
 #define ASUS_ALLY_INNER_THRESHOLD_MAX 50
 
+#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
 #define ASUS_SPURIOUS_CODE_0XEC 0xec
@@ -821,6 +826,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),
 };
@@ -1337,9 +1347,213 @@ static const struct attribute_group ally_right_joystick_axis_group = {
 	.attrs = right_joystick_axis_attrs,
 };
 
+/**
+ * ally_set_trigger_ranges() - Set the gamepad trigger travel ranges
+ * @hdev: HID device the command packet is sent on
+ * @left_min: left trigger minimum (start of travel)
+ * @left_max: left trigger maximum (end of travel)
+ * @right_min: right trigger minimum (start of travel)
+ * @right_max: right trigger maximum (end of travel)
+ *
+ * Context: Caller holds ally_prefs.lock.
+ * Return: 0 on success, -ENOMEM if the command buffer cannot be allocated,
+ * or a negative errno if sending the packet fails.
+ */
+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[] = {
@@ -1372,6 +1586,7 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
 
 	ally_vibration_defaults();
 	ally_joystick_defaults();
+	ally_trigger_defaults();
 
 	scoped_guard(mutex, &ally_prefs.lock) {
 		if (!ally_prefs.initialized) {
@@ -1676,6 +1891,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] 25+ messages in thread

* [PATCH v13 07/18] HID: asus: add joysticks anti-deadzone configuration
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (5 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 06/18] HID: asus: add triggers " Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 08/18] HID: asus: add support for response curve Denis Benato
                   ` (10 subsequent siblings)
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 | 157 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 157 insertions(+)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 8c49e9bdad50..344abf1510c4 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -106,6 +106,9 @@ MODULE_DESCRIPTION("Asus HID Keyboard and TouchPad");
 #define ASUS_ALLY_INNER_THRESHOLD_MIN 0
 #define ASUS_ALLY_INNER_THRESHOLD_MAX 50
 
+#define ASUS_ALLY_ANTI_DEADZONE_MIN 0
+#define ASUS_ALLY_ANTI_DEADZONE_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
@@ -831,6 +834,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),
 };
@@ -1317,11 +1323,153 @@ 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 > ASUS_ALLY_ANTI_DEADZONE_MAX)
+		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, "%u %u\n", ASUS_ALLY_ANTI_DEADZONE_MIN,			\
+			  ASUS_ALLY_ANTI_DEADZONE_MAX);					\
+}
+
+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
 };
 
@@ -1330,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
 };
 
@@ -1901,6 +2051,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] 25+ messages in thread

* [PATCH v13 08/18] HID: asus: add support for response curve
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (6 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 07/18] HID: asus: add joysticks anti-deadzone configuration Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 09/18] HID: asus: add support to force feedback Denis Benato
                   ` (9 subsequent siblings)
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 | 271 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 271 insertions(+)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 344abf1510c4..b55fe10826a5 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>
@@ -793,6 +794,18 @@ static int ally_get_endpoint_address(struct hid_device *hdev)
 	return ep->desc.bEndpointAddress;
 }
 
+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;
+
 /*
  * Settings that must survive the controller powering off and coming back.
  * No device or input pointers live here: every transfer uses the interface
@@ -837,6 +850,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
 };
 
@@ -1737,6 +1990,7 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
 	ally_vibration_defaults();
 	ally_joystick_defaults();
 	ally_trigger_defaults();
+	ally_curve_defaults();
 
 	scoped_guard(mutex, &ally_prefs.lock) {
 		if (!ally_prefs.initialized) {
@@ -2058,6 +2312,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] 25+ messages in thread

* [PATCH v13 09/18] HID: asus: add support to force feedback
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (7 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 08/18] HID: asus: add support for response curve Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 10/18] HID: asus: add support for gamepad mode Denis Benato
                   ` (8 subsequent siblings)
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 | 142 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 143 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 b55fe10826a5..9c9ed75b98a4 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>
@@ -219,6 +221,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. */
@@ -235,6 +254,14 @@ struct ally_intf {
 	 */
 	bool sysfs_live;
 	bool cal_sysfs_live;
+	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 {
@@ -341,10 +368,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
@@ -2217,6 +2249,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);
@@ -2245,8 +2355,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;
@@ -2437,6 +2563,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;
@@ -2459,6 +2593,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;
 
@@ -2487,6 +2628,7 @@ static struct ally_intf *hid_asus_ally_terminate(struct asus_drvdata *drvdata)
 	smp_store_release(&drvdata->ally, NULL);
 
 	ally_quiesce(ally);
+	ally_ff_quiesce(ally);
 	ally_unregister_gamepad(ally);
 	ally_sysfs_remove(drvdata->hdev, ally);
 
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v13 10/18] HID: asus: add support for gamepad mode
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (8 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 09/18] HID: asus: add support to force feedback Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 11/18] HID: asus: add support for turbo buttons Denis Benato
                   ` (7 subsequent siblings)
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 via the gamepad_mode sysfs property.

Also list available models with gamepad_mode_index to align with
other handheld drivers.

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 9c9ed75b98a4..d3dbeba529c1 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -368,6 +368,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
 
@@ -1371,6 +1385,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_index_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_index);
+
 /**
  * ally_set_anti_deadzone() - Send the joystick anti-deadzone values to the device
  * @hdev: HID device the packet is sent on
@@ -1993,6 +2113,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_index.attr,
 	NULL
 };
 
@@ -2026,7 +2148,7 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
 
 	scoped_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;
 		}
@@ -2455,6 +2577,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] 25+ messages in thread

* [PATCH v13 11/18] HID: asus: add support for turbo buttons
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (9 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 10/18] HID: asus: add support for gamepad mode Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:52   ` sashiko-bot
  2026-11-04  1:39 ` [PATCH v13 12/18] HID: asus: add support for btn remapping Denis Benato
                   ` (6 subsequent siblings)
  17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 | 342 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 342 insertions(+)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index d3dbeba529c1..70c9d71fc5fa 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -238,6 +238,8 @@ struct ally_ff_report {
 	struct ally_ff_data ff;
 } __packed;
 
+struct ally_btn_sysfs_entry;
+
 struct ally_intf {
 	struct hid_device *hdev;
 	/* HID-core evdev of this interface. Reports for it land here. */
@@ -254,6 +256,7 @@ struct ally_intf {
 	 */
 	bool sysfs_live;
 	bool cal_sysfs_live;
+	struct ally_btn_sysfs_entry *button_entries;
 	struct {
 		/* Guards live, update, and packet. */
 		spinlock_t lock;
@@ -852,6 +855,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;
+};
+
 /*
  * Settings that must survive the controller powering off and coming back.
  * No device or input pointers live here: every transfer uses the interface
@@ -899,6 +976,8 @@ static struct ally_prefs {
 
 	struct ally_joystick_resp_curve left_curve;
 	struct ally_joystick_resp_curve right_curve;
+
+	struct ally_turbo_config turbo;
 } ally_prefs = {
 	.lock = __MUTEX_INITIALIZER(ally_prefs.lock),
 };
@@ -2132,8 +2211,252 @@ 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
+ * @hdev: The HID device to send the packet to.
+ *
+ * Context: Caller holds ally_prefs.lock.
+ *
+ * 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;
+	size_t buf_len;
+	int ret;
+
+	u8 *buf __free(kfree) = ally_alloc_cmd(hdev, CMD_SET_TURBO_PARAMS,
+					       (const u8 *)turbo, sizeof(*turbo),
+					       &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;
+}
+
+/* Which per-button period a sysfs attribute operates on. */
+enum ally_btn_period {
+	ALLY_TURBO_PERIOD,
+	ALLY_TOGGLE_PERIOD,
+};
+
+static u8 *ally_btn_period_field(struct ally_btn_turbo_params *params,
+				 enum ally_btn_period which)
+{
+	return which == ALLY_TURBO_PERIOD ? &params->turbo : &params->toggle;
+}
+
+static ssize_t ally_btn_period_show(struct ally_btn_sysfs_entry *entry, char *buf,
+				    enum ally_btn_period which)
+{
+	struct ally_btn_turbo_params *params;
+
+	scoped_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", *ally_btn_period_field(params, which));
+}
+
+static ssize_t ally_btn_period_store(struct device *dev,
+				     struct ally_btn_sysfs_entry *entry,
+				     const char *buf, size_t count,
+				     enum ally_btn_period which, u8 max,
+				     const char *name)
+{
+	struct hid_device *hdev = to_hid_device(dev);
+	struct ally_btn_turbo_params *params;
+	u8 old_value, value, *field;
+	int ret;
+
+	ret = kstrtou8(buf, 10, &value);
+	if (ret)
+		return ret;
+	if (value > max)
+		return -EINVAL;
+
+	scoped_guard(mutex, &ally_prefs.lock) {
+		if (!ally_prefs.turbo_support)
+			return -EOPNOTSUPP;
+
+		params = ally_btn_param(entry->btn);
+		if (!params)
+			return -ENODEV;
+
+		field = ally_btn_period_field(params, which);
+		old_value = *field;
+		*field = value;
+
+		ret = ally_set_turbo_params(hdev);
+		if (ret) {
+			*field = old_value;
+			return ret;
+		}
+	}
+
+	sysfs_notify(&dev->kobj, entry->group.name, name);
+
+	return count;
+}
+
+/* Generates btn_<kind>_period_{show,store,range_show}. */
+#define ALLY_BTN_PERIOD_FUNCS(kind, KIND, max)						\
+static ssize_t btn_##kind##_period_show(struct device *dev,				\
+					struct device_attribute *attr, char *buf)	\
+{											\
+	struct ally_btn_sysfs_entry *entry = container_of(attr,				\
+		struct ally_btn_sysfs_entry, attr_##kind##_period);			\
+											\
+	return ally_btn_period_show(entry, buf, ALLY_##KIND##_PERIOD);			\
+}											\
+											\
+static ssize_t btn_##kind##_period_store(struct device *dev,				\
+					 struct device_attribute *attr,			\
+					 const char *buf, size_t count)			\
+{											\
+	struct ally_btn_sysfs_entry *entry = container_of(attr,				\
+		struct ally_btn_sysfs_entry, attr_##kind##_period);			\
+											\
+	return ally_btn_period_store(dev, entry, buf, count, ALLY_##KIND##_PERIOD,	\
+				     max, #kind "_period");				\
+}											\
+											\
+static ssize_t btn_##kind##_period_range_show(struct device *dev,			\
+					      struct device_attribute *attr,		\
+					      char *buf)				\
+{											\
+	return sysfs_emit(buf, "%u %u\n", 0, (unsigned int)(max));			\
+}
+
+ALLY_BTN_PERIOD_FUNCS(turbo,  TURBO,  ALLY_TURBO_PERIOD_MAX)
+ALLY_BTN_PERIOD_FUNCS(toggle, TOGGLE, U8_MAX)
+
+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;
+
+	scoped_guard(mutex, &ally_prefs.lock)
+		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;
+
+	if (!entries)
+		return;
+
+	for (i = 0; i < ALLY_BTN_MAX; i++) {
+		sysfs_remove_group(&hdev->dev.kobj, &entries[i].group);
+	}
+
+	kfree(entries);
+}
+
 static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
 {
+	struct ally_btn_sysfs_entry *entries = NULL;
 	bool cal_live = false;
 	int ret, i, cal_n = 0;
 
@@ -2192,7 +2515,15 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
 		}
 	}
 
+	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;
+	}
+
 	scoped_guard(mutex, &ally_prefs.lock) {
+		intf->button_entries = entries;
 		intf->cal_sysfs_live = cal_live;
 		intf->sysfs_live = true;
 	}
@@ -2211,6 +2542,7 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
 
 static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
 {
+	struct ally_btn_sysfs_entry *entries;
 	bool cal_live;
 	int i;
 
@@ -2225,10 +2557,14 @@ static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
 			return;
 
 		intf->sysfs_live = false;
+		entries = intf->button_entries;
+		intf->button_entries = NULL;
 		cal_live = intf->cal_sysfs_live;
 		intf->cal_sysfs_live = false;
 	}
 
+	ally_remove_button_attributes(hdev, entries);
+
 	for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
 		sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
 
@@ -2583,6 +2919,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] 25+ messages in thread

* [PATCH v13 12/18] HID: asus: add support for btn remapping
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (10 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 11/18] HID: asus: add support for turbo buttons Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:58   ` sashiko-bot
  2026-11-04  1:39 ` [PATCH v13 13/18] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
                   ` (5 subsequent siblings)
  17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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_available 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 and 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 | 848 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 840 insertions(+), 8 deletions(-)

diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
index 70c9d71fc5fa..dcb3c9f5d23e 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -40,6 +40,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>
@@ -117,6 +118,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
@@ -921,12 +926,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];
 };
 
 /*
@@ -978,10 +1186,587 @@ static struct ally_prefs {
 	struct ally_joystick_resp_curve right_curve;
 
 	struct ally_turbo_config turbo;
+	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
+ *
+ * This function deals with data from userspace therefore it
+ * must not trust the input blindly.
+ *
+ * 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 (!sysfs_streq(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;
+	const struct btn_code_map *old_code, *code;
+	const struct btn_code_map **target_map;
+	struct ally_button_mapping *mode_map;
+	enum btn_pair_index pair_idx;
+	struct button_pair_map *pair;
+	struct button_map *btn_map;
+	bool is_first;
+	int ret, i;
+
+	code = find_button_by_name(buf);
+	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;
+
+	target_map = btn_attr->is_macro ? &btn_map->macro : &btn_map->remap;
+	old_code = *target_map;
+	*target_map = 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:
+	*target_map = old_code;
+
+	/* Try to recover the mapping presented to the user. */
+	ally_set_button_mapping(hdev, pair);
+	goto err_out;
+
+err_restore:
+	*target_map = 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
@@ -1523,8 +2308,8 @@ static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *a
 				  const char *buf, size_t count)
 {
 	struct hid_device *hdev = to_hid_device(dev);
+	int mode, ret, i;
 	u8 mode_byte;
-	int mode, ret;
 
 	mode = sysfs_match_string(ally_gamepad_mode_text, buf);
 	if (mode < 0)
@@ -1539,11 +2324,27 @@ static ssize_t gamepad_mode_store(struct device *dev, struct device_attribute *a
 			return ret;
 
 		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);
 	}
 
 	hid_dbg(hdev, "Set gamepad mode to %s\n", ally_gamepad_mode_text[mode]);
 	sysfs_notify(&dev->kobj, NULL, "gamepad_mode");
 
+	for (i = 0; i < ALLY_BTN_MAX; i++) {
+		sysfs_notify(&dev->kobj, ally_button_names[i], "remap");
+		sysfs_notify(&dev->kobj, ally_button_names[i], "macro");
+		sysfs_notify(&dev->kobj, ally_button_names[i], "remap_available");
+	}
+
 	return count;
 }
 
@@ -2399,11 +3200,11 @@ 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)
 {
+	size_t entries_attr_index;
 	int i, ret;
 
 	scoped_guard(mutex, &ally_prefs.lock)
-		if (!ally_prefs.turbo_support)
-			return NULL;
+		entries_attr_index = ally_prefs.turbo_support ? 4 : 0;
 
 	struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
 	if (!entries)
@@ -2412,13 +3213,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;
 
@@ -2436,6 +3257,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);
 }
 
@@ -2449,6 +3275,8 @@ static void ally_remove_button_attributes(struct hid_device *hdev,
 
 	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);
@@ -2468,6 +3296,7 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
 	ally_joystick_defaults();
 	ally_trigger_defaults();
 	ally_curve_defaults();
+	ally_mapping_defaults();
 
 	scoped_guard(mutex, &ally_prefs.lock) {
 		if (!ally_prefs.initialized) {
@@ -2925,6 +3754,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_apply_button_mappings(hdev);
+
 	return 0;
 }
 
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v13 13/18] HID: asus: identify XG Mobile stations and bind the LED to the reported model
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (11 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 12/18] HID: asus: add support for btn remapping Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 14/18] HID: asus: expose XG Mobile identity and firmware information Denis Benato
                   ` (4 subsequent siblings)
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 dcb3c9f5d23e..eb9dd69fd605 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>
@@ -69,6 +70,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
 
@@ -216,6 +240,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;
@@ -290,6 +321,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);
@@ -4868,6 +4900,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 };
@@ -5134,14 +5329,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] 25+ messages in thread

* [PATCH v13 14/18] HID: asus: expose XG Mobile identity and firmware information
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (12 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 13/18] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 15/18] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
                   ` (3 subsequent siblings)
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 eb9dd69fd605..a24c64c86b5f 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -73,6 +73,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. */
@@ -80,6 +81,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
@@ -245,6 +252,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 {
@@ -5063,6 +5072,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 };
@@ -5366,9 +5522,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,
@@ -5401,6 +5577,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;
 }
@@ -5617,6 +5795,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);
 
@@ -5641,6 +5822,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] 25+ messages in thread

* [PATCH v13 15/18] HID: asus: document ROG XG Mobile sysfs attributes
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (13 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 14/18] HID: asus: expose XG Mobile identity and firmware information Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 16/18] HID: asus: document ROG Ally " Denis Benato
                   ` (2 subsequent siblings)
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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] 25+ messages in thread

* [PATCH v13 16/18] HID: asus: document ROG Ally sysfs attributes
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (14 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 15/18] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:55   ` sashiko-bot
  2026-11-04  1:39 ` [PATCH v13 17/18] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
  2026-11-04  1:39 ` [PATCH v13 18/18] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato
  17 siblings, 1 reply; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 gamepad configuration of the ROG Ally
handhelds through sysfs: the controller emulation and the
operating mode, the per-motor vibration strength, the per-axis
joystick calibration (dead zone, outer threshold, anti-dead
zone and response curve), the trigger activation ranges, and
the per-button mapping, macro and turbo settings.

Document each file: where it is created, what a read returns,
which values a write accepts, and the error returned when the
controller lacks the capability.

Signed-off-by: Denis Benato <denis.benato@linux.dev>
---
 .../ABI/testing/sysfs-driver-hid-asus         | 389 ++++++++++++++++++
 1 file changed, 389 insertions(+)

diff --git a/Documentation/ABI/testing/sysfs-driver-hid-asus b/Documentation/ABI/testing/sysfs-driver-hid-asus
index 30e0d17d4cec..515eb7c550fc 100644
--- a/Documentation/ABI/testing/sysfs-driver-hid-asus
+++ b/Documentation/ABI/testing/sysfs-driver-hid-asus
@@ -93,3 +93,392 @@ Description:
 
 		Access: Read only
 		Valid values: a single line of text
+
+What:		/sys/bus/hid/drivers/asus/<dev>/xbox_controller
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Enable or disable the Xbox controller emulation of the
+		gamepad.
+
+		The file is created on the configuration interface of a
+		bound ASUS ROG Ally or ROG Ally X controller. A read
+		returns 1 when the emulation is enabled and 0 when it
+		is disabled; the emulation is enabled by default.
+
+		Reading and writing return -EOPNOTSUPP when the
+		controller does not report the capability.
+
+		Access: Read/Write
+		Valid values: 0, 1
+
+What:		/sys/bus/hid/drivers/asus/<dev>/gamepad_mode
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Operating mode of the gamepad. In "gamepad" mode the
+		buttons report gamepad codes, in "desktop" mode they
+		report keyboard, mouse and media codes. The default
+		mode is gamepad.
+
+		The file is created on the configuration interface of a
+		bound ASUS ROG Ally or ROG Ally X controller. A switch
+		asks the controller to change mode and resets the
+		recorded button mappings to the firmware defaults of
+		the new mode, so the remap, macro and remap_available
+		files report those defaults afterwards.
+
+		Access: Read/Write
+		Valid values: "gamepad", "desktop"
+
+What:		/sys/bus/hid/drivers/asus/<dev>/gamepad_mode_index
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		List of the operating modes the gamepad_mode file
+		accepts, space separated.
+
+		The file is created on the configuration interface of a
+		bound ASUS ROG Ally or ROG Ally X controller.
+
+		Access: Read only
+		Valid values: "gamepad desktop"
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_vibration/intensity
+		/sys/bus/hid/drivers/asus/<dev>/right_vibration/intensity
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Strength of the left or the right rumble motor, in
+		percent of the maximum intensity. The controller does
+		not keep the strength across a reset: the driver sends
+		the recorded values again when it re-initializes the
+		controller.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller. Both
+		motors default to 100 and a write is rejected when the
+		value is above 100.
+
+		Access: Read/Write
+		Valid values: an unsigned number 0..100
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_vibration/intensity_range
+		/sys/bus/hid/drivers/asus/<dev>/right_vibration/intensity_range
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Range of the values the sibling intensity file accepts.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller.
+
+		Access: Read only
+		Valid values: "0 100"
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/inner_threshold
+		/sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/inner_threshold
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Size of the inner dead zone of the joystick, in percent
+		of the axis range. The default is 10.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller. Reading
+		and writing return -EOPNOTSUPP when the controller does
+		not report user calibration support.
+
+		Access: Read/Write
+		Valid values: an unsigned number 0..50
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/outer_threshold
+		/sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/outer_threshold
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Outer threshold of the joystick, in percent of the axis
+		range: at and beyond this deflection the joystick
+		reports full deflection. The default is 90.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller. Reading
+		and writing return -EOPNOTSUPP when the controller does
+		not report user calibration support.
+
+		Access: Read/Write
+		Valid values: an unsigned number 70..100
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/inner_threshold_range
+		/sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/inner_threshold_range
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Range of the values the sibling inner_threshold file
+		accepts.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller.
+
+		Access: Read only
+		Valid values: "0 50"
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/outer_threshold_range
+		/sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/outer_threshold_range
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Range of the values the sibling outer_threshold file
+		accepts.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller.
+
+		Access: Read only
+		Valid values: "70 100"
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/anti_deadzone
+		/sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/anti_deadzone
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Anti-dead zone of the joystick, in percent of the axis
+		range: the deflection the controller applies to
+		counteract the inner dead zone. The default is 0.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller. Reading
+		and writing return -EOPNOTSUPP when the controller does
+		not report anti-dead zone support.
+
+		Access: Read/Write
+		Valid values: an unsigned number 0..100
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/anti_deadzone_range
+		/sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/anti_deadzone_range
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Range of the values the sibling anti_deadzone file
+		accepts.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller.
+
+		Access: Read only
+		Valid values: "0 100"
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/response_curve
+		/sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/response_curve
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Response curve of the joystick, as four points. Each
+		point is a pair: a move percentage, the deflection at
+		which the point is placed, and a response percentage,
+		applied from that point on.
+
+		The write is all-or-nothing: it is rejected when any
+		value is out of range or the move percentages are not
+		strictly increasing, and the recorded curve is left
+		untouched. The default is the linear curve
+		"0 0 33 33 66 66 100 100".
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller. Reading
+		and writing return -EOPNOTSUPP when the controller does
+		not report response curve support.
+
+		Access: Read/Write
+		Valid values: eight unsigned numbers 0..100, the move
+		ones strictly increasing
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/response_curve_index
+		/sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/response_curve_index
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Explanation of the format of the sibling response_curve
+		file.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller.
+
+		Access: Read only
+		Valid values: a single line of text
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_trigger/range_lower_limit
+		/sys/bus/hid/drivers/asus/<dev>/right_trigger/range_lower_limit
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Lower limit of the analog trigger activation range, in
+		percent of the trigger travel: below this deflection
+		the trigger reports zero. The default is 0.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller. Reading
+		and writing return -EOPNOTSUPP when the controller does
+		not report user calibration support.
+
+		Access: Read/Write
+		Valid values: an unsigned number 0..50
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_trigger/range_upper_limit
+		/sys/bus/hid/drivers/asus/<dev>/right_trigger/range_upper_limit
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Upper limit of the analog trigger activation range, in
+		percent of the trigger travel: at and beyond this
+		deflection the trigger reports full deflection. The
+		default is 100.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller. Reading
+		and writing return -EOPNOTSUPP when the controller does
+		not report user calibration support.
+
+		Access: Read/Write
+		Valid values: an unsigned number 70..100
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_trigger/range_lower_limit_range
+		/sys/bus/hid/drivers/asus/<dev>/right_trigger/range_lower_limit_range
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Range of the values the sibling range_lower_limit file
+		accepts.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller.
+
+		Access: Read only
+		Valid values: "0 50"
+
+What:		/sys/bus/hid/drivers/asus/<dev>/left_trigger/range_upper_limit_range
+		/sys/bus/hid/drivers/asus/<dev>/right_trigger/range_upper_limit_range
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Range of the values the sibling range_upper_limit file
+		accepts.
+
+		The files are created on the configuration interface of
+		a bound ASUS ROG Ally or ROG Ally X controller.
+
+		Access: Read only
+		Valid values: "70 100"
+
+What:		/sys/bus/hid/drivers/asus/<dev>/<btn>/remap
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Target the button is remapped to, by name. A write
+		sends the mapping to the controller and is rejected
+		when the name is unknown or not valid for the button
+		in the current mode; the recorded mapping is restored
+		when the controller rejects it.
+
+		"NONE" is always valid. In "gamepad" mode only gamepad
+		codes are valid, in "desktop" mode only keyboard,
+		mouse and media codes are; btn_m1 and btn_m2 accept
+		every code.
+
+		The file is created in one directory per button of the
+		configuration interface of a bound ASUS ROG Ally or
+		ROG Ally X controller. <btn> is one of: btn_a, btn_b,
+		btn_x, btn_y, btn_lb, btn_rb, dpad_up, dpad_down,
+		dpad_left, dpad_right, btn_l3, btn_r3, btn_menu,
+		btn_view, btn_m1 and btn_m2.
+
+		Access: Read/Write
+		Valid values: one name from the sibling
+		remap_available file
+
+What:		/sys/bus/hid/drivers/asus/<dev>/<btn>/macro
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Macro target of the button, by name. The macro target
+		is not restricted by the controller mode: every name
+		the controller knows is valid for every button.
+
+		The file is created in the <btn> directories listed
+		for the remap file.
+
+		Access: Read/Write
+		Valid values: one name; the btn_m1 and btn_m2
+		remap_available files list them all
+
+What:		/sys/bus/hid/drivers/asus/<dev>/<btn>/remap_available
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Names the remap file of this button accepts in the
+		current mode, space separated. The btn_m1 and btn_m2
+		files list every known name in every mode.
+
+		The file is created in the <btn> directories listed
+		for the remap file.
+
+		Access: Read only
+		Valid values: a single line of text
+
+What:		/sys/bus/hid/drivers/asus/<dev>/<btn>/turbo_period
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Repeat period of the turbo function of the button, in
+		firmware units: while turbo is on, the button is
+		automatically pressed again at this period. The period
+		0, the default, disables the function.
+
+		The file is created in the <btn> directories listed
+		for the remap file, only when the controller reports
+		turbo support.
+
+		Access: Read/Write
+		Valid values: an unsigned number 0..20
+
+What:		/sys/bus/hid/drivers/asus/<dev>/<btn>/toggle_period
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Toggle period of the turbo function of the button, in
+		firmware units.
+
+		The file is created in the <btn> directories listed
+		for the remap file, only when the controller reports
+		turbo support.
+
+		Access: Read/Write
+		Valid values: an unsigned number 0..255
+
+What:		/sys/bus/hid/drivers/asus/<dev>/<btn>/turbo_period_range
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Range of the values the sibling turbo_period file
+		accepts.
+
+		The file is created in the <btn> directories listed
+		for the remap file, only when the controller reports
+		turbo support.
+
+		Access: Read only
+		Valid values: "0 20"
+
+What:		/sys/bus/hid/drivers/asus/<dev>/<btn>/toggle_period_range
+Date:		October 2026
+Contact:	linux-input@vger.kernel.org
+Description:
+		Range of the values the sibling toggle_period file
+		accepts.
+
+		The file is created in the <btn> directories listed
+		for the remap file, only when the controller reports
+		turbo support.
+
+		Access: Read only
+		Valid values: "0 255"
-- 
2.47.3


^ permalink raw reply related	[flat|nested] 25+ messages in thread

* [PATCH v13 17/18] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (15 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 16/18] HID: asus: document ROG Ally " Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  2026-11-04  1:39 ` [PATCH v13 18/18] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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 a24c64c86b5f..4af4c897cb05 100644
--- a/drivers/hid/hid-asus.c
+++ b/drivers/hid/hid-asus.c
@@ -88,6 +88,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.
@@ -254,6 +268,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 {
@@ -5149,6 +5164,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;
 }
 
@@ -5529,6 +5556,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
@@ -5577,6 +5606,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;
@@ -5795,8 +5825,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);
@@ -5822,8 +5854,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] 25+ messages in thread

* [PATCH v13 18/18] MAINTAINERS: add maintainer for the ASUS HID driver
  2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
                   ` (16 preceding siblings ...)
  2026-11-04  1:39 ` [PATCH v13 17/18] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
@ 2026-11-04  1:39 ` Denis Benato
  17 siblings, 0 replies; 25+ messages in thread
From: Denis Benato @ 2026-11-04  1:39 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] 25+ messages in thread

* Re: [PATCH v13 11/18] HID: asus: add support for turbo buttons
  2026-11-04  1:39 ` [PATCH v13 11/18] HID: asus: add support for turbo buttons Denis Benato
@ 2026-11-04  1:52   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04  1:52 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:
- [Medium] HID: asus: unlocked read of turbo period in ally_btn_period_show()
- [Low] HID: asus: mix of cleanup helpers and goto in ally_create_button_attributes()
- [Low] HID: asus: non-static lookup table on stack in ally_btn_param()
--

commit 3795dba8be2afc39142aa94e1d2e669448280abb
Author: Denis Benato <denis.benato@linux.dev>

HID: asus: add support for turbo buttons

This commit adds support for configuring ROG Ally turbo and toggle button
periods via per-button sysfs attribute groups and restores these settings on
controller initialization.

> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index d3dbeba529c12..70c9d71fc5fa9 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c

[ ... ]

> @@ -2132,8 +2211,252 @@ 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];
> +}

[Severity: Low]
As reported by Sashiko on v12 [1], in ally_btn_param() in
drivers/hid/hid-asus.c, table[ALLY_BTN_MAX] maps enum ally_button_id
values to fixed addresses inside the file-scope ally_prefs.turbo struct,
but it is not declared static const. As a result, the 16-pointer lookup
array is reconstructed on the stack on every sysfs show, store, and setup
call.

Could table be declared static struct ally_btn_turbo_params *const to
match the other lookup tables in this driver?

[ ... ]

> +static ssize_t ally_btn_period_show(struct ally_btn_sysfs_entry *entry, char *buf,
> +				    enum ally_btn_period which)
> +{
> +	struct ally_btn_turbo_params *params;
> +
> +	scoped_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", *ally_btn_period_field(params, which));
> +}
> +
> +static ssize_t ally_btn_period_store(struct device *dev,
> +				     struct ally_btn_sysfs_entry *entry,
> +				     const char *buf, size_t count,
> +				     enum ally_btn_period which, u8 max,
> +				     const char *name)
> +{
> +	struct hid_device *hdev = to_hid_device(dev);
> +	struct ally_btn_turbo_params *params;
> +	u8 old_value, value, *field;
> +	int ret;
> +
> +	ret = kstrtou8(buf, 10, &value);
> +	if (ret)
> +		return ret;
> +	if (value > max)
> +		return -EINVAL;
> +
> +	scoped_guard(mutex, &ally_prefs.lock) {
> +		if (!ally_prefs.turbo_support)
> +			return -EOPNOTSUPP;
> +
> +		params = ally_btn_param(entry->btn);
> +		if (!params)
> +			return -ENODEV;
> +
> +		field = ally_btn_period_field(params, which);
> +		old_value = *field;
> +		*field = value;
> +
> +		ret = ally_set_turbo_params(hdev);
> +		if (ret) {
> +			*field = old_value;
> +			return ret;
> +		}
> +	}

[Severity: Medium]
In ally_btn_period_show() in drivers/hid/hid-asus.c, scoped_guard(mutex,
&ally_prefs.lock) only covers checking ally_prefs.turbo_support and
fetching the params pointer into ally_prefs.turbo via ally_btn_param().
The mutex is released when the scoped_guard block exits, before
sysfs_emit() dereferences *ally_btn_period_field(params, which).

Meanwhile, ally_btn_period_store() modifies *field in-place inside
ally_prefs.turbo under ally_prefs.lock before blocking on the feature
report transfer in ally_set_turbo_params(hdev), and rolls *field back to
old_value if that transfer fails.

Can a concurrent read on turbo_period or toggle_period race with
ally_btn_period_store() and observe an uncommitted value that is
subsequently rolled back on error? Should ally_btn_period_show() use
guard(mutex)(&ally_prefs.lock) (or read the byte into a local variable
inside the guarded scope) so the read is serialized by ally_prefs.lock?

[ ... ]

> +static struct ally_btn_sysfs_entry *ally_create_button_attributes(struct hid_device *hdev)
> +{
> +	int i, ret;
> +
> +	scoped_guard(mutex, &ally_prefs.lock)
> +		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);
> +}

[ ... ]

> @@ -2192,7 +2515,15 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
>  		}
>  	}
>  
> +	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;
> +	}
> +
>  	scoped_guard(mutex, &ally_prefs.lock) {
> +		intf->button_entries = entries;
>  		intf->cal_sysfs_live = cal_live;
>  		intf->sysfs_live = true;
>  	}

[Severity: Low]
As reported by Sashiko on v12 [2], in ally_create_button_attributes() in
drivers/hid/hid-asus.c, scope-based cleanup helpers (scoped_guard and
entries __free(kfree)) are mixed with goto ally_btn_err_cleanup in the
same function, and ally_prefs_setup() similarly mixes scoped_guard with
goto err_cal_groups.

The kernel cleanup API conventions require not mixing scope-based cleanup
helpers with goto-based error handling within the same function. Could
ally_create_button_attributes() either avoid goto (for example by calling
 a helper to unwind the created groups before returning Directly) or manage
entries with explicit kfree() on the error path?

[1] https://lore.kernel.org/r/20261031174130.208240-12-denis.benato@linux.dev
[2] https://lore.kernel.org/r/20261031174130.208240-12-denis.benato@linux.dev

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=11

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration
  2026-11-04  1:39 ` [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration Denis Benato
@ 2026-11-04  1:53   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04  1:53 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:
- [Low] HID: asus: ally_prefs_setup() mixes scoped_guard() with goto error handling
--

commit 23b7fd753beca8abc912e5e22d7a6deed94442a0
Author: Denis Benato <denis.benato@linux.dev>

HID: asus: add joysticks inner and outer range configuration

This commit adds sysfs attributes for configuring the inner and outer
joystick thresholds on ASUS ROG Ally devices and restores those threshold
settings on device initialization.

> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 459061391c378..a89ce83ae93f4 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c

[ ... ]

> @@ -1182,15 +1401,44 @@ static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
>  		}
>  	}
>  
> +	scoped_guard(mutex, &ally_prefs.lock)
> +		cal_live = ally_prefs.user_cal_support ||
> +			ally_prefs.anti_deadzone_support ||
> +			ally_prefs.resp_curve_support;
> +
> +	/* Skip the calibration groups when the capability is missing. */
> +	if (cal_live) {
> +		for (; cal_n < ARRAY_SIZE(ally_cal_attr_groups); cal_n++) {
> +			ret = sysfs_create_group(&hdev->dev.kobj,
> +						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_n]->name, ret);
> +				goto err_cal_groups;
> +			}
> +		}
> +	}
> +
>  	scoped_guard(mutex, &ally_prefs.lock) {
> +		intf->cal_sysfs_live = cal_live;
>  		intf->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 ally_prefs_setup() uses scoped_guard() while also
introducing a goto err_cal_groups error path when sysfs_create_group()
fails for ally_cal_attr_groups.

Since the cleanup API guidelines discourage mixing scope-based cleanup
helpers with goto-based error handling in the same function, could the
unwinding be done inline (similar to the earlier ally_attr_groups loop in
ally_prefs_setup()), or by using sysfs_create_groups() and
sysfs_remove_groups() with a NULL-terminated array to avoid the goto label?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=5

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v13 16/18] HID: asus: document ROG Ally sysfs attributes
  2026-11-04  1:39 ` [PATCH v13 16/18] HID: asus: document ROG Ally " Denis Benato
@ 2026-11-04  1:55   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04  1:55 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: inverted sysfs_streq() check in find_button_by_name()
- [Low] HID: asus: response_curve_index ABI doc mismatches multi-line output
--

commit 82b20f9d7b18cb0817f799140de2a8d33e0549b4
Author: Denis Benato <denis.benato@linux.dev>

HID: asus: document ROG Ally sysfs attributes

This commit documents the sysfs attributes exposed by hid-asus for configuring
the ASUS ROG Ally and ROG Ally X gamepad controller, including emulation mode,
vibration, joystick calibration, trigger limits, and button mappings.

> diff --git a/Documentation/ABI/testing/sysfs-driver-hid-asus b/Documentation/ABI/testing/sysfs-driver-hid-asus
> index 30e0d17d4cecc..515eb7c550fc5 100644
> --- a/Documentation/ABI/testing/sysfs-driver-hid-asus
> +++ b/Documentation/ABI/testing/sysfs-driver-hid-asus
> @@ -93,3 +93,392 @@ Description:
>  
>  		Access: Read only
>  		Valid values: a single line of text
> +

[ ... ]

> +What:		/sys/bus/hid/drivers/asus/<dev>/left_joystick_axis/response_curve_index
> +		/sys/bus/hid/drivers/asus/<dev>/right_joystick_axis/response_curve_index
> +Date:		October 2026
> +Contact:	linux-input@vger.kernel.org
> +Description:
> +		Explanation of the format of the sibling response_curve
> +		file.
> +
> +		The files are created on the configuration interface of
> +		a bound ASUS ROG Ally or ROG Ally X controller.
> +
> +		Access: Read only
> +		Valid values: a single line of text

[Severity: Low]
Does response_curve_index actually return a single line of text?

In drivers/hid/hid-asus.c, response_curve_index_show() emits six lines of text
with embedded newlines:

drivers/hid/hid-asus.c:response_curve_index_show() {
	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");
}

Should this entry describe the multi-line output, or should
response_curve_index_show() emit a single line?

[ ... ]

> +What:		/sys/bus/hid/drivers/asus/<dev>/<btn>/remap
> +Date:		October 2026
> +Contact:	linux-input@vger.kernel.org
> +Description:
> +		Target the button is remapped to, by name. A write
> +		sends the mapping to the controller and is rejected
> +		when the name is unknown or not valid for the button
> +		in the current mode; the recorded mapping is restored
> +		when the controller rejects it.
> +
> +		"NONE" is always valid. In "gamepad" mode only gamepad
> +		codes are valid, in "desktop" mode only keyboard,
> +		mouse and media codes are; btn_m1 and btn_m2 accept
> +		every code.
> +
> +		The file is created in one directory per button of the
> +		configuration interface of a bound ASUS ROG Ally or
> +		ROG Ally X controller. <btn> is one of: btn_a, btn_b,
> +		btn_x, btn_y, btn_lb, btn_rb, dpad_up, dpad_down,
> +		dpad_left, dpad_right, btn_l3, btn_r3, btn_menu,
> +		btn_view, btn_m1 and btn_m2.
> +
> +		Access: Read/Write
> +		Valid values: one name from the sibling
> +		remap_available file

[Severity: High]
Does the driver implementation match this documented behavior for <btn>/remap
and <btn>/macro?

In drivers/hid/hid-asus.c, find_button_by_name() negates sysfs_streq():

drivers/hid/hid-asus.c:find_button_by_name() {
	for (i = 0; i < ARRAY_SIZE(ally_btn_codes); i++) {
		if (!sysfs_streq(ally_btn_codes[i].name, name))
			return &ally_btn_codes[i];
	}

	return NULL;
}

Since sysfs_streq() returns true when the strings match, !sysfs_streq()
returns the first entry in ally_btn_codes[] whose name does not match name.

Because ally_btn_codes[0] is "NONE" and ally_btn_codes[1] is "BTN_SOUTH",
calling find_button_by_name("NONE") returns "BTN_SOUTH", while calling it with
any other string (including unknown names in button_remap_store()) matches
index 0 and returns "NONE" instead of NULL.

Consequently, ally_set_button_pair_mappings_default() initializes every button
and macro slot to "BTN_SOUTH", ally_set_default_gamepad_mapping() and
ally_set_default_keyboard_mapping() set explicitly mapped buttons to "NONE",
and writes to <btn>/remap or <btn>/macro never reject unknown names with
-EINVAL.

Should the negation in find_button_by_name() be removed?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=16

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v13 12/18] HID: asus: add support for btn remapping
  2026-11-04  1:39 ` [PATCH v13 12/18] HID: asus: add support for btn remapping Denis Benato
@ 2026-11-04  1:58   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04  1:58 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: inverted sysfs_streq() check in find_button_by_name()
- [Medium] HID: asus: inconsistent pair update and rollback in button_remap_store()
- [Low] HID: asus: mixed cleanup helpers and goto in button_remap_store()
--

commit 72acf6c4d3c76532f8563fc72f2841557666f18a
Author: Denis Benato <denis.benato@linux.dev>

HID: asus: add support for btn remapping

This commit adds sysfs support for remapping physical buttons and assigning
macro codes on Asus ROG Ally devices, updating the cached mappings when the
gamepad mode changes.

> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 70c9d71fc5fa9..dcb3c9f5d23e8 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c

[ ... ]

> @@ -978,10 +1186,587 @@ static struct ally_prefs {
>  	struct ally_joystick_resp_curve right_curve;
>  
>  	struct ally_turbo_config turbo;
> +	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
> + *
> + * This function deals with data from userspace therefore it
> + * must not trust the input blindly.
> + *
> + * 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 (!sysfs_streq(ally_btn_codes[i].name, name))
> +			return &ally_btn_codes[i];
> +	}
> +
> +	return NULL;
> +}

[Severity: High]
Is the sysfs_streq() condition in find_button_by_name() inverted?

Unlike strcmp(), sysfs_streq() returns true when the strings match. Because
ally_btn_codes[0] is "NONE" and ally_btn_codes[1] is "BTN_SOUTH", negating
sysfs_streq() causes find_button_by_name("NONE") in
ally_set_button_pair_mappings_default() to return &ally_btn_codes[1]
(BTN_SOUTH), and causes lookups for any other button name (or invalid strings
written by userspace to button_remap_store()) to immediately return
&ally_btn_codes[0] (NONE).

When hid_asus_ally_init() calls ally_apply_button_mappings(), or when
gamepad_mode_store() or button_remap_store() updates mappings, all default
remaps resolve to NONE, all default macros resolve to BTN_SOUTH, and invalid
button names from userspace never fail with -EINVAL.

[ ... ]

> +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;
> +	const struct btn_code_map *old_code, *code;
> +	const struct btn_code_map **target_map;
> +	struct ally_button_mapping *mode_map;
> +	enum btn_pair_index pair_idx;
> +	struct button_pair_map *pair;
> +	struct button_map *btn_map;
> +	bool is_first;
> +	int ret, i;
> +
> +	code = find_button_by_name(buf);
> +	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;
> +
> +	target_map = btn_attr->is_macro ? &btn_map->macro : &btn_map->remap;
> +	old_code = *target_map;
> +	*target_map = 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:
> +	*target_map = old_code;
> +
> +	/* Try to recover the mapping presented to the user. */
> +	ally_set_button_mapping(hdev, pair);
> +	goto err_out;
> +
> +err_restore:
> +	*target_map = old_code;
> +
> +err_out:
> +	hid_err(hdev, "Failed to set button mapping: %d\n", ret);
> +	return ret;
> +}

[Severity: Medium]
Does the err_restore_hw path in button_remap_store() properly roll back the
hardware state on first-generation ROG Ally devices?

In ally_detect_capabilities(), ally_prefs.is_ally_x is set only for the second-
generation ROG Ally X (USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X). In
button_remap_store(), the comment says first-generation devices require
individual button pair updates, yet the code sends a single pair when
ally_prefs.is_ally_x is true and loops over all 9 pairs in
mode_map->button_pairs when ally_prefs.is_ally_x is false.

Furthermore, err_restore_hw is only reachable from the !ally_prefs.is_ally_x
loop when ally_set_button_mapping() fails at iteration i > pair_idx - 1, but
err_restore_hw then calls ally_set_button_mapping(hdev, pair) for only that
single pair. If !ally_prefs.is_ally_x devices require all 9 pairs to be sent,
wouldn't a single-pair update in err_restore_hw fail to restore the hardware
mapping, or if single-pair updates work on !ally_prefs.is_ally_x, why does the
else branch send all 9 pairs?

[ ... ]

> @@ -2399,11 +3200,11 @@ 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)
>  {
> +	size_t entries_attr_index;
>  	int i, ret;
>  
>  	scoped_guard(mutex, &ally_prefs.lock)
> -		if (!ally_prefs.turbo_support)
> -			return NULL;
> +		entries_attr_index = ally_prefs.turbo_support ? 4 : 0;
>  
>  	struct ally_btn_sysfs_entry *entries __free(kfree) = kzalloc_objs(*entries, ALLY_BTN_MAX);
>  	if (!entries)
> @@ -2412,13 +3213,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;
>  
> @@ -2436,6 +3257,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);
>  }

[Severity: Low]
As reported by Sashiko on v12 [1], this isn't a bug, but button_remap_store()
mixes guard(mutex)(&ally_prefs.lock) with goto labels (err_restore_hw,
err_restore, and err_out) while also holding ally_prefs.lock across
sysfs_notify(), and ally_create_button_attributes() similarly mixes
scoped_guard() and __free(kfree) on entries with goto ally_btn_err_cleanup.

Could button_remap_store() and ally_create_button_attributes() avoid mixing
scope-based cleanup helpers with goto error paths (and could remap_attr and
macro_attr be embedded directly in struct ally_btn_sysfs_entry like
attr_turbo_period, attr_toggle_period, and attr_remap_available)?

[1] https://lore.kernel.org/r/sashiko-outbox-192087@kernel.org

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=12

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v13 03/18] HID: asus: add gamepad configuration
  2026-11-04  1:39 ` [PATCH v13 03/18] HID: asus: add gamepad configuration Denis Benato
@ 2026-11-04  1:59   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04  1:59 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:
- [Medium] HID: asus: unlocked feature report transfers in hid_asus_ally_reset_resume()
- [Low] HID: asus: ally_detect_capabilities() mixes guard(mutex) with goto
- [Low] HID: asus: open-coded sysfs group helpers in ally_prefs_setup()
--

commit 23ba9584ee0ff2df9f8412928af4abbe35ed412d
Author: Denis Benato <denis.benato@linux.dev>

HID: asus: add gamepad configuration

This commit adds gamepad capability detection and preference storage for
ASUS ROG Ally devices, exposes the xbox_controller sysfs attribute, and
waits for the gamepad MCU to report ready during reset_resume.

> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 2a3e8c399f0aa..8277d7108bb97 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c

[ ... ]

> @@ -762,6 +776,281 @@ static int ally_get_endpoint_address(struct hid_device *hdev)
>  	return ep->desc.bEndpointAddress;
>  }

[ ... ]

> +static int ally_detect_capabilities(struct hid_device *hdev)
> +{
> +	int ret;
> +
> +	guard(mutex)(&ally_prefs.lock);
> +
> +	ally_prefs.is_ally_x = (hdev->product == USB_DEVICE_ID_ASUSTEK_ROG_NKEY_ALLY_X);
> +
> +	ret = ally_check_capability(hdev, CMD_CHECK_XBOX_SUPPORT);
> +	if (ret < 0)
> +		goto ally_detect_capabilities_err;
> +	ally_prefs.xbox_controller_support = ret;
> +
> +
> +	ret = ally_check_capability(hdev, CMD_CHECK_USER_CAL_SUPPORT);
> +	if (ret < 0)
> +		goto ally_detect_capabilities_err;
> +	ally_prefs.user_cal_support = ret;
> +
> +	ret = ally_check_capability(hdev, CMD_CHECK_TURBO_SUPPORT);
> +	if (ret < 0)
> +		goto ally_detect_capabilities_err;
> +	ally_prefs.turbo_support = ret;
> +
> +	ret = ally_check_capability(hdev, CMD_CHECK_RESP_CURVE_SUPPORT);
> +	if (ret < 0)
> +		goto ally_detect_capabilities_err;
> +	ally_prefs.resp_curve_support = ret;
> +
> +	ret = ally_check_capability(hdev, CMD_CHECK_DIR_TO_BTN_SUPPORT);
> +	if (ret < 0)
> +		goto ally_detect_capabilities_err;
> +	ally_prefs.dir_to_btn_support = ret;
> +
> +	ret = ally_check_capability(hdev, CMD_CHECK_GYRO_TO_JOYSTICK);
> +	if (ret < 0)
> +		goto ally_detect_capabilities_err;
> +	ally_prefs.gyro_support = ret;
> +
> +	ret = ally_check_capability(hdev, CMD_CHECK_ANTI_DEADZONE);
> +	if (ret < 0)
> +		goto ally_detect_capabilities_err;
> +	ally_prefs.anti_deadzone_support = ret;
> +
> +	return 0;
> +ally_detect_capabilities_err:
> +	hid_err(hdev, "Failed to detect capabilities: %d\n", ret);
> +	return ret;
> +}

[Severity: Low]
This isn't a bug, but ally_detect_capabilities() in drivers/hid/hid-asus.c
mixes guard(mutex)(&ally_prefs.lock) with goto ally_detect_capabilities_err,
contrary to the cleanup.h convention of not mixing scope-based cleanup
helpers with goto error paths in the same function.

Since ally_check_capability() and the caller hid_asus_ally_probe() already
log hid_err() on failure, and ally_detect_capabilities_err performs no
resource cleanup, could each failed check in ally_detect_capabilities()
simply return ret directly?

[ ... ]

> +static int ally_prefs_setup(struct hid_device *hdev, struct ally_intf *intf)
> +{
> +	int ret, i;
> +
> +	ret = ally_detect_capabilities(hdev);
> +	if (ret)
> +		return ret;
> +
> +	scoped_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 (intf->sysfs_live)
> +			return 0;
> +	}
> +
> +	/*
> +	 * Create and remove sysfs groups without ally_prefs.lock. Attribute
> +	 * methods take that lock while holding a kernfs active reference;
> +	 * removing a group drains those references, so locking here would
> +	 * invert the order and deadlock.
> +	 */
> +	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;
> +		}
> +	}
> +
> +	scoped_guard(mutex, &ally_prefs.lock) {
> +		intf->sysfs_live = true;
> +	}
> +
> +	return 0;
> +}
> +
> +static void ally_sysfs_remove(struct hid_device *hdev, struct ally_intf *intf)
> +{
> +	int i;
> +
> +	if (!intf)
> +		return;
> +
> +	if (ally_get_endpoint_address(hdev) != HID_ALLY_INTF_CFG_IN)
> +		return;
> +
> +	scoped_guard(mutex, &ally_prefs.lock) {
> +		if (!intf->sysfs_live)
> +			return;
> +
> +		intf->sysfs_live = false;
> +	}
> +
> +	for (i = 0; i < ARRAY_SIZE(ally_attr_groups); i++)
> +		sysfs_remove_group(&hdev->dev.kobj, &ally_attr_groups[i]);
> +}

[Severity: Low]
This isn't a bug, but ally_prefs_setup() and ally_sysfs_remove() in
drivers/hid/hid-asus.c open-code the iteration and error rollback over
ally_attr_groups using sysfs_create_group() and sysfs_remove_group().

Could ally_attr_groups be defined with ATTRIBUTE_GROUPS() (or a
NULL-terminated struct attribute_group pointer array) so ally_prefs_setup()
and ally_sysfs_remove() can use sysfs_create_groups() and
sysfs_remove_groups() instead?

[ ... ]

> @@ -944,6 +1233,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;
>  }
>  
> @@ -1038,6 +1335,12 @@ static struct ally_intf *hid_asus_ally_probe(struct hid_device *hdev)
>  			return ERR_PTR(ret);
>  		}
>  
> +		ret = ally_prefs_setup(hdev, intf);
> +		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:
> @@ -1058,7 +1361,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
> @@ -1071,6 +1374,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);
>  }

[Severity: Medium]
Can a concurrent write to xbox_controller_store() race with the unlocked
feature report transfers in hid_asus_ally_reset_resume() and
hid_asus_ally_init()?

Once ally_prefs_setup() exposes the sysfs group on hdev->dev.kobj,
xbox_controller_store() remains live across suspend and reset_resume (as
well as during the rest of hid_asus_ally_probe()):

    xbox_controller_store()
      guard(mutex)(&ally_prefs.lock)
      ally_set_xbox_controller()
        ally_gamepad_send_packet()
          ally_dev_set_report() /* feature report 0x5A */

However, hid_asus_ally_reset_resume() runs ally_gamepad_check_ready()
without holding ally_prefs.lock, where each attempt performs a two-step
command and response sequence over USB control transfers:

    hid_asus_ally_reset_resume()
      ally_gamepad_check_ready()
        ally_gamepad_send_receive_packet()
          ally_dev_set_report() /* feature report 0x5A */
          ally_dev_get_report() /* feature report 0x0D */

Similarly, hid_asus_ally_init() sends ally_force_feedback_off via
ally_gamepad_send_packet() before acquiring ally_prefs.lock.

If userspace writes to xbox_controller during hid_asus_ally_reset_resume()
or hid_asus_ally_init(), can CMD_SET_XBOX_CONTROLLER be sent before the MCU
is ready or interleave between ally_dev_set_report() and
ally_dev_get_report() in ally_gamepad_check_ready(), causing the readiness
check or configuration command to fail?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=3

^ permalink raw reply	[flat|nested] 25+ messages in thread

* Re: [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds
  2026-11-04  1:39 ` [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds Denis Benato
@ 2026-11-04  2:07   ` sashiko-bot
  0 siblings, 0 replies; 25+ messages in thread
From: sashiko-bot @ 2026-11-04  2:07 UTC (permalink / raw)
  To: Denis Benato; +Cc: linux-input, dmitry.torokhov

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] HID: asus: use-after-free of struct ally_intf in asus_resume()
- [Medium] HID: asus: ally_vendor_keys() omits 0x93 (ROG Ally X AC button)
- [Medium] HID: asus: duplicate gamepad input_dev registered due to mapping report 0x0B
- [Medium] HID: asus: ally_dev_get_report() sizes 0x0D read from 0x5A report length
--

commit 964cb88b58989515f1ea0adc56e4d20719f664a7
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 handhelds, including
device ready-state initialization, Ally X gamepad input mapping, vendor
key handling, and Ctrl+Alt+Del sequence filtering.

> diff --git a/drivers/hid/hid-asus.c b/drivers/hid/hid-asus.c
> index 46643022b68d0..2a3e8c399f0aa 100644
> --- a/drivers/hid/hid-asus.c
> +++ b/drivers/hid/hid-asus.c

[ ... ]

> @@ -239,12 +285,816 @@ static const struct asus_touchpad_info medion_e1239t_tp = {

[ ... ]

> +static bool ally_vendor_keys(struct input_dev *input, const u8 *data, int size)
> +{
> +	bool is_vendor;
> +
> +	if (size < 2 || data[0] != FEATURE_KBD_REPORT_ID)
> +		return false;
> +
> +	is_vendor = data[1] == 0x38 || data[1] == 0xA6 ||
> +		    data[1] == 0xA7 || data[1] == 0xA8;
> +
> +	/* Neither a vendor press nor a release: leave it to the other handlers. */
> +	if (!is_vendor && data[1] != 0)
> +		return false;
> +
> +	if (input) {
> +		/* AC short press */
> +		input_report_key(input, KEY_PROG1, data[1] == 0x38);
> +
> +		/* CC short press & release */
> +		input_report_key(input, KEY_F16,   data[1] == 0xA6);
> +
> +		/* AC long press */
> +		input_report_key(input, KEY_F17,   data[1] == 0xA7);
> +
> +		/* AC long release */
> +		input_report_key(input, KEY_F18,   data[1] == 0xA8);
> +
> +		input_sync(input);
> +	}
> +
> +	/*
> +	 * Only the vendor codes are consumed. They are reported on this
> +	 * interface's evdev above, so hid-core never sees them pressed. Note
> +	 * that 0xA8 ("AC long release") is itself a usage the firmware sends,
> +	 * not the report that clears one. Everything else, including the 0x00
> +	 * report that clears a held usage, must still reach hid-core: it can
> +	 * only release the usages it saw pressed if it sees the report that
> +	 * clears them.
> +	 */
> +	return is_vendor;
> +}

[Severity: Medium]
Should ally_vendor_keys() also check for 0x93 when handling KEY_PROG1?

Later in asus_input_mapping(), this patch maps usage 0x93 to KEY_PROG1 as
the ROG Ally X AC button:

    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 */

Because ally_vendor_keys() only checks data[1] against 0x38, 0xA6, 0xA7,
and 0xA8, a report with data[1] == 0x93 on the ROG Ally X returns false
and falls through to hid-core, which records 0x93 in field->value[0] and
reports KEY_PROG1 = 1.

If the release report (data[1] == 0x00) is missed across suspend/resume
(where ally_resume_work_fn() -> ally_release_vendor_keys() clears
KEY_PROG1 directly on intf->input without clearing hid-core's
field->value[]), or if the firmware transitions from 0x93 to 0xA7 (AC
long press, which ally_vendor_keys() consumes while reporting
KEY_PROG1 = 0), field->value[0] in hid-core will retain 0x93 while
intf->input has KEY_PROG1 released. Wouldn't hid-core then ignore the
next 0x93 press because 0x93 is still present in field->value[]?

[ ... ]

> +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 the buffer from
FEATURE_KBD_REPORT_ID (0x5A) via asus_report_len(), and
ally_gamepad_send_receive_packet() passes that length to
ally_dev_get_report() to read HID_ALLY_GET_REPORT_ID (0x0D).

Since asus_kbd_get_report() was moved just above ally_dev_set_report(),
should ally_dev_get_report() call asus_kbd_get_report(hdev,
HID_ALLY_GET_REPORT_ID, out, len) instead of calling hid_hw_raw_request()
directly? That would both size the GET_REPORT transfer from report 0x0D's
descriptor entry and return 0 on success as documented in the kernel-doc
for ally_gamepad_send_receive_packet().

[ ... ]

> @@ -512,6 +1363,19 @@ 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) {
> +		/*
> +		 * 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]
As reported by Sashiko on v12 [2], does returning -1 in asus_raw_event()
actually prevent hid-core from creating a duplicate generic gamepad input
device for report 0x0B?

During asus_probe(), hid_hw_start(hdev, HID_CONNECT_DEFAULT) runs
hidinput_connect() -> hidinput_configure_usages() -> asus_input_mapping()
before any raw events arrive. Because asus_input_mapping() does not return
-1 for usages in report 0x0B (HID_ALLY_X_INPUT_REPORT), hid-core maps all
of the standard gamepad usages in report 0x0B and registers a generic
gamepad input_dev on that interface in addition to 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 the duplicate input
device is not created?

[ ... ]

> @@ -1569,6 +2336,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 +2347,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;
>  }

[ ... ]

> @@ -1763,6 +2564,7 @@ static int asus_probe(struct hid_device *hdev, const struct hid_device_id *id)
>  static void asus_remove(struct hid_device *hdev)
>  {
>  	struct asus_drvdata *drvdata = hid_get_drvdata(hdev);
> +	struct ally_intf *ally;
>  
>  	if (drvdata->listener.brightness_set)
>  		asus_hid_unregister_listener(&drvdata->listener);
> @@ -1770,6 +2572,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 = hid_asus_ally_terminate(drvdata);
> +		asus_worker_stop(drvdata->worker);
> +		hid_hw_stop(hdev);
> +		ally_free(ally);
> +		return;
> +	}

[Severity: High]
As reported by Sashiko on v12 [3], can asus_resume() access freed memory
in intf->resume_work if it races with asus_remove() or the err_stop_hw
path in asus_probe()?

In hid_asus_ally_terminate() -> ally_quiesce(), disable_delayed_work_sync()
marks intf->resume_work disabled in intf->resume_work.work.data so a
concurrent asus_resume() cannot re-queue it, and then ally_free(ally)
frees the struct ally_intf with kfree().

However, if asus_resume() has already loaded the non-NULL drvdata->ally
pointer via smp_load_acquire(&drvdata->ally) before
hid_asus_ally_terminate() clears drvdata->ally, and reaches
schedule_delayed_work(&intf->resume_work, ...) after ally_free(ally) runs,
schedule_delayed_work() will access and modify intf->resume_work.work.data
after intf has been freed. Could intf be allocated with devm_kzalloc() on
&hdev->dev (or freed via kfree_rcu() with RCU read protection in
asus_resume()) so that intf->resume_work remains valid until the driver
is unbound?

[1] https://lore.kernel.org/r/20261025235232.129945-16-denis.benato@linux.dev
[2] https://lore.kernel.org/r/20261031174130.208240-3-denis.benato@linux.dev
[3] https://lore.kernel.org/r/20261031174130.208240-3-denis.benato@linux.dev

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261104013935.512713-1-denis.benato@linux.dev?part=2

^ permalink raw reply	[flat|nested] 25+ messages in thread

end of thread, other threads:[~2026-11-04  2:07 UTC | newest]

Thread overview: 25+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-04  1:39 [PATCH v13 00/18] HID: asus: add support for ROG Ally handhelds Denis Benato
2026-11-04  1:39 ` [PATCH v13 01/18] HID: asus: size feature reports from the HID report descriptor Denis Benato
2026-11-04  1:39 ` [PATCH v13 02/18] HID: asus: add support for ROG Ally handhelds Denis Benato
2026-11-04  2:07   ` sashiko-bot
2026-11-04  1:39 ` [PATCH v13 03/18] HID: asus: add gamepad configuration Denis Benato
2026-11-04  1:59   ` sashiko-bot
2026-11-04  1:39 ` [PATCH v13 04/18] HID: asus: add vibration strength configuration Denis Benato
2026-11-04  1:39 ` [PATCH v13 05/18] HID: asus: add joysticks inner and outer range configuration Denis Benato
2026-11-04  1:53   ` sashiko-bot
2026-11-04  1:39 ` [PATCH v13 06/18] HID: asus: add triggers " Denis Benato
2026-11-04  1:39 ` [PATCH v13 07/18] HID: asus: add joysticks anti-deadzone configuration Denis Benato
2026-11-04  1:39 ` [PATCH v13 08/18] HID: asus: add support for response curve Denis Benato
2026-11-04  1:39 ` [PATCH v13 09/18] HID: asus: add support to force feedback Denis Benato
2026-11-04  1:39 ` [PATCH v13 10/18] HID: asus: add support for gamepad mode Denis Benato
2026-11-04  1:39 ` [PATCH v13 11/18] HID: asus: add support for turbo buttons Denis Benato
2026-11-04  1:52   ` sashiko-bot
2026-11-04  1:39 ` [PATCH v13 12/18] HID: asus: add support for btn remapping Denis Benato
2026-11-04  1:58   ` sashiko-bot
2026-11-04  1:39 ` [PATCH v13 13/18] HID: asus: identify XG Mobile stations and bind the LED to the reported model Denis Benato
2026-11-04  1:39 ` [PATCH v13 14/18] HID: asus: expose XG Mobile identity and firmware information Denis Benato
2026-11-04  1:39 ` [PATCH v13 15/18] HID: asus: document ROG XG Mobile sysfs attributes Denis Benato
2026-11-04  1:39 ` [PATCH v13 16/18] HID: asus: document ROG Ally " Denis Benato
2026-11-04  1:55   ` sashiko-bot
2026-11-04  1:39 ` [PATCH v13 17/18] platform/x86: asus-armoury: pick the eGPU activation from the reported GPU Denis Benato
2026-11-04  1:39 ` [PATCH v13 18/18] MAINTAINERS: add maintainer for the ASUS HID driver Denis Benato

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox