HID: ayaneo: Add AYANEO 3 detachable controller driver - #3
Conversation
| /* Input reports are not delivered during probe by default */ | ||
| hid_device_io_start(hdev); | ||
|
|
||
| mutex_lock(&aya->lock); |
There was a problem hiding this comment.
here I would use scoped_guard to spare the line of mutex_unlock
| if (ret) | ||
| hid_warn(hdev, "controller did not answer status check: %d\n", | ||
| ret); | ||
| else |
There was a problem hiding this comment.
kernel practice is not to print anything when things go as planned
| F: drivers/spi/spi-axiado.c | ||
| F: drivers/spi/spi-axiado.h | ||
|
|
||
| AYANEO 3 CONTROLLER HID DRIVER |
There was a problem hiding this comment.
I have rarely seen hid devices requiring an entry in MAINTAINERS, are you sure?
There was a problem hiding this comment.
It would only be used for people to send patches, and the module author is most likely to be the one to test it. I see no issues here.
| return 0; | ||
| } | ||
|
|
||
| /* Send the command in aya->xfer and wait for the echoing reply. */ |
There was a problem hiding this comment.
I think this function deserves a kernel-doc also explaining arguments and the locking
|
|
||
| static void aya3_checksum(u8 *buf) | ||
| { | ||
| unsigned int sum = 0; |
There was a problem hiding this comment.
personally I would use a fixed-width type here, like u32 or u64 depending on if the result would fit the u32.... Maybe a u16 can also work? Especially since you then use put_unaligned_le16
| return ret; | ||
| } | ||
|
|
||
| static void aya3_remove(struct hid_device *hdev) |
There was a problem hiding this comment.
I can guarantee you sahiko-bot is going to cry over this with a bunch of "what if user uses sysfs attributes while a remove is started?"
| static int aya3_send_config(struct aya3 *aya, u8 eject) | ||
| { | ||
| u8 *buf = aya->xfer; | ||
| u8 mode = AYA3_RGB_SOLID; |
There was a problem hiding this comment.
I would do here u8 mode = (led_on_condition) ? AYA3_RGB_SOLID : AYA3_RGB_OFF; and spare the next two lines.
| if (!aya->rgb[0] && !aya->rgb[1] && !aya->rgb[2]) | ||
| mode = AYA3_RGB_OFF; | ||
|
|
||
| memset(buf, 0, AYA3_REPORT_SIZE); |
There was a problem hiding this comment.
I think I would do it the other way around: create a const u8 buf[SIZE] = {}; that will be zero-filled automatically on unspecified elements and then copy that to the dma buffer.
| for (i = 0; i < 3; i++) | ||
| aya->rgb[i] = min_t(unsigned int, aya->subleds[i].brightness, 255); | ||
|
|
||
| ret = aya3_send_config(aya, 0); |
There was a problem hiding this comment.
Since on error IDK what happens I would use a hid_err here in case ret has unexpected values
| aya->mcled.subled_info = aya->subleds; | ||
| aya->mcled.num_colors = 3; | ||
|
|
||
| cdev->name = "ayaneo:rgb:joystick_rings"; |
There was a problem hiding this comment.
sahiko-bot is going to complain about the name with a "what if an aya3 spoofed device is being emulated?". I would suggest doing what hid-asus does and compose this name with a dynamic part.
|
Thanks for the thorough review @NeroReflex! All addressed, I pushed each point as a separate commit for easy re-review (I'll squash everything back into the single patch before this goes to LKML):
I think the current code works well in two spots, let me know if you see it differently:
checkpatch --strict is clean on all the new commits, and I re-tested the updated driver on my AYANEO 3: probe is now silent, module type reads work, and the renamed LED sets/clears the joystick rings correctly (which also exercises the template-built config command end-to-end, since the device has to ACK it). |
I'm not sure adding the static is good idea: first time it will get populated and later on not touched... Are you sure driver still works? Beside it adds to the .bss without any real reason. I thing const is enough here.
sashiko-bot will tell you: what if dev_name(...) is NULL?
Maybe it can be moved above so that the kernel doesn't even try reading the descriptor if it's not a USB?
|
|
You should merge every modification into the single patch, then do a [NOT-FOR-UPSTREAM] patch that adds to the fragment in this repo the CONFIG_AYANEO required to build the driver so that the github workflow can compile the driver. |
The AYANEO 3 handheld has a detachable controller with swappable
modules ("Magic Modules"). The controller exposes three USB HID
interfaces behind 1c4f:0002 (a generic SigmaMicro VID/PID, hence the
DMI gate): a gamepad, a keyboard for the extra buttons, and a vendor
interface accepting 65-byte commands.
Add a driver for the vendor interface providing module identification
(module_left/module_right sysfs attributes), software eject of the
modules (eject sysfs attribute, blocking until the firmware confirms
the release handshake), and RGB control of the joystick rings as a
multicolor LED class device named ayaneo:rgb:joystick_rings, matching
the name InputPlumber already expects for this device.
This complements the ayaneo-ec platform driver, which exposes module
attach state and controller power. A full physical eject is performed
by writing to eject and then cutting power through ayaneo-ec's
controller_power attribute; that orchestration is deliberately left
to userspace.
The protocol was reverse engineered in the Handheld Daemon project by
Antheas Kapenekakis. Tested on an AYANEO 3 (7.2.0-ogc4.1): module
identification, RGB, and a full eject/reinsert/repower cycle.
Signed-off-by: Matías Martínez <hello@matias.me>
Lets the build workflow compile the new driver. The real OGC config change is OpenGamingCollective/kernel-packages#35, which lands once the driver merges. Signed-off-by: Matías Martínez <hello@matias.me>
55e0f67 to
501d2cc
Compare
|
@NeroReflex Thank you! Done on both process points: everything is squashed back into the single On the three code points:
Current state: I think the earlier CI failure was indeed the config gate flagging the missing |
|
Perfect, thank you. As soon as CI compiles the driver I will merge |
|
I asked claude to review one of my drivers. It has said this, I will paste it because I think it's useful to you too. Feel free to start from a HEAD prior to my merge and reopen another PR. I will take care of the rest. Good find — and this one's subtler than a simple missing check. hid_is_usb(hdev) only inspects hdev->bus: static inline bool hid_is_usb(const struct hid_device *hdev)
{
return hdev->bus == BUS_USB;
}That field is attacker-controlled: any unprivileged process with access to /dev/uhid can issue UHID_CREATE and set bus = BUS_USB while the actual hdev->dev.parent is the uhid virtual device, not a struct usb_interface. So hid_is_usb(hdev) returning true does not guarantee hdev->dev.parent is safe to cast with to_usb_interface()/interface_to_usbdev(). This affects every USB-cast site in the driver gated only by hid_is_usb(), not just the one hunk the bot flagged in hid_asus_ally_probe() — ally_get_endpoint_address(), asus_kbd_register_leds(), and the QUIRK_T100_KEYBOARD/QUIRK_MEDION_E1239T branches in asus_probe() all have the same gap. The fix: verify the parent device is actually attached to the USB bus (dev->bus == &usb_bus_type) before trusting the cast, not just the spoofable hdev->bus field. +/*
+ * hid_is_usb() only checks hdev->bus, which is attacker-controlled by any
+ * process with access to /dev/uhid: UHID_CREATE lets userspace claim an
+ * arbitrary bus id, including BUS_USB, while hdev->dev.parent is the uhid
+ * virtual device, not a struct usb_interface. Casting dev.parent based on
+ * hid_is_usb() alone lets such a spoofed "USB" HID device redirect the
+ * cast at unrelated memory. Confirm the parent is actually on the USB bus
+ * before trusting the cast.
+ */
+static bool asus_hdev_is_usb(struct hid_device *hdev)
+{
+ return hid_is_usb(hdev) && hdev->dev.parent &&
+ hdev->dev.parent->bus == &usb_bus_type;
+}
+
static int ally_get_endpoint_address(struct hid_device *hdev)
{
struct usb_host_endpoint *ep;
struct usb_interface *intf;
- if (!hid_is_usb(hdev))
+ if (!asus_hdev_is_usb(hdev))
return -ENODEV;
intf = to_usb_interface(hdev->dev.parent); |
|
@NeroReflex Thanks for the merge and the fast review cycle! For completeness I checked hid-ayaneo against the underlying concern anyway: the driver never casts Next on my side; the InputPlumber |
|
When you send it upstream please include And send to me too please. |
|
Done! Submitted to linux-input/LKML with your https://lore.kernel.org/linux-input/20260824215041.79892-1-hello@matias.me/ Rebased onto hid.git Thanks again for the review and the merge <3 I'll follow up here if the upstream review produces changes worth backporting to the OGC tree. |
Now we wait for sashiko-bot XD |
pastaq
left a comment
There was a problem hiding this comment.
I have a few nuts, and some suggestions. Some of it is negotiable.
I briefly mentioned it below but wanted to expound more here regarding debounce. Have you fully stress tested the write speed of the RGB interface? In my experience it is best to do a write queue using mod_delayed_work() with a timeout that is approximately what the return time for a write to the interface is in ms. Some userspace applications (like steam) write once per increment of a slider in a single threaded operation. When sliding over the entire color spectrum (255^3 options) that can significantly delay a system even if the return time is only a few ms, added up it becomes seconds. mod_delayed_work() will ensure that the sysfs returns immediately and only the most recent write is sent to the device.
Since this protocol uses a single command buffer to write all attributes, that means you can protect everything with the same mod_delayed_work() which should reduce the complexity if this approach compared to other drivers. It will need special handling in suspend/resume to make sashiko happy, but you can gate re-arming with a bool on drvdata.
| else | ||
| return -EINVAL; | ||
|
|
||
| ret = mutex_lock_interruptible(&aya->lock); |
There was a problem hiding this comment.
It's better to use guard or scoped_guard from cleanup.h for new code in the kernel. They will unlock themselves immediately as they go out of scope and it prevents mistaken drops of the unlock in future revisions.
Applies to all instances
|
|
||
| static struct hid_driver aya3_driver = { | ||
| .name = "hid-ayaneo", | ||
| .id_table = aya3_devices, |
There was a problem hiding this comment.
I would make these function titles more generic (hid_ayaneo_*). If a future device uses this protocol it won't be confusing, and if the protocol updates later for a new generation it produces less churn turning the entry points into branching probes/resumes.
| cdev->brightness = 0; | ||
| cdev->max_brightness = 255; | ||
| cdev->brightness_set_blocking = aya3_led_set; | ||
|
|
There was a problem hiding this comment.
Please give the parent led_cdev the color index LED_COLOR_ID_RGB. That will allow userspace to detect the interface and an RGB interface an plumb up things like KDE's chameleon service automatically.
| #define AYA3_RGB_SOLID 0x01 | ||
| #define AYA3_RGB_OFF 0xff | ||
|
|
||
| #define AYA3_VIBRATION_DEFAULT 0x02 /* medium */ |
There was a problem hiding this comment.
This implies that it is variable. I would make this an enum with all values and expose a rumble_intensity attribute as well, with a rumble_intensity_index to expose to userspace the options.
| Reading these attributes queries the controller and can | ||
| take up to a second. | ||
|
|
||
| What: /sys/bus/hid/drivers/hid-ayaneo/<dev>/eject |
There was a problem hiding this comment.
Please add a RO eject_index attr so that userspace can detect options automatically without the need to consult the kernel docs
| F: drivers/spi/spi-axiado.c | ||
| F: drivers/spi/spi-axiado.h | ||
|
|
||
| AYANEO 3 CONTROLLER HID DRIVER |
There was a problem hiding this comment.
It would only be used for people to send patches, and the module author is most likely to be the one to test it. I see no issues here.
| dev_name(&aya->hdev->dev)); | ||
| if (!cdev->name) | ||
| return -ENOMEM; | ||
| cdev->brightness = 0; |
There was a problem hiding this comment.
Is there no way to prove the device for its current state? It would be preferable that the interface reflects the status of the hardware at all times rather than when it's written to from userspace. Other programs (like HHD or huesync) write to the hid interface directly, which can desync the sysfs from the hardware state
| static ssize_t aya3_module_show(struct device *dev, char *buf, int offset) | ||
| { | ||
| struct aya3 *aya = dev_get_drvdata(dev); | ||
| u8 resp[AYA3_RESP_SIZE]; |
There was a problem hiding this comment.
It would be better IMO to have an ayaneo_resp struct that you can cast the response into to improve readability and prevent mistakes with offsets.
| */ | ||
| static int aya3_send_config(struct aya3 *aya, u8 eject) | ||
| { | ||
| static const u8 template[AYA3_REPORT_SIZE] = { |
There was a problem hiding this comment.
I would turn this into a struct and then cast it into a byte buffer before sending. It improves readability significantly. See hid-lenovo-go or hid-oxp for examples.
| [3] = AYA3_CMD_CONFIG, | ||
| [4] = AYA3_SUBCMD_CONFIG, | ||
| [22] = 0x33, | ||
| [23] = 0x22, /* joystick sensitivity 100%/100% */ |
There was a problem hiding this comment.
Consider adding this attribute as configurable as well
|
Thank you @pastaq! Great, exactly the input I was holding v3 for. Here's what I've adopted (staged for the LKML v3; I'll bring the OGC tree in sync once the upstream scope discussion settles):
On the rest:
|
|
Great. Please CC me on the LKML as well. Derek J. Clark derekjohn.clark@gmail.com |
|
@matmartinez I noticed you didn't mention anything about the debounce issue I brought up, did you see that portion as well? |
|
@pastaq apologies, I worked through the inline comments and missed the review body entirely! I went and traced the path in the tree, and I believe the scenario you describe is already covered by the LED core, because the driver deliberately registers only
I stress-tested while chasing the teardown race: tight-loop brightness hammer sustained over several rmmod cycles, minutes at a time results in no lag buildup, no protocol errors, and the rings track the most recent value (it's the same work item What |
|
I'm not confident that is accurate, my concern comes from real world experience with this issue. I experienced significant userspace hitching with the go, OXP, and MSI drivers. The results are amplified depending on the round trip time for a urb in the device, the wait_for_completion timeout becomes a bottleneck and the queued calls pile up. I haven't mitigated this in go_s drivers because it has a sub 4ms round trip and it's not really possible to over queue the buffer in the same way |
|
@pastaq I went and traced all three drivers, and I think the difference is which LED-core op they register. hid-lenovo-go, hid-oxp and hid-msi all register hid-ayaneo registers only I just measured on my hardware:
The residual thing your 50 ms settle would still buy is a lower device-command rate during a continuous drag (~190/s at this round trip → ~20/s). The rings held up fine under sustained tight-loop hammering in the teardown stress tests, so I'd lean toward keeping the stock core plumbing but if you've seen a |
|
I suppose it depends on if you're going to implement the additional attribute for effect using the same ABI I did for the other drivers. It's probably worth me investigating if I can get brightness_set_blocking() working in those contexts as well. I did try using that but my implementation wasn't successful. I don't recall why precisely. I wouldn't call this a blocker for v3 if you're getting reasonable performance. |
|
Yeah, no custom effect attribute planned... the AYA3's only effect is exposed through the stock One tip in case you retry Thanks for helping me on this @pastaq! |
Submitting here for review prior to LKML, per @pastaq in ShadowBlip/OpenGamepadUI#528.
Same commit as OpenGamingCollective/linux#101 (rebased onto this master; happy to close whichever of the two is redundant — guidance welcome on how these flow together).
What it does
Driver for the AYANEO 3 detachable controller ("Magic Modules") vendor HID interface (
1c4f:0002, application usage0xff000001; DMI-gated to the AYANEO 3 since the VID/PID is a generic SigmaMicro ID):module_left/module_rightsysfs attrs — raw firmware module-type IDs (bits 0–5 type, bit 6 rotated)ejectsysfs attr (left/right/both) — blocks until the firmware confirms the release handshakereset— quick controller config resetayaneo:rgb:joystick_rings(the name InputPlumber's50-ayaneo_3.yamlalready expects)EC power-off is deliberately left to userspace (write
0to ayaneo-ec'scontroller_powerafterejectreturns) so orchestration/UX stays in the OpenGamepadUI layer. Protocol reverse engineered in Handheld Daemon by Antheas Kapenekakis (he'll be CC'd on the LKML series). IncludesDocumentation/ABI/and MAINTAINERS entries; checkpatch --strict clean except the standard-ENOSYSoutput-report-fallback false positive.Testing
On an AYANEO 3 / Bazzite 44 (OGC 7.2.0-ogc4.1): probe identifies modules (
left 0x04 right 0x50), RGB via LED class verified, and full physical eject → power-off → release → reinsert → repower → re-enumeration → rebind cycles, both from the shell and driven by a working OpenGamepadUI quick-bar plugin (see ShadowBlip/OpenGamepadUI#528). Community testing guide: https://github.com/matmartinez/ayaneo-3-bazzite-compat/blob/main/TESTING.md