feat(host): virtual DualSense via UHID (hid-playstation) — device + report mapping
ci / rust (push) Has been cancelled

Roadmap #5 (rich DualSense). A UHID device presents a real Sony DualSense to the kernel's
hid-playstation driver (matched by VID 054C/PID 0CE6), which exposes the full controller —
gamepad, motion sensors, touchpad, lightbar/player LEDs, adaptive triggers — unlike the
uinput X-Box-360 pad.

- inject/dualsense.rs: hand-rolled /dev/uhid codec (no bindgen) mirroring the uinput style;
  the canonical inputtino 232-byte USB HID report descriptor + the feature-report replies
  (calibration 0x05 / pairing 0x09 / firmware 0x20) — answering hid-playstation's GET_REPORTs
  during init is REQUIRED or it creates no input devices. DsState::from_gamepad maps a
  GameStream/XInput frame → the DualSense input report (buttons/sticks/triggers/dpad, +
  touchpad/motion fields); service() answers GET_REPORTs and parses HID OUTPUT (rumble /
  lightbar RGB / player LEDs / adaptive triggers) into quic::HidOutput.
- scripts/60-punktfunk.rules: grant /dev/uhid to the 'input' group (like /dev/uinput).
- `punktfunk-host dualsense-test`: standalone validation (no streaming session).

Validated live: `dualsense-test` → hid-playstation binds + loads ff_memless + led_class_
multicolor; the kernel creates "Punktfunk DualSense 0" (event/js gamepad + Motion Sensors +
Touchpad + Headset Jack) at VID 054c/PID 0ce6, plus the lightbar at /sys/class/leds/
input*:rgb:indicator; js shows the Cross button firing + the left-stick sweep. Clippy/fmt
clean, workspace tests green. Wiring into the session (pad-type select, touchpad/motion
routing, HID-output back-channel) is the next commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 07:27:19 +00:00
parent 3a51551f97
commit 2372b02620
4 changed files with 485 additions and 3 deletions
+49
View File
@@ -77,6 +77,55 @@ fn real_main() -> Result<()> {
println!("{compositor:?} ready");
Ok(())
}
// Create a virtual DualSense via UHID and exercise it (validation, no streaming session):
// toggles the Cross button, sweeps the left stick, and prints any HID output the kernel
// sends back. Verify with `evtest` / `ls /dev/input/by-id/*Punktfunk*` / `wpctl status`.
#[cfg(target_os = "linux")]
Some("dualsense-test") => {
use inject::dualsense::{DsState, DualSensePad};
let secs: u64 = args
.iter()
.skip_while(|a| *a != "--seconds")
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(20);
use std::time::{Duration, Instant};
let mut pad =
DualSensePad::open(0).context("create virtual DualSense via /dev/uhid")?;
// Answer the kernel's init GET_REPORTs promptly so hid-playstation creates the input
// devices before we start streaming state.
let init = Instant::now() + Duration::from_millis(800);
while Instant::now() < init {
pad.service(0);
std::thread::sleep(Duration::from_millis(10));
}
println!(
"virtual DualSense created — check `evtest`, `ls /dev/input/by-id/*Punktfunk*`, \
`ls /sys/class/leds/`. Cycling Cross + sweeping LS for {secs}s."
);
let deadline = Instant::now() + Duration::from_secs(secs);
let (mut i, mut last_write) = (0i32, Instant::now());
while Instant::now() < deadline {
for o in pad.service(0) {
println!(" hid output from kernel/game: {o:?}");
}
if last_write.elapsed() >= Duration::from_millis(300) {
last_write = Instant::now();
i += 1;
let buttons = if i % 2 == 0 {
punktfunk_core::input::gamepad::BTN_A
} else {
0
};
let lx = (((i % 64) - 32) * 1024) as i16; // sweep left stick X
let st = DsState::from_gamepad(buttons, lx, 0, 0, 0, 0, 0);
pad.write_state(&st).context("write DualSense report")?;
}
std::thread::sleep(Duration::from_millis(15));
}
println!("dualsense-test: done");
Ok(())
}
// M0 pipeline spike.
Some("m0") => m0::run(parse_m0(&args[1..])?),
// M3: native punktfunk/1 host (QUIC control plane + UDP data plane).