From 040e46b4fcf4664d28e8f8b46141faf5231ee1a2 Mon Sep 17 00:00:00 2001 From: b3n0b1 Date: Sun, 21 Jun 2026 08:47:50 -0400 Subject: [PATCH 1/5] add Konkr Fit (AYANEO sub-brand) controller support The AYANEO Konkr Fit reports DMI product_name 'KONKR FIT' (vendor 'KONKR') and uses the same controller hardware as the AYANEO 3 (1c4f:0002 composite + 045e:028e gamepad), so it is not autodetected. Add a CONFS entry. It has no detachable modules (no ayaneo-ec EC interface) and two back paddles, so magic_modules is omitted and extra_buttons is set to dual. The paddles emit KEY_L/KEY_R, which the existing driver already maps to extra_l1/extra_r1. Co-Authored-By: Claude Opus 4.8 --- src/hhd/device/ayaneo/const.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/hhd/device/ayaneo/const.py b/src/hhd/device/ayaneo/const.py index 6db6fdd4..10be8234 100644 --- a/src/hhd/device/ayaneo/const.py +++ b/src/hhd/device/ayaneo/const.py @@ -27,6 +27,15 @@ "rgb": True, **AYA_DEFAULT_CONF, }, + # Konkr (Ayaneo sub-brand). Same controller hw as AYANEO 3 + # (1c4f:0002 composite + 045e:028e gamepad) but no detachable + # modules (no ayaneo-ec EC interface), dual back paddles. + "KONKR FIT": { + "name": "KONKR FIT", + "extra_buttons": "dual", + "rgb": True, + **AYA_DEFAULT_CONF, + }, } AYA3_INIT = [ From 5f665b156028488d4caf9f8ac23865efb33def2a Mon Sep 17 00:00:00 2001 From: b3n0b1 Date: Sun, 21 Jun 2026 09:19:28 -0400 Subject: [PATCH 2/5] konkr fit: quad paddles + map BTN_MODE to guide The Konkr Fit has four extra buttons (two rear paddles emitting KEY_L/KEY_R -> extra_l1/r1, plus LC/RC emitting KEY_F21/F22 -> extra_l2/r2), so use extra_buttons=quad. Its main system button is the gamepad BTN_MODE; map it to the guide/Steam button (mode) instead of the QAM/overlay (share) via a new mode_is_guide dconf flag, leaving the AYANEO 3 behaviour unchanged. Co-Authored-By: Claude Opus 4.8 --- src/hhd/device/ayaneo/base.py | 8 +++++++- src/hhd/device/ayaneo/const.py | 7 +++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/hhd/device/ayaneo/base.py b/src/hhd/device/ayaneo/base.py index 42b60ab4..c7a618b2 100644 --- a/src/hhd/device/ayaneo/base.py +++ b/src/hhd/device/ayaneo/base.py @@ -509,13 +509,19 @@ def controller_loop( d_timer = HrtimerTrigger(conf["imu_hz"].to(int), [HrtimerTrigger.IMU_NAMES]) # Inputs + # By default the gamepad's BTN_MODE is repurposed as the QAM/overlay + # button (share). On devices where it is the main "guide" button (e.g. + # Konkr Fit), keep it as the XBOX default (mode) so it opens Steam. + xinput_btn_map = {**XBOX_BUTTON_MAP} + if not dconf.get("mode_is_guide", False): + xinput_btn_map[EC("BTN_MODE")] = "share" d_xinput = GenericGamepadEvdev( vid=[GAMEPAD_VID], pid=[GAMEPAD_PID], capabilities={EC("EV_KEY"): [EC("BTN_A")]}, required=True, hide=True, - btn_map={**XBOX_BUTTON_MAP, EC("BTN_MODE"): "share"}, + btn_map=xinput_btn_map, ) d_kbd_1 = GenericGamepadEvdev( diff --git a/src/hhd/device/ayaneo/const.py b/src/hhd/device/ayaneo/const.py index 10be8234..51b71411 100644 --- a/src/hhd/device/ayaneo/const.py +++ b/src/hhd/device/ayaneo/const.py @@ -29,10 +29,13 @@ }, # Konkr (Ayaneo sub-brand). Same controller hw as AYANEO 3 # (1c4f:0002 composite + 045e:028e gamepad) but no detachable - # modules (no ayaneo-ec EC interface), dual back paddles. + # modules (no ayaneo-ec EC interface). Four extra buttons (two rear + # paddles + LC/RC) and BTN_MODE is the main button, so it acts as the + # guide/Steam button rather than the QAM/overlay button. "KONKR FIT": { "name": "KONKR FIT", - "extra_buttons": "dual", + "extra_buttons": "quad", + "mode_is_guide": True, "rgb": True, **AYA_DEFAULT_CONF, }, From 8f6f1230dc0255a7594e36838c5b26b3be52ba99 Mon Sep 17 00:00:00 2001 From: b3n0b1 Date: Sun, 21 Jun 2026 12:53:06 -0400 Subject: [PATCH 3/5] konkr fit: per-button remap UI for the six system buttons Adds a "Konkr Button Map" section with a dropdown per system/face button (Steam/Guide, QAM, Select, Start, Disabled). Right-top (Ctrl+F23) and konkr (F23) share a keycode, so split them with a new ChordGamepadEvdev that latches the resolved action on press. Gated behind a face_remap dconf flag so the AYANEO 3's identical gamepad buttons are untouched. Documents the hardware layout and evdev codes in KONKR.md. Co-Authored-By: Claude Opus 4.8 --- src/hhd/controller/physical/evdev.py | 86 +++++++++++++++++++++- src/hhd/device/ayaneo/KONKR.md | 98 +++++++++++++++++++++++++ src/hhd/device/ayaneo/__init__.py | 5 ++ src/hhd/device/ayaneo/base.py | 77 ++++++++++++++++--- src/hhd/device/ayaneo/const.py | 4 + src/hhd/device/ayaneo/konkr_buttons.yml | 48 ++++++++++++ 6 files changed, 305 insertions(+), 13 deletions(-) create mode 100644 src/hhd/device/ayaneo/KONKR.md create mode 100644 src/hhd/device/ayaneo/konkr_buttons.yml diff --git a/src/hhd/controller/physical/evdev.py b/src/hhd/controller/physical/evdev.py index c26a7067..52135697 100644 --- a/src/hhd/controller/physical/evdev.py +++ b/src/hhd/controller/physical/evdev.py @@ -515,6 +515,83 @@ def produce(self, fds: Sequence[int]) -> Sequence[Event]: return out +class ChordGamepadEvdev(GenericGamepadEvdev): + """A key-only ``GenericGamepadEvdev`` where a single physical key resolves + to one of two actions depending on whether a modifier key is held when it + is pressed. + + Used by the AYANEO Konkr, whose right-top and konkr (right-bottom-left) + buttons both emit ``KEY_F23`` -- the right-top button just also holds Ctrl. + The resolved action is latched on press so the release matches even if the + modifier is released first. Pass ``mod_btn``/``plain_btn`` as ``None`` to + leave that combination unmapped. Every other key uses the normal btn_map. + """ + + def __init__( + self, + *args, + mod_code: int, + plain_code: int, + mod_btn: Button | None, + plain_btn: Button | None, + **kwargs, + ) -> None: + super().__init__(*args, **kwargs) + self.mod_code = mod_code + self.plain_code = plain_code + self.mod_btn = mod_btn + self.plain_btn = plain_btn + self.mod_held = False + self.chord_active: Button | None = None + + def produce(self, fds: Sequence[int]) -> Sequence[Event]: + if not self.dev or self.fd not in fds: + return [] + + out: list[Event] = [] + while can_read(self.fd): + for e in self.dev.read(): + if e.type != B("EV_KEY"): + continue + if e.code == self.mod_code: + # Track the modifier but never emit it directly. + self.mod_held = e.value != 0 + continue + if e.code == self.plain_code: + if e.value == 1: + self.chord_active = ( + self.mod_btn if self.mod_held else self.plain_btn + ) + if self.chord_active is not None: + out.append( + { + "type": "button", + "code": self.chord_active, + "value": True, + } + ) + elif e.value == 0: + if self.chord_active is not None: + out.append( + { + "type": "button", + "code": self.chord_active, + "value": False, + } + ) + self.chord_active = None + continue + if e.code in self.btn_map and e.value in (0, 1): + out.append( + { + "type": "button", + "code": self.btn_map[e.code], + "value": bool(e.value), + } + ) + return out + + _kbd_raw: dict[KeyboardButton, Sequence[int]] = { "key_esc": [B("KEY_ESC")], # 1 "key_enter": [B("KEY_ENTER")], # 28 @@ -687,4 +764,11 @@ def produce(self, fds: Sequence[int]) -> Sequence[Event]: KEYBOARD_MAP: dict[int, KeyboardButton] = to_map(_kbd_raw) -__all__ = ["GenericGamepadEvdev", "XBOX_BUTTON_MAP", "XBOX_AXIS_MAP", "B", "to_map"] +__all__ = [ + "GenericGamepadEvdev", + "ChordGamepadEvdev", + "XBOX_BUTTON_MAP", + "XBOX_AXIS_MAP", + "B", + "to_map", +] diff --git a/src/hhd/device/ayaneo/KONKR.md b/src/hhd/device/ayaneo/KONKR.md new file mode 100644 index 00000000..b4fc1261 --- /dev/null +++ b/src/hhd/device/ayaneo/KONKR.md @@ -0,0 +1,98 @@ +# AYANEO Konkr Fit (HX470) — controller notes + +Reference for the Konkr Fit's controller hardware and how HHD maps it. Keep +this in sync if the firmware or mappings change. + +## Identification + +- DMI `product_name` = `KONKR FIT` (matched in `const.py` → `CONFS`). +- Konkr is an AYANEO sub-brand; the controller hardware is the **same as the + AYANEO 3**: + - `1c4f:0002` — AYANEO COMPOSITE DEVICE (`AYA_VID`/`AYA_PID`). Carries the + extra buttons and the right-side system buttons as keyboard keys. + - `045e:028e` — Xbox-style gamepad (`GAMEPAD_VID`/`GAMEPAD_PID`). Carries the + sticks, triggers, ABXY, d-pad, and the three left-side system buttons. +- Unlike the AYANEO 3 it has **no detachable modules** (no `ayaneo-ec` EC + sysfs interface), so `magic_modules` is off. + +## Physical button layout + +Six "system / face" buttons surround the screen, plus four extra buttons +(two rear paddles + two LC/RC buttons). Looking at the device face-on: + +``` + LEFT module RIGHT module + [ left-top ] [ right-top ] + [ L-sel ][ L-start ] [ konkr ][ R-bottom ] +``` + +- **LC / RC** = the two inner buttons under each grip. +- **rear-left / rear-right** = the two back paddles. + +## Raw evdev codes (captured on-device) + +| # | Physical button | Device | Sends (evdev) | +|---|-----------------------|------------------|--------------------------| +| 1 | left-top | gamepad (028e) | `BTN_MODE` (316) | +| 2 | left-bottom-left | gamepad (028e) | `BTN_SELECT` (314) | +| 3 | left-bottom-right | gamepad (028e) | `BTN_START` (315) | +| 4 | right-top | composite (0002) | `Ctrl` (29) + `F23` (193)| +| 5 | konkr (right-btm-left)| composite (0002) | `F23` (193) alone | +| 6 | right-bottom-right | composite (0002) | `Meta` (125) + `D` (32) | +| - | LC | composite (0002) | `F21` (191) | +| - | RC | composite (0002) | `F22` (192) | +| - | rear-left paddle | composite (0002) | `KEY_L` (38) | +| - | rear-right paddle | composite (0002) | `KEY_R` (19) | + +> **Important gotcha:** right-top (#4) and konkr (#5) emit the **same** key +> (`F23`). right-top just also holds `Ctrl`. A plain one-code → one-action map +> can't tell them apart — see the chord handling below. + +## How HHD maps them + +### Extra buttons (LC/RC + rear paddles) +`extra_buttons: "quad"`. These map to `extra_l1/l2/r1/r2` and are exposed as +DualSense Edge / Xbox Elite back paddles — remappable in **Steam Input**: + +| Button | evdev | Output | +|------------|---------|------------| +| rear-left | `KEY_L` | `extra_l1` | +| rear-right | `KEY_R` | `extra_r1` | +| LC | `F21` | `extra_l2` | +| RC | `F22` | `extra_r2` | + +### The six system/face buttons — "Konkr Button Map" +Enabled by the `face_remap` flag in `const.py`. Each button gets a dropdown +(`konkr_buttons.yml`, injected in `__init__.py:settings()`), applied in +`base.py`. Trimmed option set: `mode` (Steam/Guide), `share` (Quick Access +Menu / overlay), `select`, `start`, `disabled`. + +| Dropdown key | Physical button | Source code | Default | +|--------------------|-------------------|----------------|------------| +| `btn_left_top` | left-top | `BTN_MODE` | `mode` | +| `btn_left_select` | left-bottom-left | `BTN_SELECT` | `select` | +| `btn_left_start` | left-bottom-right | `BTN_START` | `start` | +| `btn_right_top` | right-top | `Ctrl`+`F23` | `disabled` | +| `btn_konkr` | konkr | `F23` | `disabled` | +| `btn_right_bottom` | right-bottom-right| `KEY_D` | `share` | + +- Left-side three go through the gamepad: `base.py` overrides + `XBOX_BUTTON_MAP` for `BTN_MODE/SELECT/START`; `disabled` drops the key. +- Right-side three go through the composite keyboard: + - `KEY_D` → `btn_right_bottom`. + - `F23` is split by **`ChordGamepadEvdev`** (`evdev.py`): if `Ctrl` is held + it resolves to `btn_right_top`, otherwise `btn_konkr`. The action is + **latched on press** so the release matches even if `Ctrl` is let go + first (prevents stuck buttons). + +Defaults reproduce the pre-remap behavior, so existing users see no change +until they touch a dropdown. Everything is gated on `face_remap`, so the +AYANEO 3's identical gamepad buttons are never affected. + +## Files + +- `const.py` — `CONFS["KONKR FIT"]` device entry + flags. +- `konkr_buttons.yml` — the six dropdowns (UI schema). +- `__init__.py` — injects the schema when `face_remap` is set. +- `base.py` — reads the dropdowns and builds the evdev button maps. +- `../../controller/physical/evdev.py` — `ChordGamepadEvdev`. diff --git a/src/hhd/device/ayaneo/__init__.py b/src/hhd/device/ayaneo/__init__.py index 8d4dc60e..89b677f0 100644 --- a/src/hhd/device/ayaneo/__init__.py +++ b/src/hhd/device/ayaneo/__init__.py @@ -70,6 +70,11 @@ def settings(self) -> HHDSettings: ) ) + if self.dconf.get("face_remap", False): + base["controllers"]["ayaneo"]["children"]["face_buttons"] = ( + load_relative_yaml("konkr_buttons.yml") + ) + if self.dconf.get("display_gyro", True): base["controllers"]["ayaneo"]["children"]["imu_axis"] = get_gyro_config( self.dconf.get("mapping", DEFAULT_MAPPINGS) diff --git a/src/hhd/device/ayaneo/base.py b/src/hhd/device/ayaneo/base.py index c7a618b2..8d0ba180 100644 --- a/src/hhd/device/ayaneo/base.py +++ b/src/hhd/device/ayaneo/base.py @@ -8,7 +8,11 @@ from hhd.controller.lib.hide import unhide_all from hhd.controller.physical.evdev import XBOX_BUTTON_MAP from hhd.controller.physical.evdev import B as EC -from hhd.controller.physical.evdev import GenericGamepadEvdev, enumerate_evs +from hhd.controller.physical.evdev import ( + ChordGamepadEvdev, + GenericGamepadEvdev, + enumerate_evs, +) from hhd.controller.physical.hidraw import GenericGamepadHidraw from hhd.controller.physical.imu import CombinedImu, HrtimerTrigger from hhd.controller.virtual.uinput import UInputDevice @@ -509,11 +513,32 @@ def controller_loop( d_timer = HrtimerTrigger(conf["imu_hz"].to(int), [HrtimerTrigger.IMU_NAMES]) # Inputs + face_remap = dconf.get("face_remap", False) + + def remap_action(key: str, default: str): + # Resolve a Konkr Button Map dropdown into an output Button. The UI + # "disabled" option is returned as None so the key gets dropped. + val = conf.get(f"face_buttons.{key}", default) + return None if val == "disabled" else val + # By default the gamepad's BTN_MODE is repurposed as the QAM/overlay # button (share). On devices where it is the main "guide" button (e.g. # Konkr Fit), keep it as the XBOX default (mode) so it opens Steam. xinput_btn_map = {**XBOX_BUTTON_MAP} - if not dconf.get("mode_is_guide", False): + if face_remap: + # The three left-side buttons come through the Xbox gamepad. Apply the + # user's per-button choices, dropping any set to "Disabled". + for code, key, default in ( + (EC("BTN_MODE"), "btn_left_top", "mode"), + (EC("BTN_SELECT"), "btn_left_select", "select"), + (EC("BTN_START"), "btn_left_start", "start"), + ): + act = remap_action(key, default) + if act is None: + xinput_btn_map.pop(code, None) + else: + xinput_btn_map[code] = act + elif not dconf.get("mode_is_guide", False): xinput_btn_map[EC("BTN_MODE")] = "share" d_xinput = GenericGamepadEvdev( vid=[GAMEPAD_VID], @@ -533,21 +558,49 @@ def controller_loop( EC("KEY_F23"): "mode", }, ) - d_kbd_2 = GenericGamepadEvdev( - vid=[AYA_VID], - pid=[AYA_PID], - required=True, - grab=True, - capabilities={EC("EV_KEY"): [EC("KEY_F21")]}, - btn_map={ + if face_remap: + # The three right-side buttons come through the AYANEO composite + # keyboard. right-bottom-right is KEY_D; right-top (Ctrl+F23) and konkr + # (F23 alone) share the F23 keycode, so they are split by the chord + # producer using the Ctrl modifier. Paddles stay on the normal map. + kbd2_map = { EC("KEY_F24"): "keyboard", - EC("KEY_D"): "keyboard", EC("KEY_F21"): "extra_l2", EC("KEY_F22"): "extra_r2", EC("KEY_L"): "extra_l1", EC("KEY_R"): "extra_r1", - }, - ) + } + right_bottom = remap_action("btn_right_bottom", "share") + if right_bottom is not None: + kbd2_map[EC("KEY_D")] = right_bottom + d_kbd_2 = ChordGamepadEvdev( + vid=[AYA_VID], + pid=[AYA_PID], + required=True, + grab=True, + capabilities={EC("EV_KEY"): [EC("KEY_F21")]}, + btn_map=kbd2_map, + mod_code=EC("KEY_LEFTCTRL"), + plain_code=EC("KEY_F23"), + mod_btn=remap_action("btn_right_top", "disabled"), + plain_btn=remap_action("btn_konkr", "disabled"), + ) + else: + d_kbd_2 = GenericGamepadEvdev( + vid=[AYA_VID], + pid=[AYA_PID], + required=True, + grab=True, + capabilities={EC("EV_KEY"): [EC("KEY_F21")]}, + btn_map={ + EC("KEY_F24"): "keyboard", + EC("KEY_D"): "keyboard", + EC("KEY_F21"): "extra_l2", + EC("KEY_F22"): "extra_r2", + EC("KEY_L"): "extra_l1", + EC("KEY_R"): "extra_r1", + }, + ) d_vend = Ayaneo3Hidraw( vid=[AYA_VID], pid=[AYA_PID], diff --git a/src/hhd/device/ayaneo/const.py b/src/hhd/device/ayaneo/const.py index 51b71411..f256cd83 100644 --- a/src/hhd/device/ayaneo/const.py +++ b/src/hhd/device/ayaneo/const.py @@ -36,6 +36,10 @@ "name": "KONKR FIT", "extra_buttons": "quad", "mode_is_guide": True, + # Expose per-button remap dropdowns for the six system/face buttons + # (handled in base.py + konkr_buttons.yml). Gated so it never touches + # the gamepad BTN_MODE/SELECT/START on other AYANEO devices. + "face_remap": True, "rgb": True, **AYA_DEFAULT_CONF, }, diff --git a/src/hhd/device/ayaneo/konkr_buttons.yml b/src/hhd/device/ayaneo/konkr_buttons.yml new file mode 100644 index 00000000..8f8ccfd5 --- /dev/null +++ b/src/hhd/device/ayaneo/konkr_buttons.yml @@ -0,0 +1,48 @@ +type: container +tags: [non-essential] +title: Konkr Button Map +hint: >- + Remap the six system/face buttons unique to the Konkr Fit. The sticks, + triggers and rear paddles stay remappable through Steam Input as usual. + +children: + btn_left_top: + type: multiple + title: Left Top + options: &konkr_btn_options + mode: Steam / Guide + share: Quick Access Menu + select: Select (View) + start: Start (Menu) + disabled: Disabled + default: mode + + btn_left_select: + type: multiple + title: Left Bottom-Left + options: *konkr_btn_options + default: select + + btn_left_start: + type: multiple + title: Left Bottom-Right + options: *konkr_btn_options + default: start + + btn_right_top: + type: multiple + title: Right Top + options: *konkr_btn_options + default: disabled + + btn_konkr: + type: multiple + title: Konkr (Right Bottom-Left) + options: *konkr_btn_options + default: disabled + + btn_right_bottom: + type: multiple + title: Right Bottom-Right + options: *konkr_btn_options + default: share From f45876b364064467e462585757c5c4e0921eec58 Mon Sep 17 00:00:00 2001 From: b3n0b1 Date: Sun, 21 Jun 2026 13:04:32 -0400 Subject: [PATCH 4/5] konkr fit: add HHD Overlay / HHD Side Menu button actions Adds two direct-emit actions to the Konkr Button Map dropdowns: HHD Overlay (open_expanded) and HHD Side Menu (open_qam). They are sentinel codes the multiplexer turns into the matching special event immediately, bypassing the multi-tap QAM state machine so the binds are deterministic. Replaces the ambiguous "share" option; right-bottom-right now defaults to HHD Side Menu. Co-Authored-By: Claude Opus 4.8 --- src/hhd/controller/base.py | 19 +++++++++++++++++++ src/hhd/device/ayaneo/KONKR.md | 19 +++++++++++++++++-- src/hhd/device/ayaneo/base.py | 2 +- src/hhd/device/ayaneo/konkr_buttons.yml | 5 +++-- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/hhd/controller/base.py b/src/hhd/controller/base.py index 5fb19bdb..730b4ea3 100644 --- a/src/hhd/controller/base.py +++ b/src/hhd/controller/base.py @@ -1259,6 +1259,25 @@ def process(self, events: Sequence[Event]) -> Sequence[Event]: if self.emit: self.emit({"type": "special", "event": "overlay"}) + if ev["code"] in ("hhd_qam", "hhd_overlay"): + # Direct binds for the HHD side menu (QAM) and the + # expanded HHD overlay, used by per-button remaps. + # Emitted straight away so they bypass the multi-tap + # QAM state machine. "overlay" -> open_qam (side menu), + # "qam_triple" -> open_expanded (full overlay). + if ev["value"] and self.emit: + self.emit( + { + "type": "special", + "event": ( + "overlay" + if ev["code"] == "hhd_qam" + else "qam_triple" + ), + } + ) + ev["code"] = "" # type: ignore + if ev["code"] == "touchpad_right": match self.touchpad_right: case "disabled": diff --git a/src/hhd/device/ayaneo/KONKR.md b/src/hhd/device/ayaneo/KONKR.md index b4fc1261..8d7090a7 100644 --- a/src/hhd/device/ayaneo/KONKR.md +++ b/src/hhd/device/ayaneo/KONKR.md @@ -64,8 +64,23 @@ DualSense Edge / Xbox Elite back paddles — remappable in **Steam Input**: ### The six system/face buttons — "Konkr Button Map" Enabled by the `face_remap` flag in `const.py`. Each button gets a dropdown (`konkr_buttons.yml`, injected in `__init__.py:settings()`), applied in -`base.py`. Trimmed option set: `mode` (Steam/Guide), `share` (Quick Access -Menu / overlay), `select`, `start`, `disabled`. +`base.py`. Option set: + +| Dropdown value | Label | Result | +|----------------|----------------|--------| +| `mode` | Steam / Guide | Opens Steam (guide button) | +| `hhd_overlay` | HHD Overlay | Opens the expanded HHD overlay (`open_expanded`) | +| `hhd_qam` | HHD Side Menu | Opens the HHD QAM side menu (`open_qam`) | +| `select` | Select (View) | | +| `start` | Start (Menu) | | +| `disabled` | Disabled | Button does nothing | + +`hhd_overlay`/`hhd_qam` are not real gamepad buttons -- they are sentinel +codes handled in `controller/base.py` (the multiplexer), which emits the +matching `special` event directly (`hhd_qam` -> `overlay` -> `open_qam`; +`hhd_overlay` -> `qam_triple` -> `open_expanded`) and clears the code so it +never reaches the virtual controller. This bypasses the multi-tap QAM state +machine, so the binds are deterministic. | Dropdown key | Physical button | Source code | Default | |--------------------|-------------------|----------------|------------| diff --git a/src/hhd/device/ayaneo/base.py b/src/hhd/device/ayaneo/base.py index 8d0ba180..5690b9ee 100644 --- a/src/hhd/device/ayaneo/base.py +++ b/src/hhd/device/ayaneo/base.py @@ -570,7 +570,7 @@ def remap_action(key: str, default: str): EC("KEY_L"): "extra_l1", EC("KEY_R"): "extra_r1", } - right_bottom = remap_action("btn_right_bottom", "share") + right_bottom = remap_action("btn_right_bottom", "hhd_qam") if right_bottom is not None: kbd2_map[EC("KEY_D")] = right_bottom d_kbd_2 = ChordGamepadEvdev( diff --git a/src/hhd/device/ayaneo/konkr_buttons.yml b/src/hhd/device/ayaneo/konkr_buttons.yml index 8f8ccfd5..494847b4 100644 --- a/src/hhd/device/ayaneo/konkr_buttons.yml +++ b/src/hhd/device/ayaneo/konkr_buttons.yml @@ -11,7 +11,8 @@ children: title: Left Top options: &konkr_btn_options mode: Steam / Guide - share: Quick Access Menu + hhd_overlay: HHD Overlay + hhd_qam: HHD Side Menu select: Select (View) start: Start (Menu) disabled: Disabled @@ -45,4 +46,4 @@ children: type: multiple title: Right Bottom-Right options: *konkr_btn_options - default: share + default: hhd_qam From 465b4807f2c2620b6a6a6e9341fd91bed08bc3ae Mon Sep 17 00:00:00 2001 From: b3n0b1 Date: Sun, 21 Jun 2026 13:15:48 -0400 Subject: [PATCH 5/5] konkr fit: use official action names; hide Aya Button Map Align the Konkr Button Map dropdowns with HHD's official shortcut vocabulary from overlay/shortcuts.yml: Steam Side Menu (steam_qam), Steam Overlay (steam_expanded), HHD Side Menu (hhd_qam), HHD Overlay (hhd_expanded). Fixes the previously invented value hhd_overlay -> hhd_expanded and label "HHD QAM" -> "HHD Side Menu". Adds Steam's own QAM/overlay as options. Hide the standard Aya Button Map (swap_guide) on the Konkr since the per-button map supersedes it; base.py falls back to swap_guide=oem (no swap). Co-Authored-By: Claude Opus 4.8 --- src/hhd/controller/base.py | 20 ++++++++--- src/hhd/device/ayaneo/KONKR.md | 48 ++++++++++++++++--------- src/hhd/device/ayaneo/__init__.py | 8 +++-- src/hhd/device/ayaneo/konkr_buttons.yml | 11 ++++-- 4 files changed, 60 insertions(+), 27 deletions(-) diff --git a/src/hhd/controller/base.py b/src/hhd/controller/base.py index 730b4ea3..aea03ba7 100644 --- a/src/hhd/controller/base.py +++ b/src/hhd/controller/base.py @@ -1259,12 +1259,13 @@ def process(self, events: Sequence[Event]) -> Sequence[Event]: if self.emit: self.emit({"type": "special", "event": "overlay"}) - if ev["code"] in ("hhd_qam", "hhd_overlay"): + if ev["code"] in ("hhd_qam", "hhd_expanded"): # Direct binds for the HHD side menu (QAM) and the # expanded HHD overlay, used by per-button remaps. - # Emitted straight away so they bypass the multi-tap - # QAM state machine. "overlay" -> open_qam (side menu), - # "qam_triple" -> open_expanded (full overlay). + # Action names match shortcuts.yml. Emitted straight + # away so they bypass the multi-tap QAM state machine. + # "overlay" -> open_qam (side menu), "qam_triple" -> + # open_expanded (full overlay). if ev["value"] and self.emit: self.emit( { @@ -1278,6 +1279,17 @@ def process(self, events: Sequence[Event]) -> Sequence[Event]: ) ev["code"] = "" # type: ignore + if ev["code"] in ("steam_qam", "steam_expanded"): + # Open Steam's own QAM / expanded menu via the same + # path Steam Deck uses. Handled at the end of process() + # so it respects intercept and the chord fallbacks. + if ev["value"]: + if ev["code"] == "steam_qam": + send_steam_qam = True + else: + send_steam_expand = True + ev["code"] = "" # type: ignore + if ev["code"] == "touchpad_right": match self.touchpad_right: case "disabled": diff --git a/src/hhd/device/ayaneo/KONKR.md b/src/hhd/device/ayaneo/KONKR.md index 8d7090a7..8efee4bd 100644 --- a/src/hhd/device/ayaneo/KONKR.md +++ b/src/hhd/device/ayaneo/KONKR.md @@ -64,23 +64,37 @@ DualSense Edge / Xbox Elite back paddles — remappable in **Steam Input**: ### The six system/face buttons — "Konkr Button Map" Enabled by the `face_remap` flag in `const.py`. Each button gets a dropdown (`konkr_buttons.yml`, injected in `__init__.py:settings()`), applied in -`base.py`. Option set: - -| Dropdown value | Label | Result | -|----------------|----------------|--------| -| `mode` | Steam / Guide | Opens Steam (guide button) | -| `hhd_overlay` | HHD Overlay | Opens the expanded HHD overlay (`open_expanded`) | -| `hhd_qam` | HHD Side Menu | Opens the HHD QAM side menu (`open_qam`) | -| `select` | Select (View) | | -| `start` | Start (Menu) | | -| `disabled` | Disabled | Button does nothing | - -`hhd_overlay`/`hhd_qam` are not real gamepad buttons -- they are sentinel -codes handled in `controller/base.py` (the multiplexer), which emits the -matching `special` event directly (`hhd_qam` -> `overlay` -> `open_qam`; -`hhd_overlay` -> `qam_triple` -> `open_expanded`) and clears the code so it -never reaches the virtual controller. This bypasses the multi-tap QAM state -machine, so the binds are deterministic. +`base.py`. Values/labels mirror `plugins/overlay/shortcuts.yml` (HHD's +official vocabulary): + +| Dropdown value | Label | Result | +|------------------|-----------------|--------| +| `disabled` | Disabled | Button does nothing | +| `mode` | Steam / Guide | Opens Steam (guide button) | +| `select` | Select (View) | gamepad Select/View | +| `start` | Start (Menu) | gamepad Start/Menu | +| `steam_qam` | Steam Side Menu | Opens Steam's own QAM | +| `steam_expanded` | Steam Overlay | Opens Steam's expanded menu | +| `hhd_qam` | HHD Side Menu | Opens the HHD QAM side menu (`open_qam`) | +| `hhd_expanded` | HHD Overlay | Opens the expanded HHD overlay (`open_expanded`) | + +`mode`/`select`/`start` are plain gamepad buttons. The four menu actions are +not real gamepad buttons -- they are sentinel codes handled in +`controller/base.py` (the multiplexer), which clears the code so it never +reaches the virtual controller: + +- `hhd_qam` -> emits `special` event `overlay` -> `open_qam` (HHD side menu). +- `hhd_expanded` -> emits `special` event `qam_triple` -> `open_expanded`. + Both bypass the multi-tap QAM state machine, so they are deterministic. +- `steam_qam` / `steam_expanded` -> set `send_steam_qam` / `send_steam_expand`, + so the multiplexer opens Steam's menu through the normal Steam path at the + end of `process()` (intercept-aware, with the guide+A chord fallback if + gamescope isn't handling it). + +> The standard **Aya Button Map** (`swap_guide`) dropdown is hidden on the +> Konkr (removed in `__init__.py:settings()`), since this per-button map +> supersedes it. With it gone, `base.py` falls back to `swap_guide="oem"` +> (no swap), which is correct for the custom map. | Dropdown key | Physical button | Source code | Default | |--------------------|-------------------|----------------|------------| diff --git a/src/hhd/device/ayaneo/__init__.py b/src/hhd/device/ayaneo/__init__.py index 89b677f0..a69b9c20 100644 --- a/src/hhd/device/ayaneo/__init__.py +++ b/src/hhd/device/ayaneo/__init__.py @@ -71,9 +71,11 @@ def settings(self) -> HHDSettings: ) if self.dconf.get("face_remap", False): - base["controllers"]["ayaneo"]["children"]["face_buttons"] = ( - load_relative_yaml("konkr_buttons.yml") - ) + children = base["controllers"]["ayaneo"]["children"] + children["face_buttons"] = load_relative_yaml("konkr_buttons.yml") + # The per-button map supersedes the Aya Button Map (swap_guide), + # so hide it to avoid two conflicting remap UIs. + children.pop("swap_guide", None) if self.dconf.get("display_gyro", True): base["controllers"]["ayaneo"]["children"]["imu_axis"] = get_gyro_config( diff --git a/src/hhd/device/ayaneo/konkr_buttons.yml b/src/hhd/device/ayaneo/konkr_buttons.yml index 494847b4..5f6e2139 100644 --- a/src/hhd/device/ayaneo/konkr_buttons.yml +++ b/src/hhd/device/ayaneo/konkr_buttons.yml @@ -9,13 +9,18 @@ children: btn_left_top: type: multiple title: Left Top + # Action values/labels mirror src/hhd/plugins/overlay/shortcuts.yml so the + # Konkr map uses HHD's official vocabulary. `mode`/`select`/`start` are the + # plain gamepad buttons; the rest are overlay/Steam shortcut actions. options: &konkr_btn_options + disabled: Disabled mode: Steam / Guide - hhd_overlay: HHD Overlay - hhd_qam: HHD Side Menu select: Select (View) start: Start (Menu) - disabled: Disabled + steam_qam: Steam Side Menu + steam_expanded: Steam Overlay + hhd_qam: HHD Side Menu + hhd_expanded: HHD Overlay default: mode btn_left_select: