diff --git a/internal/tester/driver.go b/internal/tester/driver.go new file mode 100644 index 0000000..4859da9 --- /dev/null +++ b/internal/tester/driver.go @@ -0,0 +1,59 @@ +package tester + +import ( + "errors" + "time" +) + +// ErrNotConnected is returned by Read() when Connect() has not been called. +var ErrNotConnected = errors.New("tester: not connected — call Connect() first") + +// CableType identifies the physical cable standard being tested. +type CableType int + +const ( + CableTypeUSB CableType = iota + CableTypeDP + CableTypeHDMI +) + +// TestResult captures all measurements from a single cable test run. +type TestResult struct { + CableType CableType + USBVersion string + DPVersion string + HDMIVersion string + SpeedGbps float64 + MaxWatts int + PinContinuity bool + HasEMarker bool + ResistanceOhm float64 +} + +// LiveReading is a single sample from a streaming power/protocol meter (e.g. FNB58). +type LiveReading struct { + Voltage float64 + CurrentAmps float64 + PowerWatts float64 + PDProtocol string + Timestamp time.Time +} + +// TesterDriver is the common interface for all discrete cable testers. +// Connect must be called before Read. +// Disconnect is idempotent. +type TesterDriver interface { + Connect() error + Read() (TestResult, error) + Disconnect() error +} + +// StreamingTesterDriver extends TesterDriver for devices that continuously +// emit live power/protocol readings (e.g. FNIRSI FNB58). +// Stream() returns a read-only channel that emits LiveReading values and is +// closed when the stream ends or Disconnect() is called. +// Calling Stream() before Connect() returns a pre-closed channel. +type StreamingTesterDriver interface { + TesterDriver + Stream() <-chan LiveReading +} diff --git a/internal/tester/mock_drivers.go b/internal/tester/mock_drivers.go new file mode 100644 index 0000000..3a2079b --- /dev/null +++ b/internal/tester/mock_drivers.go @@ -0,0 +1,175 @@ +package tester + +import ( + "context" + "time" +) + +// MockUSBDriver simulates a Treedix USB cable tester. +// Returns deterministic USB 3.2 Gen 2 test results. +// All fields are exported so test code can construct zero values directly. +type MockUSBDriver struct { + connected bool +} + +// Compile-time interface assertion. +var _ TesterDriver = (*MockUSBDriver)(nil) + +func (d *MockUSBDriver) Connect() error { + d.connected = true + return nil +} + +func (d *MockUSBDriver) Read() (TestResult, error) { + if !d.connected { + return TestResult{}, ErrNotConnected + } + return TestResult{ + CableType: CableTypeUSB, + USBVersion: "USB 3.2 Gen 2", + SpeedGbps: 10.0, + MaxWatts: 100, + PinContinuity: true, + HasEMarker: true, + ResistanceOhm: 0.12, + }, nil +} + +func (d *MockUSBDriver) Disconnect() error { + d.connected = false + return nil +} + +// MockDPDriver simulates a Treedix DisplayPort cable tester. +// Returns deterministic DP 1.4 test results. +type MockDPDriver struct { + connected bool +} + +// Compile-time interface assertion. +var _ TesterDriver = (*MockDPDriver)(nil) + +func (d *MockDPDriver) Connect() error { + d.connected = true + return nil +} + +func (d *MockDPDriver) Read() (TestResult, error) { + if !d.connected { + return TestResult{}, ErrNotConnected + } + return TestResult{ + CableType: CableTypeDP, + DPVersion: "1.4", + PinContinuity: true, + }, nil +} + +func (d *MockDPDriver) Disconnect() error { + d.connected = false + return nil +} + +// MockHDMIDriver simulates a Treedix HDMI cable tester. +// Returns deterministic HDMI 2.1 test results. +type MockHDMIDriver struct { + connected bool +} + +// Compile-time interface assertion. +var _ TesterDriver = (*MockHDMIDriver)(nil) + +func (d *MockHDMIDriver) Connect() error { + d.connected = true + return nil +} + +func (d *MockHDMIDriver) Read() (TestResult, error) { + if !d.connected { + return TestResult{}, ErrNotConnected + } + return TestResult{ + CableType: CableTypeHDMI, + HDMIVersion: "2.1", + PinContinuity: true, + }, nil +} + +func (d *MockHDMIDriver) Disconnect() error { + d.connected = false + return nil +} + +// MockFNB58Driver simulates an FNIRSI FNB58 USB power meter. +// Stream() emits 3 deterministic LiveReading samples at 100ms intervals, +// then closes the channel. Disconnect() cancels the goroutine early. +type MockFNB58Driver struct { + connected bool + streamCh chan LiveReading + cancelFunc context.CancelFunc +} + +// Compile-time interface assertion. +var _ StreamingTesterDriver = (*MockFNB58Driver)(nil) + +func (d *MockFNB58Driver) Connect() error { + d.connected = true + d.streamCh = make(chan LiveReading, 8) + ctx, cancel := context.WithCancel(context.Background()) + d.cancelFunc = cancel + go d.runStream(ctx) + return nil +} + +func (d *MockFNB58Driver) runStream(ctx context.Context) { + defer close(d.streamCh) + samples := []LiveReading{ + {Voltage: 5.1, CurrentAmps: 3.0, PowerWatts: 15.3, PDProtocol: "PD3.0", Timestamp: time.Now()}, + {Voltage: 5.1, CurrentAmps: 3.0, PowerWatts: 15.3, PDProtocol: "PD3.0", Timestamp: time.Now()}, + {Voltage: 5.1, CurrentAmps: 3.0, PowerWatts: 15.3, PDProtocol: "PD3.0", Timestamp: time.Now()}, + } + for _, s := range samples { + select { + case <-ctx.Done(): + return + case <-time.After(100 * time.Millisecond): + s.Timestamp = time.Now() + select { + case d.streamCh <- s: + case <-ctx.Done(): + return + } + } + } +} + +func (d *MockFNB58Driver) Read() (TestResult, error) { + if !d.connected { + return TestResult{}, ErrNotConnected + } + // FNB58 is a streaming device; discrete Read returns a snapshot. + return TestResult{ + CableType: CableTypeUSB, + MaxWatts: 100, + }, nil +} + +func (d *MockFNB58Driver) Disconnect() error { + if d.cancelFunc != nil { + d.cancelFunc() + d.cancelFunc = nil + } + d.connected = false + return nil +} + +// Stream returns the live reading channel. If called before Connect(), +// returns a pre-closed channel (never nil, never blocking). +func (d *MockFNB58Driver) Stream() <-chan LiveReading { + if !d.connected || d.streamCh == nil { + closed := make(chan LiveReading) + close(closed) + return closed + } + return d.streamCh +} diff --git a/internal/usb/device.go b/internal/usb/device.go index 4520682..2fc2ea7 100644 --- a/internal/usb/device.go +++ b/internal/usb/device.go @@ -33,7 +33,12 @@ func (s DeviceSpec) String() string { return s.VIDPID() + " (" + s.Name + ")" } // Update VID/PID values after hardware characterization on 2026-04-13. var KnownDevices = map[string]DeviceSpec{ "0525:a4a7": {VID: "0525", PID: "a4a7", Name: "PRT Qutie", Role: RolePrinter, BaudRate: 9600}, - // Phase 5: Treedix testers will be added here after characterization + // TODO(hardware): Replace placeholder VID:PIDs after characterization 2026-04-13 + "dead0:0001": {VID: "dead0", PID: "0001", Name: "Treedix USB Tester", Role: RoleCableTester, BaudRate: 115200}, + "dead0:0002": {VID: "dead0", PID: "0002", Name: "Treedix DP Tester", Role: RoleCableTester, BaudRate: 115200}, + "dead0:0003": {VID: "dead0", PID: "0003", Name: "Treedix HDMI Tester", Role: RoleCableTester, BaudRate: 115200}, + // TODO(hardware): BaudRate 0 = HID protocol — replace VID:PID after characterization + "dead0:0004": {VID: "dead0", PID: "0004", Name: "FNIRSI FNB58", Role: RoleCableTester, BaudRate: 0}, } // DeviceState represents the current connection status of a managed device.