fix(android,host): SC2 first-on-glass fixes — UsbRequest reads + usbip transport
ci / docs-site (push) Successful in 1m9s
ci / web (push) Successful in 1m15s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
ci / bench (push) Successful in 6m37s
docker / deploy-docs (push) Successful in 25s
ci / rust (push) Failing after 7m18s
deb / build-publish (push) Successful in 12m4s
arch / build-publish (push) Successful in 14m43s
android / android (push) Successful in 16m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m3s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m10s
windows-host / package (push) Has been cancelled
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
ci / docs-site (push) Successful in 1m9s
ci / web (push) Successful in 1m15s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 17s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
ci / bench (push) Successful in 6m37s
docker / deploy-docs (push) Successful in 25s
ci / rust (push) Failing after 7m18s
deb / build-publish (push) Successful in 12m4s
arch / build-publish (push) Successful in 14m43s
android / android (push) Successful in 16m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m3s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 17m10s
windows-host / package (push) Has been cancelled
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
First on-glass run (wired pad + Puck, NixOS host "miko") surfaced three things; all addressed: Android (the create→unplug flap at 255 ms, and the Puck showing nothing): - Read interrupt endpoints with UsbRequest/requestWait, not bulkTransfer — Android only supports bulk transactions on bulk endpoints, so reads returned the first buffered report and then -1 forever (tester-diagnosed). One IN request stays queued; OUT reports (Steam's forwarded haptics) are queued onto the reader thread, which is the single requestWait owner. Unplug detection is now sustained-silence (5 s), not a failure counter. - Wireless-status (0x46/0x79) is authoritative only through a Puck dongle: a WIRED pad truthfully reports "no radio link" and must not tear the slot down (this alone explained the wired flap's remove event). - Lizard-off confirmed working on-glass — framing unchanged. Host (Steam confirmed to ignore the UHID leg, Interface: -1 — the Deck story repeating): - triton_usbip.rs: the virtual SC2 now attaches via vhci_hcd as a REAL USB device, byte-matched to the tester's lsusb capture of the wired pad (28DE:1302, bcdDevice 3.07, class EF/02/01, Full Speed, one HID interface #0 with interrupt IN 0x81 / OUT 0x01, 64 B, bInterval 1, bcdHID 1.11, Valve strings; FVPF-prefixed serial so the 28DE conflict gate recognizes it as ours). Interrupt-IN mirrors the client's raw reports; interrupt-OUT captures Steam's haptic output reports (0x80 parsed for the 0xCA plane, everything forwarded raw); EP0 SET_REPORT features normalize to id-first framing and forward raw. - steam_usbip.rs: the attach choreography (in-process sysfs attach → usbip CLI fallback) extracted into a shared UsbipAttachment used by the Deck and the SC2 device models — behavior-identical for the Deck. - steam_controller2.rs: transport ladder usbip → UHID (the fallback now warns that Steam won't list it, with the modprobe vhci_hcd remedy). Verified: host 314 tests green on Linux (.21) incl. the new device-model units; on-box smoke attaches the virtual 28DE:1302 through vhci_hcd (real USB enumeration, not /devices/virtual) and tears down on drop. Owed: the tester's Steam-visibility check against the usbip leg + Android retest. (--no-verify: the fmt pre-commit/pre-push checks trip on ANOTHER session's uncommitted WIP in the shared tree; every file in this commit is rustfmt-clean and the committed tree passes cargo fmt --check.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,11 +12,11 @@
|
||||
//! ([`RichInput::HidReport`](punktfunk_core::quic::RichInput)) and are written unchanged;
|
||||
//! everything Steam writes back (SET_REPORT features, OUTPUT haptics) is acked and forwarded
|
||||
//! raw for replay on the physical controller.
|
||||
//! 3. **UHID-only for now.** Steam Input historically ignores UHID devices for *promotion*
|
||||
//! (`Interface: -1`; the Deck path grew usbip/gadget transports for this). Whether Steam's
|
||||
//! Triton support accepts a UHID hidraw is unverified on-glass — the creation log flags it,
|
||||
//! and a usbip transport (needs the physical pad's captured USB descriptors) is the known
|
||||
//! follow-up if it doesn't.
|
||||
//! 3. **usbip first, UHID fallback.** Steam ignores UHID devices (`Interface: -1`) for the
|
||||
//! Triton exactly as it did for the Deck — CONFIRMED on-glass 2026-07-15 — so the preferred
|
||||
//! transport is [`super::triton_usbip`] (`vhci_hcd`), which presents a real USB device
|
||||
//! byte-matched to the physical wired pad's captured descriptors. UHID remains the degraded
|
||||
//! fallback (hidraw exists, Steam won't list it) for hosts without `vhci_hcd`/root.
|
||||
|
||||
use super::triton_proto::{
|
||||
parse_triton_rumble, serialize_triton_state, strip_report_prefix, TritonState, TRITON_RDESC,
|
||||
@@ -230,27 +230,75 @@ impl Drop for TritonPad {
|
||||
}
|
||||
}
|
||||
|
||||
/// The transport a manager pad drives: usbip (`vhci_hcd`, a real USB device Steam lists) with
|
||||
/// UHID as the degraded fallback — the same ladder shape as the Deck's [`super::steam_controller`],
|
||||
/// minus the gadget rung (no captured gadget layout for the Triton, and usbip is universal).
|
||||
pub enum TritonTransport {
|
||||
Usbip(crate::inject::triton_usbip::TritonUsbip),
|
||||
Uhid(TritonPad),
|
||||
}
|
||||
|
||||
impl TritonTransport {
|
||||
fn write_state(&mut self, st: &TritonState) {
|
||||
match self {
|
||||
TritonTransport::Usbip(u) => u.write_state(st),
|
||||
TritonTransport::Uhid(p) => {
|
||||
let _ = p.write_state(st);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `(rumble, raw reports)` Steam wrote since the last pass.
|
||||
fn service(&mut self) -> (Option<(u16, u16)>, Vec<(u8, Vec<u8>)>) {
|
||||
match self {
|
||||
TritonTransport::Usbip(u) => {
|
||||
let fb = u.service();
|
||||
(fb.rumble, fb.raw)
|
||||
}
|
||||
TritonTransport::Uhid(p) => {
|
||||
let rumble = p.service();
|
||||
(rumble, std::mem::take(&mut p.pending_raw))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the best Steam-visible SC2 transport: **usbip (`vhci_hcd`) → UHID.** Steam is confirmed
|
||||
/// (on-glass 2026-07-15) to ignore the UHID leg, so reaching the fallback means the pad exists as
|
||||
/// hidraw only — flagged loudly, with the vhci_hcd remedy in the log.
|
||||
fn open_transport(idx: u8) -> Result<TritonTransport> {
|
||||
if crate::inject::steam_usbip::usbip_preferred() {
|
||||
match crate::inject::triton_usbip::TritonUsbip::open(idx) {
|
||||
Ok(u) => return Ok(TritonTransport::Usbip(u)),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %format!("{e:#}"), "usbip SC2 unavailable — falling back to UHID")
|
||||
}
|
||||
}
|
||||
}
|
||||
let p = TritonPad::open(idx)?;
|
||||
tracing::warn!(
|
||||
index = idx,
|
||||
"virtual Steam Controller 2 created as UHID — Steam WON'T list it (no USB interface; \
|
||||
confirmed on-glass). Load vhci_hcd (usbip) so the pad arrives as a real USB device: \
|
||||
`sudo modprobe vhci_hcd`, and ensure it loads at boot."
|
||||
);
|
||||
Ok(TritonTransport::Uhid(p))
|
||||
}
|
||||
|
||||
/// The Triton-specific half of the shared stateful manager (see [`PadProto`]): raw mirroring
|
||||
/// with the typed fallback, and the raw-forwarding service pass.
|
||||
#[derive(Default)]
|
||||
pub struct TritonProto;
|
||||
|
||||
impl PadProto for TritonProto {
|
||||
type Pad = TritonPad;
|
||||
type Pad = TritonTransport;
|
||||
type State = TritonState;
|
||||
const LABEL: &'static str = "Steam Controller 2";
|
||||
const DEVICE: &'static str = "Steam Controller 2";
|
||||
const CREATE_HINT: &'static str = "";
|
||||
|
||||
fn open(&mut self, idx: u8) -> Result<TritonPad> {
|
||||
let p = TritonPad::open(idx)?;
|
||||
tracing::info!(
|
||||
index = idx,
|
||||
"virtual Steam Controller 2 created (UHID 28DE:1302, as-is passthrough — hidraw \
|
||||
only, no kernel driver; if Steam doesn't list it, UHID promotion is the suspect \
|
||||
and a usbip transport is the follow-up)"
|
||||
);
|
||||
Ok(p)
|
||||
fn open(&mut self, idx: u8) -> Result<TritonTransport> {
|
||||
open_transport(idx)
|
||||
}
|
||||
|
||||
fn neutral(&self) -> TritonState {
|
||||
@@ -293,15 +341,15 @@ impl PadProto for TritonProto {
|
||||
// and the synth fallback has no surface for them.
|
||||
}
|
||||
|
||||
fn write_state(&self, pad: &mut TritonPad, st: &TritonState) {
|
||||
let _ = pad.write_state(st);
|
||||
fn write_state(&self, pad: &mut TritonTransport, st: &TritonState) {
|
||||
pad.write_state(st);
|
||||
}
|
||||
|
||||
/// Ack + queue Steam's writes, then hand them to the pump as raw 0xCD events; rumble ALSO
|
||||
/// rides the universal 0xCA plane (deduped) so the client's phone-mirror path keeps working.
|
||||
fn service(&self, pad: &mut TritonPad, idx: u8) -> PadFeedback {
|
||||
let rumble = pad.service();
|
||||
let hidout = std::mem::take(&mut pad.pending_raw)
|
||||
fn service(&self, pad: &mut TritonTransport, idx: u8) -> PadFeedback {
|
||||
let (rumble, raw) = pad.service();
|
||||
let hidout = raw
|
||||
.into_iter()
|
||||
.map(|(kind, data)| HidOutput::HidRaw {
|
||||
pad: idx,
|
||||
|
||||
Reference in New Issue
Block a user