Cross-platform Tauri app recreating Apple's old USB Prober.app:
hierarchical USB device tree with parsed Device descriptors,
Configuration descriptors, and HID Report descriptors.
See PLAN.md for the full architecture.
- Rust backend, React/TypeScript frontend (Tauri v2)
- Workspace layout:
crates/usb-types— shared data model (serde + specta types)crates/usb-collector-macos— nusb + ioreg HID pass collector (cfg-gated)crates/usb-collector-linux—/sys-based collector (cfg-gated)crates/usb-collector-windows— nusb-based collector (cfg-gated); partial descriptor supportcrates/hid-parser— platform-agnostic HID report descriptor parsercrates/usb-cli— standalone CLI binary (usb-probester-cli)src-tauri— Tauri shell, backend commands, text formatter
- Platform code behind
cfggates; frontend never sees platform-specific shapes. - Prefer parsing raw descriptor bytes over scraping pretty-printed tool output.
- Capture real OS output as test fixtures in
tests/fixtures/so unit tests don't depend on attached hardware.
✓ doneusb-typescratemacOS collector (nusb + ioreg HID pass)✓ done✓ donehid-parsercrateTauri wiring + basic frontend✓ doneLinux collector (✓ done/sys)Frontend tree + descriptor panels✓ doneHotplug (nusb watch_devices + auto-refresh toggle)✓ doneWindows basic enumeration (nusb)✓ done — HID descriptors via HidD_GetPreparsedData reconstructionClass-specific descriptors (CS_INTERFACE/HID/IAD)✓ done — CDC, Audio, MIDI decoded; generic hex fallback for unknownsRow selection✓ done — click/drag selects rows line-by-line; Cmd+C copies formatter-matched text
Both collector crates are unconditional workspace members. They use a
top-level #![cfg(target_os = "…")] to become no-ops on the wrong OS,
avoiding the need for conditional workspace membership. src-tauri/Cargo.toml
declares each under [target.'cfg(…)'.dependencies].
Reads everything from sysfs — no device open, no elevated privileges:
nusb::list_devices()for metadata (busnum, port_chain, strings, speed)descriptorssysfs file for raw USB descriptor bytes; parsed insrc/descriptor.rs- HID report descriptors from
<dev>/<dev>:<cfg>.<iface>/0003:<VID>:<PID>.<N>/report_descriptor location_idis the sysfs basename (e.g."2-4","2-2.3")
src-tauri/src/formatter.rs contains the Mac USB Prober-style text renderer,
shared by both the Tauri "Save Output" command and the CLI binary.
The same logic also lives in crates/usb-cli/src/main.rs (standalone copy
for the CLI; these should be kept in sync if the format changes).
# CLI — USB Prober-style text dump
cargo run -p usb-cli
# CLI — JSON dump
cargo run -p usb-cli -- --format json
# CLI — standalone release binary
cargo build --release -p usb-cli
# binary at target/release/usb-probester-cli
# Windows — cross-compile x86_64 binary from ARM Windows build machine
rustup target add x86_64-pc-windows-msvc
cargo build --release -p usb-cli --target x86_64-pc-windows-msvc
# binary at target\x86_64-pc-windows-msvc\release\usb-probester-cli.exe
# Linux — live USB enumeration
cargo run -p usb-collector-linux --example dump_one
# Linux — parse stored sysfs descriptors binary (built-in blink(1) fixture)
cargo run -p usb-collector-linux --example from_sysfs_file
# Linux — parse a real sysfs descriptors file
cargo run -p usb-collector-linux --example from_sysfs_file -- /sys/bus/usb/devices/2-4/descriptors
# macOS — structured dump
cargo run -p usb-collector-macos --example dump_one
# macOS — USB Prober-style text output
cargo run -p usb-collector-macos --example prober_fmt
# Build everything
cargo build
# Run Tauri dev server
npm run tauridev
# Build release app bundle
npm run tauribuild
# Clean all build artifacts
npm run cleancrates/usb-collector-windows/src/lib.rs — nusb-based, cfg-gated with #![cfg(target_os = "windows")].
nusb::list_devices()for metadata (port_chain, strings, speed)- nusb opens devices via
GUID_DEVINTERFACE_USB_DEVICE(hub-level access); gives descriptor reads for ALL devices, not just WinUSB ones claim_interface(n)requires WinUSB on interface n; class-driver interfaces (HID.sys, usbstor, usbaudio, usbser) return "incompatible driver" and cannot be claimedlocation_idis"{vid:04x}:{pid:04x}:{serial}"or"…:{port.chain}"if no serialbus_numberis always 0 (Windows doesn't expose it the same way)- HID report descriptors via
src/hid.rs— preparsed-data approach (hidapi style):- SetupDi enumerates all HID device interfaces (
GUID_DEVINTERFACE_HID) - Opens each with
CreateFile→HidD_GetAttributesfor VID/PID/version HidD_GetSerialNumberStringfor serial (map key:(vid, pid, serial))parse_interface_numberextractsMI_xxfrom the device path forinterface_numberHidD_GetPreparsedData→HidP_GetCaps/HidP_GetButtonCaps/HidP_GetValueCapsto enumerate capabilities for Input, Output, Feature report types- Reconstructs a synthetic but valid and parseable HID report descriptor
- Works for all HID devices regardless of driver (HID.sys, WinUSB, etc.)
- SetupDi enumerates all HID device interfaces (
The Windows kernel unconditionally overwrites bmRequest=0x80 in
IOCTL_USB_GET_DESCRIPTOR_FROM_NODE_CONNECTION, making HID class descriptor
requests (types 0x21, 0x22) impossible. HidD_GetReportDescriptor is kernel-mode
only and not exported from user-mode hid.dll. The preparsed-data path is the
correct user-mode approach and is what hidapi uses.
- Not byte-identical to the device's original descriptor
- Vendor-specific items absent (not exposed via
HidP_APIs) - Sub-collection nesting flattened to a single Application collection
- Item ordering may differ from the original
- Output is valid HID and fully parseable by the
hid-parsercrate
rustup target add x86_64-pc-windows-msvc
cargo build --release -p usb-cli --target x86_64-pc-windows-msvc
crates/usb-formatter/src/lib.rs — the Mac USB Prober-style text renderer used by
both the Tauri "Save Output" command and the CLI binary.
HID report descriptors are nested inside the HID Descriptor block, matching the
USB Prober reference fixture:
Interface #N - HID
HID Descriptor
Descriptor Version Number: 0x0111
Country Code: 0
Descriptor Count: 1
Descriptor 1
Type: 0x22 (Report Descriptor)
Length (and contents): 156
Raw Descriptor (hex) 0000: ...
Parsed Report Descriptor:
Usage Page (Generic Desktop)
Endpoint ...
The GUI tree view uses the same hierarchy via ClassSpecificNode in App.tsx.
All planned steps done. No blocking code issues remain.