homelabby/.planning/phases/04-usb-manager-label-printing/04-01-SUMMARY.md
Mikkel Georgsen 63b66f7a94 docs(04-01): complete USB manager plan — goroutine-per-device, VID/PID enumeration
- 10/10 tests pass with -race flag
- goroutine count stable across 5 replug cycles
- T-04-01 read buffer cap mitigated
- Known stubs: VID/PID placeholder pending hardware 2026-04-13
2026-04-10 06:46:07 +00:00

5.2 KiB

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
04-usb-manager-label-printing 01 usb
usb
serial
goroutine
enumeration
reconnect
requires provides affects
internal/usb package
USBManager
DeviceSpec
DeviceEvent
Command
internal/api/router.go (future SSE integration)
main.go (future Manager.Start call)
added patterns
go.bug.st/serial v1.6.4
github.com/creack/goselect v0.1.2
goroutine-per-device
context+done-channel teardown
injectable test seams
created modified
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
go.mod
go.sum
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
duration_minutes completed_date tasks_completed tasks_total files_created files_modified
15 2026-04-10 2 2 6 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