diff --git a/.planning/phases/04-usb-manager-label-printing/04-01-SUMMARY.md b/.planning/phases/04-usb-manager-label-printing/04-01-SUMMARY.md new file mode 100644 index 0000000..346baac --- /dev/null +++ b/.planning/phases/04-usb-manager-label-printing/04-01-SUMMARY.md @@ -0,0 +1,110 @@ +--- +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