Skip to content

feat(CapabilityMap): add generic hidraw button driver#563

Open
honjow wants to merge 3 commits intoShadowBlip:mainfrom
honjow:feat/generic_hidraw
Open

feat(CapabilityMap): add generic hidraw button driver#563
honjow wants to merge 3 commits intoShadowBlip:mainfrom
honjow:feat/generic_hidraw

Conversation

@honjow
Copy link
Copy Markdown
Contributor

@honjow honjow commented Mar 24, 2026

Summary

Add a generic hidraw button driver that uses capability map YAML configs to define button mappings, following the same pattern as existing evdev capability maps. This removes the need to write per-device Rust driver modules for simple HID button devices.

Includes GPD Win 5 HID button support (new firmware) as the first use case. Supersedes #558, which implements the same functionality with a dedicated driver.

Approach

The existing evdev source already uses a declarative approach — devices like MSI Claw and GPD define button mappings in YAML capability maps without any device-specific Rust code. This PR extends that pattern to hidraw by:

  1. Extending HidrawConfig with flexible detection modes (non-zero byte, exact value match, specific bit check)
  2. Adding HidrawEventTranslator to translate raw HID reports based on the capability map
  3. Falling back to the generic driver automatically when DriverType::Unknown but a hidraw capability map is present

Adding support for a new simple HID button device now only requires a YAML capability map file — no Rust code or recompilation needed.

Test plan

  • Verify GPD Win 5 back buttons (mode switch, L4, R4) work with new firmware
  • Verify existing hidraw devices (DualSense, Legion Go, etc.) are unaffected

Copy link
Copy Markdown
Contributor

@pastaq pastaq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much needed feature, thanks for working on this. I want to discuss a few things, so before you change anything below lets get to a consensus.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file name doesn't match our current convention. gpd_type5 would be the next logical pattern. TBS, perhaps it would be good to have more descriptive names that include the family, capability map version, and subsystem.

i.e. gpd_type1 would become v1_gpd_evdev1 and this could be v2_gpd_hid1

@ShadowApex thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sorting, I would prefer something like gpd_v1_evdev1, gpd_v2_hid1, etc. But yes, I agree with having more descriptive filenames.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'll rename it to gpd_v2_hid1? @pastaq

let mut mappings = Vec::new();

for mapping in capability_map.mapping.iter() {
for source in mapping.source_events.iter() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call this source_event? Its a little ambiguous since we also have source_device elsewhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course

}

if mapping.byte_index >= report.len() {
continue;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is be very spammy, but we should add a warn here so people know why their mapping is failing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, will add a warn log here.

Self { mappings, state }
}

pub fn has_mappings(&self) -> bool {
Copy link
Copy Markdown
Contributor

@pastaq pastaq Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know why we need both this and has_hid_mappings? It already filters entries to self.mappings based on hidraw events, so its more performant to just check if self.mappings is empty in both cases since the work is done. In evdev we use has_translation. Perhaps we should rename that one to has_evdev_translation to disambiguate, and call this one has_hid_translation and also remove the below one?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, no reason to keep both. Will rename to has_hid_translation to match the evdev pattern and drop the static one.

match driver_type {
DriverType::Unknown => Err("No driver for hidraw interface found".into()),
DriverType::Unknown => {
if let Some(cap_map) = Self::load_hidraw_capability_map(&conf) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of checking this and running an iter() again when there are mappings, we could just load the GenericHidrawButtons and have it return an error if there are no translations in the map.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that simplifies things. Will just pass the capability map to GenericHidrawButtons and let it handle the validation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants