feat(host/steam): shippable usbip/vhci_hcd virtual Deck + client leave-shortcuts
apple / screenshots (push) Has been cancelled
android / android (push) Has been cancelled
apple / swift (push) Has been cancelled
audit / cargo-audit (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
apple / screenshots (push) Has been cancelled
android / android (push) Has been cancelled
apple / swift (push) Has been cancelled
audit / cargo-audit (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
decky / build-publish (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
Steam Deck pass-through (design/steam-deck-passthrough-plan.md), code-complete + all CI checks green on Linux + adversarially reviewed; on-glass validation pending: - usbip/`vhci_hcd` virtual Deck transport (inject/linux/steam_usbip.rs) for non-SteamOS hosts (Bazzite/generic) — presents a real interface-2 USB Deck so Steam Input promotes it. In-process vhci attach (loopback OP_REQ_IMPORT handshake → sysfs attach) with a bounded `usbip`-CLI fallback; detach on drop. - Backed by a vendored, libusb-free trim of the `usbip` crate (crates/punktfunk-host/vendor/usbip-sim, MIT + NOTICE; host/cdc/hid + rusb/nusb removed; interrupt-IN paced by bInterval). - Selection ladder raw_gadget (SteamOS fast-path) → usbip (universal) → UHID, with PUNKTFUNK_STEAM_USBIP / PUNKTFUNK_USBIP_ATTACH knobs. - Shared Deck descriptors + the 0x83/0xAE feature contract + a Steam-accepted serial consolidated into steam_proto.rs; the raw_gadget backend reuses them. - Linux client leave-shortcuts: Ctrl+Alt+Shift+D + holding the escape chord (L1+R1+Start+Select) >=1.5s end the session (short press still exits fullscreen); the chord state resets across sessions. Also bundles in-progress work already staged in the tree: - host(kwin): xdg-output logical-geometry mapping so the KWin fake_input backend places absolute coordinates correctly under display scaling. - docs: design/README index entries + design/controller-only-mode.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+250
@@ -0,0 +1,250 @@
|
||||
//! A USB/IP server (simulation path only).
|
||||
//!
|
||||
//! Vendored + trimmed from `usbip` v0.8.0 (jiegec/usbip, MIT); the USB *host* modules and the
|
||||
//! `rusb`/`nusb` device constructors are removed so this carries no libusb dependency. See `NOTICE`.
|
||||
|
||||
use log::*;
|
||||
use num_derive::FromPrimitive;
|
||||
use num_traits::FromPrimitive;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{ErrorKind, Result};
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::sync::RwLock;
|
||||
use usbip_protocol::UsbIpCommand;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod consts;
|
||||
mod device;
|
||||
mod endpoint;
|
||||
mod interface;
|
||||
mod setup;
|
||||
pub mod usbip_protocol;
|
||||
mod util;
|
||||
pub use consts::*;
|
||||
pub use device::*;
|
||||
pub use endpoint::*;
|
||||
pub use interface::*;
|
||||
pub use setup::*;
|
||||
pub use util::*;
|
||||
|
||||
use crate::usbip_protocol::{UsbIpResponse, USBIP_RET_SUBMIT, USBIP_RET_UNLINK};
|
||||
|
||||
/// Main struct of a USB/IP server
|
||||
#[derive(Default, Debug)]
|
||||
pub struct UsbIpServer {
|
||||
available_devices: RwLock<Vec<UsbDevice>>,
|
||||
used_devices: RwLock<HashMap<String, UsbDevice>>,
|
||||
}
|
||||
|
||||
impl UsbIpServer {
|
||||
/// Create a [UsbIpServer] with simulated devices
|
||||
pub fn new_simulated(devices: Vec<UsbDevice>) -> Self {
|
||||
Self {
|
||||
available_devices: RwLock::new(devices),
|
||||
used_devices: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_device(&self, device: UsbDevice) {
|
||||
self.available_devices.write().await.push(device);
|
||||
}
|
||||
|
||||
pub async fn remove_device(&self, bus_id: &str) -> Result<()> {
|
||||
let mut available_devices = self.available_devices.write().await;
|
||||
|
||||
if let Some(device) = available_devices.iter().position(|d| d.bus_id == bus_id) {
|
||||
available_devices.remove(device);
|
||||
Ok(())
|
||||
} else if let Some(device) = self
|
||||
.used_devices
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.find(|d| d.bus_id == bus_id)
|
||||
{
|
||||
Err(std::io::Error::other(format!(
|
||||
"Device {} is in use",
|
||||
device.bus_id
|
||||
)))
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
ErrorKind::NotFound,
|
||||
format!("Device {bus_id} not found"),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handler<T: AsyncReadExt + AsyncWriteExt + Unpin>(
|
||||
mut socket: &mut T,
|
||||
server: Arc<UsbIpServer>,
|
||||
) -> Result<()> {
|
||||
let mut current_import_device_id: Option<String> = None;
|
||||
loop {
|
||||
let command = UsbIpCommand::read_from_socket(&mut socket).await;
|
||||
if let Err(err) = command {
|
||||
if let Some(dev_id) = current_import_device_id {
|
||||
let mut used_devices = server.used_devices.write().await;
|
||||
let mut available_devices = server.available_devices.write().await;
|
||||
match used_devices.remove(&dev_id) {
|
||||
Some(dev) => available_devices.push(dev),
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
if err.kind() == ErrorKind::UnexpectedEof {
|
||||
info!("Remote closed the connection");
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
let used_devices = server.used_devices.read().await;
|
||||
let mut current_import_device = current_import_device_id
|
||||
.clone()
|
||||
.and_then(|ref id| used_devices.get(id));
|
||||
|
||||
match command.unwrap() {
|
||||
UsbIpCommand::OpReqDevlist { .. } => {
|
||||
trace!("Got OP_REQ_DEVLIST");
|
||||
let devices = server.available_devices.read().await;
|
||||
|
||||
// OP_REP_DEVLIST
|
||||
UsbIpResponse::op_rep_devlist(&devices)
|
||||
.write_to_socket(socket)
|
||||
.await?;
|
||||
trace!("Sent OP_REP_DEVLIST");
|
||||
}
|
||||
UsbIpCommand::OpReqImport { busid, .. } => {
|
||||
trace!("Got OP_REQ_IMPORT");
|
||||
|
||||
current_import_device_id = None;
|
||||
current_import_device = None;
|
||||
std::mem::drop(used_devices);
|
||||
|
||||
let mut used_devices = server.used_devices.write().await;
|
||||
let mut available_devices = server.available_devices.write().await;
|
||||
let busid_compare =
|
||||
&busid[..busid.iter().position(|&x| x == 0).unwrap_or(busid.len())];
|
||||
for (i, dev) in available_devices.iter().enumerate() {
|
||||
if busid_compare == dev.bus_id.as_bytes() {
|
||||
let dev = available_devices.remove(i);
|
||||
let dev_id = dev.bus_id.clone();
|
||||
used_devices.insert(dev.bus_id.clone(), dev);
|
||||
current_import_device_id = dev_id.clone().into();
|
||||
current_import_device = Some(used_devices.get(&dev_id).unwrap());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let res = if let Some(dev) = current_import_device {
|
||||
UsbIpResponse::op_rep_import_success(dev)
|
||||
} else {
|
||||
UsbIpResponse::op_rep_import_fail()
|
||||
};
|
||||
res.write_to_socket(socket).await?;
|
||||
trace!("Sent OP_REP_IMPORT");
|
||||
}
|
||||
UsbIpCommand::UsbIpCmdSubmit {
|
||||
mut header,
|
||||
transfer_buffer_length,
|
||||
setup,
|
||||
data,
|
||||
..
|
||||
} => {
|
||||
trace!("Got USBIP_CMD_SUBMIT");
|
||||
let device = current_import_device.unwrap();
|
||||
|
||||
let out = header.direction == 0;
|
||||
let real_ep = if out { header.ep } else { header.ep | 0x80 };
|
||||
|
||||
header.command = USBIP_RET_SUBMIT.into();
|
||||
|
||||
let res = match device.find_ep(real_ep as u8) {
|
||||
None => {
|
||||
warn!("Endpoint {real_ep:02x?} not found");
|
||||
UsbIpResponse::usbip_ret_submit_fail(&header)
|
||||
}
|
||||
Some((ep, intf)) => {
|
||||
trace!("->Endpoint {ep:02x?}");
|
||||
trace!("->Setup {setup:02x?}");
|
||||
trace!("->Request {data:02x?}");
|
||||
let resp = device
|
||||
.handle_urb(
|
||||
ep,
|
||||
intf,
|
||||
transfer_buffer_length,
|
||||
SetupPacket::parse(&setup),
|
||||
&data,
|
||||
)
|
||||
.await;
|
||||
|
||||
match resp {
|
||||
Ok(resp) => {
|
||||
if out {
|
||||
trace!("<-Wrote {}", data.len());
|
||||
} else {
|
||||
trace!("<-Resp {resp:02x?}");
|
||||
}
|
||||
UsbIpResponse::usbip_ret_submit_success(&header, 0, 0, resp, vec![])
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Error handling URB: {err}");
|
||||
UsbIpResponse::usbip_ret_submit_fail(&header)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
res.write_to_socket(socket).await?;
|
||||
trace!("Sent USBIP_RET_SUBMIT");
|
||||
}
|
||||
UsbIpCommand::UsbIpCmdUnlink {
|
||||
mut header,
|
||||
unlink_seqnum,
|
||||
} => {
|
||||
trace!("Got USBIP_CMD_UNLINK for {unlink_seqnum:10x?}");
|
||||
|
||||
header.command = USBIP_RET_UNLINK.into();
|
||||
|
||||
let res = UsbIpResponse::usbip_ret_unlink_success(&header);
|
||||
res.write_to_socket(socket).await?;
|
||||
trace!("Sent USBIP_RET_UNLINK");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a USB/IP server at `addr` using [TcpListener]
|
||||
pub async fn server(addr: SocketAddr, server: Arc<UsbIpServer>) {
|
||||
let listener = TcpListener::bind(addr).await.expect("bind to addr");
|
||||
|
||||
let server = async move {
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((mut socket, _addr)) => {
|
||||
info!("Got connection from {:?}", socket.peer_addr());
|
||||
let new_server = server.clone();
|
||||
tokio::spawn(async move {
|
||||
let res = handler(&mut socket, new_server).await;
|
||||
info!("Handler ended with {res:?}");
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Got error {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
server.await
|
||||
}
|
||||
|
||||
// (Host-mode constructors and in-crate tests removed in the vendored copy — see NOTICE.)
|
||||
Reference in New Issue
Block a user