--- phase: 04-usb-manager-label-printing plan: "01" subsystem: usb tags: [usb, serial, goroutine, enumeration, reconnect] dependency_graph: requires: [] provides: [internal/usb package, USBManager, DeviceSpec, DeviceEvent, Command] affects: [internal/api/router.go (future SSE integration), main.go (future Manager.Start call)] tech_stack: added: [go.bug.st/serial v1.6.4, github.com/creack/goselect v0.1.2] patterns: [goroutine-per-device, context+done-channel teardown, injectable test seams] key_files: created: - internal/usb/device.go - internal/usb/enumerate.go - internal/usb/manager.go - internal/usb/device_test.go - internal/usb/manager_test.go - internal/usb/mock_port_test.go modified: - go.mod - go.sum decisions: - "VID/PID enumeration via system_profiler SPUSBDataType JSON + injectable sysProfilerCmd for tests; gracefully returns empty map on Linux/CI" - "serialPort interface abstraction allows test injection via noopSerialOpener without hardware" - "mockPort.Read() blocks on closeCh channel until Close() is called — mirrors real blocking serial Read behavior" - "Read buffer capped at 4096 bytes per T-04-01 threat mitigation before forwarding to command handler" - "pollInterval injected at newManagerForTest level (20ms in tests vs 2s default) for fast deterministic cycling" metrics: duration_minutes: 15 completed_date: "2026-04-10" tasks_completed: 2 tasks_total: 2 files_created: 6 files_modified: 2 --- # Phase 4 Plan 01: USB Manager Summary **One-liner:** Goroutine-per-device USB Manager with VID/PID enumeration via system_profiler, context+done-channel teardown, and injectable test seams — no hardware required for testing. ## What Was Built ### internal/usb/device.go Core types: `DeviceSpec`, `DeviceRole`, `DeviceState`, `DeviceEvent`, `Command`, `CommandType`. `KnownDevices` registry maps `"VID:PID"` to `DeviceSpec` (PRT Qutie placeholder `0525:a4a7`). `ParseVIDPID()` helper with error on malformed input. ### internal/usb/enumerate.go `enumerateConnected()` shells out to `system_profiler SPUSBDataType -json`, parses the USB device tree, normalises hex IDs (strips `0x` prefix, lowercases), and cross-references with `serial.GetPortsList()` to return `map[VIDPID]portPath` containing only `KnownDevices` entries. Two injectable function vars (`sysProfilerCmd`, `serialPortsListCmd`) enable hermetic unit tests. Returns empty map with no error on Linux/CI (graceful degradation when `system_profiler` is unavailable). ### internal/usb/manager.go `Manager` owns all device goroutines. Poll loop calls `enumerateFunc()` every `pollInterval`, reconciles prev/current snapshots, spawns/stops `deviceLoop` goroutines. `deviceLoop` opens the serial port, runs an inner read goroutine (exits on `ctx.Done()` or port error), and handles `CmdWrite`/`CmdClose` commands. Goroutine teardown: child context cancelled → `port.Close()` called → read goroutine unblocked → `done` channel closed → poll loop proceeds. `ErrDeviceNotConnected` returned by `Send()` when device absent. ### Threat mitigations applied - **T-04-01**: Read buffer capped at 4096 bytes in `deviceLoop` before forwarding to command handler. ## Commits | Task | Commit | Description | |------|---------|-------------| | Task 1 | f5b1d31 | USB device types, KnownDevices registry, VID/PID enumeration | | Task 2 | 82eaf6b | USB Manager goroutine-per-device, poll loop, reconnect, leak-safe teardown | ## Test Results ``` === RUN TestKnownDevicesContainsPRTQutie PASS === RUN TestEnumerateConnectedMockOutput PASS === RUN TestDeviceSpecString PASS === RUN TestParseVIDPIDValid PASS === RUN TestParseVIDPIDInvalid PASS === RUN TestManagerStartSpawnsOneGoroutine PASS === RUN TestManagerDisconnectReconnectEvents PASS === RUN TestManagerStopGoroutineLeak PASS === RUN TestManagerSendToAbsentDevice PASS === RUN TestGoroutineStability PASS PASS (race detector clean, 30s timeout) ``` ## Deviations from Plan ### Auto-fixed Issues None — plan executed exactly as written. ### Additional Files `internal/usb/mock_port_test.go` was added (not listed in plan's `files_modified`). The plan referenced `newMockPort()` in the test action spec but did not create a dedicated file for it — the mock helper needed to be in a separate `_test.go` file to avoid shipping test infrastructure in the production binary. ## Known Stubs | File | Item | Reason | |------|------|--------| | internal/usb/device.go | `KnownDevices["0525:a4a7"]` VID/PID placeholder | Real VID/PID unknown until hardware arrives 2026-04-13 — update after characterization | | internal/usb/manager.go | Read bytes in `deviceLoop` are logged but not forwarded | No upstream consumer yet; Phase 4 plan 03 (printer driver) will consume read events | ## Threat Flags None — no new network endpoints, auth paths, or schema changes introduced. USB serial I/O is local-only per T-04-03 acceptance. ## Self-Check: PASSED - internal/usb/device.go: FOUND - internal/usb/enumerate.go: FOUND - internal/usb/manager.go: FOUND - internal/usb/device_test.go: FOUND - internal/usb/manager_test.go: FOUND - internal/usb/mock_port_test.go: FOUND - Commit f5b1d31: FOUND - Commit 82eaf6b: FOUND