feat(05-01): TesterDriver interface, 4 mock drivers, KnownDevices tester entries

- TesterDriver interface: Connect/Read/Disconnect
- StreamingTesterDriver interface: embeds TesterDriver, adds Stream()
- TestResult struct: CableType, versions, speed, power, continuity, eMarker, resistance
- LiveReading struct: Voltage, CurrentAmps, PowerWatts, PDProtocol, Timestamp
- MockUSBDriver: deterministic USB 3.2 Gen 2 result, ErrNotConnected guard
- MockDPDriver: deterministic DP 1.4 result
- MockHDMIDriver: deterministic HDMI 2.1 result
- MockFNB58Driver: 3 LiveReading samples at 100ms, context-cancelled via Disconnect()
- KnownDevices: 4 RoleCableTester placeholder entries (dead0:0001-0004)
This commit is contained in:
Mikkel Georgsen 2026-04-10 07:07:27 +00:00
parent 384ffac870
commit 11aea60aee
3 changed files with 240 additions and 1 deletions

59
internal/tester/driver.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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.