app
Adom USB
Public Unreviewedby John Lauer
See every USB device across the entire Adom ecosystem — laptop, WSL2, cloud container, Azure VM, workcells — in one dashboard, and mount / unmount / reset / test / force-class / sniff any of them. USB/IP slingshot for the whole mesh.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
//! The unified USB device inventory — the model behind the dashboard.
//!
//! A `Device` is one physical USB device, wherever it is physically plugged
//! in across the whole Adom ecosystem. Every device knows:
//! - its identity (vid/pid/serial/class) — the `BoundDevice` shape that the
//! workcell USB/IP feature already uses (hydrogen-desktop workcell.ts),
//! - its `source` host (where it's physically plugged in),
//! - where it can be mounted (`targets`) given the v1 routing matrix,
//! - where it is *currently* mounted (`mounted_on`), and
//! - which per-device actions are available (reset/test/class/sniff are
//! Linux-target-only — greyed out for Windows-native devices).
//!
//! For M1 the live inventory comes from (a) a real `lsusb` enumeration of THIS
//! container and (b) any registered agents (laptop/Azure/workcell) once wired.
//! Until the agents land, `--demo` seeds a representative cross-ecosystem set
//! so the dashboard is fully demonstrable. Demo devices are flagged `demo:true`
//! and never silently masquerade as real hardware.
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::process::Command;
/// A host in the mesh — a place a device can be plugged into and/or mounted.
#[derive(Clone, Serialize)]
pub struct Host {
pub id: String,
pub label: String,
/// laptop | azure-vm | wsl2 | container | workcell
pub kind: String,
pub os: String,
/// Filter bucket shown in the UI: "local" (user's own machine) or "cloud".
pub location: String,
/// Can this host *consume* a remote USB device as a mount target?
pub can_consume: bool,
/// Is this host a Linux target (so reset/test/class/sniff are possible)?
pub linux: bool,
/// Is the host's agent live, or is this a placeholder until wired?
pub agent_online: bool,
}
/// A mount target option offered for a given device.
#[derive(Clone, Serialize)]
pub struct Target {
pub host_id: String,
pub label: String,
/// usbipd-attach-wsl | reverse-tunnel | native | direct-tcp
pub mechanism: String,
/// Empty when available; a human reason when not (e.g. deferred).
pub blocked_reason: String,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct Device {
pub id: String,
pub name: String,
pub vid: u16,
pub pid: u16,
pub serial: Option<String>,
pub class: String,
pub bus_id: Option<String>,
/// host id where it is physically plugged in
pub source_id: String,
pub source_label: String,
/// "local" | "cloud" — mirrors the source host's bucket
pub location: String,
/// host id it is currently mounted on, or None
pub mounted_on: Option<String>,
pub demo: bool,
}
/// The full inventory snapshot the UI renders from. `overlay` carries live
/// mount-state changes made this session (device_id → Some(host) mounted /
/// None unmounted), applied on top of the freshly enumerated inventory.
pub fn snapshot(demo: bool, overlay: &std::collections::HashMap<String, Option<String>>) -> Value {
let hosts = hosts(demo);
let mut devices = enumerate_local();
if demo {
devices.extend(demo_devices());
}
for d in devices.iter_mut() {
if let Some(m) = overlay.get(&d.id) {
d.mounted_on = m.clone();
}
}
let devices_json: Vec<Value> = devices
.iter()
.map(|d| {
let targets = targets_for(d, &hosts);
// Exportable = can be slingshotted to a host other than its own source.
let exportable = targets.iter().any(|t| t.host_id != d.source_id);
let caps = capabilities_for(d, &hosts, exportable);
let mut v = serde_json::to_value(d).unwrap_or(json!({}));
v["exportable"] = json!(exportable);
v["vid_hex"] = json!(format!("{:04X}", d.vid));
v["pid_hex"] = json!(format!("{:04X}", d.pid));
v["targets"] = serde_json::to_value(&targets).unwrap_or(json!([]));
v["capabilities"] = caps;
v
})
.collect();
json!({
"hosts": serde_json::to_value(&hosts).unwrap_or(json!([])),
"devices": devices_json,
"demo": demo,
"counts": {
"total": devices.len(),
"local": devices.iter().filter(|d| d.location == "local").count(),
"cloud": devices.iter().filter(|d| d.location == "cloud").count(),
"mounted": devices.iter().filter(|d| d.mounted_on.is_some()).count(),
}
})
}
/// The hosts in the mesh. `this-container` is always real; the rest are agent
/// endpoints that come online as adom-desktop / azure-winvm / workcell register.
fn hosts(demo: bool) -> Vec<Host> {
let online = |id: &str| -> bool {
// TODO(agents): replace with a live registry probe (Curium /sources or
// a direct ping over the adom-desktop relay / azure-winvm). In demo
// mode everything is shown online so the matrix is explorable.
demo || id == "this-container"
};
vec![
Host { id: "laptop".into(), label: "John's laptop (Windows)".into(), kind: "laptop".into(),
os: "windows".into(), location: "local".into(), can_consume: true, linux: false, agent_online: online("laptop") },
Host { id: "laptop-wsl2".into(), label: "Laptop WSL2 / Hydrogen Desktop".into(), kind: "wsl2".into(),
os: "linux".into(), location: "local".into(), can_consume: true, linux: true, agent_online: online("laptop-wsl2") },
Host { id: "this-container".into(), label: "This cloud container".into(), kind: "container".into(),
os: "linux".into(), location: "cloud".into(), can_consume: true, linux: true, agent_online: true },
Host { id: "azure-vm".into(), label: "Azure VM desktop (Windows)".into(), kind: "azure-vm".into(),
os: "windows".into(), location: "cloud".into(), can_consume: false, linux: false, agent_online: online("azure-vm") },
Host { id: "azure-wsl2".into(), label: "Azure VM WSL2 / HD-on-VM".into(), kind: "wsl2".into(),
os: "linux".into(), location: "cloud".into(), can_consume: true, linux: true, agent_online: online("azure-wsl2") },
Host { id: "workcell-rpi-001".into(), label: "Workcell rpi-001".into(), kind: "workcell".into(),
os: "linux".into(), location: "cloud".into(), can_consume: false, linux: true, agent_online: online("workcell-rpi-001") },
]
}
/// v1 routing matrix: which hosts a given device may be mounted to.
/// Windows is source + local-only — a remote device cannot mount INTO a native
/// Windows desktop in v1 (needs the test-signed usbip-win client driver).
fn targets_for(d: &Device, hosts: &[Host]) -> Vec<Target> {
let src = hosts.iter().find(|h| h.id == d.source_id);
let src_machine = src.map(machine_of).unwrap_or_default();
// Only hosts with a USB *export* agent can slingshot a device elsewhere:
// laptop / Azure VM (usbipd-win) and workcells (usbip-host). A device whose
// source is a cloud container or a WSL2 distro is a consumer, not a source —
// it stays native on the host it's already on.
let exportable = matches!(src.map(|h| h.kind.as_str()), Some("laptop") | Some("azure-vm") | Some("workcell"));
if !exportable {
return vec![Target {
host_id: d.source_id.clone(),
label: format!("{} (native)", d.source_label),
mechanism: "native".into(),
blocked_reason: String::new(),
}];
}
hosts
.iter()
.filter_map(|h| {
if h.id == d.source_id && !h.linux {
// Native use on the same Windows box it's plugged into.
return Some(Target { host_id: h.id.clone(), label: format!("{} (native)", h.label),
mechanism: "native".into(), blocked_reason: String::new() });
}
if !h.can_consume {
if h.os == "windows" {
return Some(Target { host_id: h.id.clone(), label: h.label.clone(),
mechanism: "usbip-win-client".into(),
blocked_reason: "Deferred in v1 — mounting a remote device into native Windows needs the test-signed usbip-win client driver.".into() });
}
return None;
}
let same_machine = machine_of(h) == src_machine && !src_machine.is_empty();
let mechanism = if h.kind == "wsl2" && same_machine {
"usbipd-attach-wsl" // local, no relay
} else if h.os == "linux" {
"reverse-tunnel" // cross-machine via adom-desktop tunnel
} else {
"native"
};
Some(Target { host_id: h.id.clone(), label: h.label.clone(),
mechanism: mechanism.into(), blocked_reason: String::new() })
})
.collect()
}
/// Group hosts by the physical machine they live on, so "same machine" →
/// local usbipd attach --wsl rather than a relay tunnel.
fn machine_of(h: &Host) -> String {
match h.id.as_str() {
"laptop" | "laptop-wsl2" => "laptop-machine".into(),
"azure-vm" | "azure-wsl2" => "azure-machine".into(),
other => other.into(),
}
}
/// reset/test/class-override/sniff operate on the virtual device in a Linux
/// target. Available only when the device is currently mounted on a Linux host.
fn capabilities_for(d: &Device, hosts: &[Host], exportable: bool) -> Value {
let on_linux = d
.mounted_on
.as_ref()
.and_then(|m| hosts.iter().find(|h| &h.id == m))
.map(|h| h.linux)
.unwrap_or(false);
// "Mounted remotely" = slingshotted to a host other than its own source.
// A device sitting native on its own host is home, not unmountable.
let mounted_remote = d.mounted_on.as_deref().map(|m| m != d.source_id).unwrap_or(false);
json!({
"mount": exportable && !mounted_remote,
"unmount": mounted_remote,
"reset": on_linux,
"test": on_linux,
"class_override": on_linux,
"sniff": on_linux,
"reason": if on_linux { Value::Null } else { json!("Mount to a Linux target to reset / test / force-class / sniff.") }
})
}
/// Real enumeration of THIS container's USB via `lsusb`. Usually empty in a
/// cloud container, which is itself informative (nothing physically attached).
fn enumerate_local() -> Vec<Device> {
let out = match Command::new("lsusb").output() {
Ok(o) if o.status.success() => o,
_ => return Vec::new(),
};
String::from_utf8_lossy(&out.stdout)
.lines()
.filter_map(parse_lsusb_line)
.collect()
}
/// `Bus 001 Device 002: ID 1d6b:0002 Linux Foundation 2.0 root hub`
fn parse_lsusb_line(line: &str) -> Option<Device> {
let id_pos = line.find(" ID ")?;
let rest = &line[id_pos + 4..];
let (idpart, name) = rest.split_once(' ').unwrap_or((rest, ""));
let (vid_s, pid_s) = idpart.split_once(':')?;
let vid = u16::from_str_radix(vid_s.trim(), 16).ok()?;
let pid = u16::from_str_radix(pid_s.trim(), 16).ok()?;
// Root hubs are container plumbing, not user-actionable devices.
if vid == 0x1d6b { return None; }
let name = if name.trim().is_empty() { format!("{:04X}:{:04X} device", vid, pid) } else { name.trim().to_string() };
Some(Device {
id: format!("this-container:{:04x}:{:04x}", vid, pid),
name,
vid,
pid,
serial: None,
class: classify(vid, pid),
bus_id: None,
source_id: "this-container".into(),
source_label: "This cloud container".into(),
location: "cloud".into(),
mounted_on: Some("this-container".into()),
demo: false,
})
}
fn classify(vid: u16, _pid: u16) -> String {
match vid {
0x1d6b => "Hub".into(),
_ => "USB device".into(),
}
}
/// Representative cross-ecosystem devices for demoing the dashboard before the
/// live agents are wired. Includes the RP2040 from the canonical example.
fn demo_devices() -> Vec<Device> {
vec![
Device { id: "laptop:2e8a:0003".into(), name: "Raspberry Pi RP2040 (BOOTSEL)".into(),
vid: 0x2e8a, pid: 0x0003, serial: Some("E660C0D1C7332A28".into()), class: "Mass storage (UF2)".into(),
bus_id: Some("2-4".into()), source_id: "laptop".into(), source_label: "John's laptop (Windows)".into(),
location: "local".into(), mounted_on: None, demo: true },
Device { id: "laptop:0483:374b".into(), name: "ST-Link/V2.1 (STM32 debugger)".into(),
vid: 0x0483, pid: 0x374b, serial: Some("066BFF495054".into()), class: "Vendor / CMSIS-DAP".into(),
bus_id: Some("2-1".into()), source_id: "laptop".into(), source_label: "John's laptop (Windows)".into(),
location: "local".into(), mounted_on: Some("laptop-wsl2".into()), demo: true },
Device { id: "laptop:10c4:ea60".into(), name: "CP2102 USB-to-UART Bridge".into(),
vid: 0x10c4, pid: 0xea60, serial: Some("0001".into()), class: "CDC serial".into(),
bus_id: Some("2-3".into()), source_id: "laptop".into(), source_label: "John's laptop (Windows)".into(),
location: "local".into(), mounted_on: None, demo: true },
Device { id: "workcell-rpi-001:1a86:7523".into(), name: "CH340 Serial".into(),
vid: 0x1a86, pid: 0x7523, serial: None, class: "CDC serial".into(),
bus_id: Some("1-1".into()), source_id: "workcell-rpi-001".into(), source_label: "Workcell rpi-001".into(),
location: "cloud".into(), mounted_on: Some("this-container".into()), demo: true },
Device { id: "azure-vm:0403:6010".into(), name: "FTDI FT2232H (JTAG/serial)".into(),
vid: 0x0403, pid: 0x6010, serial: Some("FT9AB2CD".into()), class: "Vendor / JTAG".into(),
bus_id: Some("3-2".into()), source_id: "azure-vm".into(), source_label: "Azure VM desktop (Windows)".into(),
location: "cloud".into(), mounted_on: None, demo: true },
]
}