usb

package
v0.7.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package usb is the platform-abstraction layer that the pure-Go RTL-SDR driver speaks to. It exposes the minimal slice of USB the RTL2832U demodulator needs — vendor control transfers in both directions and an async bulk-IN endpoint — and nothing else.

Each supported OS provides one implementation behind the Enumerator interface returned by DefaultEnumerator:

  • Linux uses USBDEVFS ioctls on /dev/bus/usb/BBB/DDD. See usb_linux.go.
  • Windows (PR-02) will use WinUSB via the system DLL.
  • macOS (PR-10) will use IOKit via purego.

A non-platform MockEnumerator/MockTransport pair lives alongside the real backends so the RTL2832U register layer and the tuner drivers can be unit-tested without touching real hardware.

Index

Constants

View Source
const (
	VendorOut uint8 = 0x40 // bmRequestType: host → device, vendor, device recipient
	VendorIn  uint8 = 0xC0 // bmRequestType: device → host, vendor, device recipient
)

Vendor request-type byte values for libusb-style control transfers. RTL2832U firmware only speaks vendor requests; class/standard requests are unused by the driver.

View Source
const (
	DefaultRingBuffers = 32
	DefaultBufferLen   = 16 * 1024
)

Default async-bulk geometry the RTL-SDR driver uses. Held here so the transport implementations can pre-validate caller inputs against a known-good baseline; the driver still supplies the values explicitly.

Variables

View Source
var (
	// ErrUnsupportedPlatform is returned by [DefaultEnumerator] when the
	// running OS has no USB backend compiled in.
	ErrUnsupportedPlatform = errors.New("usb: backend not implemented on this platform")

	// ErrBulkActive is returned by StartBulkIn when a stream is already
	// running on the transport.
	ErrBulkActive = errors.New("usb: bulk-IN already active")

	// ErrBulkInactive is returned by StopBulkIn when no stream is
	// running.
	ErrBulkInactive = errors.New("usb: bulk-IN not active")

	// ErrDeviceGone is returned by any operation after the device has
	// been physically removed or the kernel has decided it's dead.
	// Maps to ENODEV on Linux and ERROR_DEVICE_NOT_CONNECTED /
	// ERROR_NO_SUCH_DEVICE / ERROR_DEV_NOT_EXIST on Windows. Note
	// ERROR_GEN_FAILURE is NOT mapped here — that's a firmware
	// reject / driver-binding issue, not a physical disconnect
	// (see winErr in usb_windows.go; issue #270).
	ErrDeviceGone = errors.New("usb: device disconnected")

	// ErrTimeout is returned when a control transfer didn't complete
	// within the supplied timeoutMs window.
	ErrTimeout = errors.New("usb: transfer timed out")

	// ErrStreamStalled is surfaced via a bulk-IN stream's onStreamDead
	// when the reaper stopped delivering packets for the stall-watch
	// window while the device is still enumerated — the macOS Airspy
	// "silent freeze", where a blocking ReadPipe never returns so no
	// per-URB error is ever raised. The stall watchdog aborts the pipe
	// to unblock the reaper, which then dies like any other reaper death
	// (issue #345), converting a silent hang into a real EOF the driver
	// and daemon can act on. See bulk_stall.go.
	ErrStreamStalled = errors.New("usb: bulk-IN stream stalled (no data within watchdog window)")

	// ErrPipeStalled is returned when the device stalled the USB pipe
	// (CLEAR_FEATURE(ENDPOINT_HALT) recoverable). Maps to ERROR_GEN_FAILURE
	// on Windows/WinUSB — the clone-dongle cold-boot symptom where the
	// chip latches the first vendor-OUT write and then NAKs the next one
	// with byte-identical wire bytes. The librtlsdr-parity Linux
	// equivalent surfaces as syscall.EPIPE. Callers should clear the
	// halt via [Transport.Reset] and retry once.
	ErrPipeStalled = errors.New("usb: pipe stalled")

	// ErrClosed is returned by methods invoked after Close.
	ErrClosed = errors.New("usb: transport closed")
)

Sentinel errors. Implementations may wrap these or return their own types; callers compare with errors.Is.

Functions

func DebugLogf added in v0.2.9

func DebugLogf(component, format string, args ...interface{})

DebugLogf is the exported entry point to the gated debug sink for callers outside this package (e.g. the rtlsdr bring-up path in purego/driver.go, which logs the swallowed sacrificial-warmup failure). Like debugLogf it only emits when RTLSDR_DEBUG_USB is set, writes to the same sink (so SetDebugSink works in tests), and uses the same "rtlsdr-usb [component]" line prefix — keeping every USB diagnostic on one diffable channel.

func SetDebugSink added in v0.1.6

func SetDebugSink(w io.Writer) io.Writer

SetDebugSink redirects debug-transport output. Returns the previous sink so tests can restore it. Intended for tests only.

Types

type CtrlExchange

type CtrlExchange struct {
	In        bool
	BRequest  uint8
	WValue    uint16
	WIndex    uint16
	Data      []byte // expected for OUT, ignored for IN
	Reply     []byte // returned for IN, ignored for OUT
	Err       error  // returned instead of completing the transfer
	N         int    // expected `n` for IN (0 = don't check)
	TimeoutOK bool   // when true, ignore the SUT's timeout arg
}

CtrlExchange is one step in a MockTransport.Script. ControlIn matches against (BRequest, WValue, WIndex) and returns Reply; ControlOut matches the same triple plus the data payload and returns Err. Direction is inferred from In: if In is true the SUT must call ControlIn, otherwise ControlOut.

type Descriptor

type Descriptor struct {
	Bus          uint8  // USB bus number
	Address      uint8  // device address on the bus
	VID, PID     uint16 // vendor / product ID
	Serial       string // iSerialNumber, may be empty
	Manufacturer string // iManufacturer, may be empty
	Product      string // iProduct, may be empty
	// Path is the implementation-defined locator (e.g. /dev/bus/usb/001/007
	// on Linux). Treat as opaque; pass back to [Enumerator.Open] verbatim.
	Path string
}

Descriptor is the pre-resolved identity of a single USB device. Returned by Enumerator.List without claiming the device, so enumeration is safe to run without root privileges where sysfs permissions allow it.

type Diagnoser added in v0.2.9

type Diagnoser interface {
	Diagnostics() string
}

Diagnoser is an optional Transport capability. Implementations that can describe the underlying device — the bound driver, USB descriptors, a control-IN read probe — expose Diagnostics. The bring-up path (purego/driver.go) appends this to a failed Open so a single probe run captures everything needed to triage a dongle that rejects control transfers. The Windows transport implements it; transports that don't simply contribute no extra diagnostics.

type DriverBinding added in v0.2.3

type DriverBinding struct {
	Descriptor Descriptor
	DriverName string
	DriverDesc string
	Expected   string
	OK         bool
	Hint       string
	Err        error
}

DriverBinding is one row of the per-OS "what is currently bound to this dongle" report consumed by `gophertrunk sdr doctor`. It lets operators see why a dongle that physically appears in lsusb / Device Manager refuses to open: the function driver bound at plug time isn't the one the pure-Go RTL-SDR transport expects.

On Linux the expected state is "no driver" (the dongle is opened raw via usbdevfs). The auto-detach landed in usb_linux.go means most operators will see OK=true here even if dvb_usb_rtl28xxu was initially bound — the inspector reflects post-detach state.

On Windows the expected driver is "WinUSB". RTL2832UUSB / RTL28xxBDA are the in-box DVB-T drivers Windows binds by default; libusbK and libusb0 are Zadig-installable but not what the transport speaks; usbccgp is the composite-device parent (the operator picked the wrong node in Zadig).

type DriverInspector added in v0.2.3

type DriverInspector interface {
	Inspect(vid, pid uint16) ([]DriverBinding, error)
}

DriverInspector is the platform-abstract API the doctor subcommand calls. One implementation per OS, registered via platformDriverInspector. Returning ErrUnsupportedPlatform from Inspect is acceptable on exotic targets.

func DefaultDriverInspector added in v0.2.3

func DefaultDriverInspector() DriverInspector

DefaultDriverInspector returns the platform's inspector. Mirrors the shape of DefaultEnumerator so the doctor command needs no build tags of its own.

type Enumerator

type Enumerator interface {
	// Name identifies the backend (e.g. "usbdevfs", "winusb", "iokit",
	// "mock"). Used in error messages and logs.
	Name() string

	// List returns every device that matches both vid and pid. A zero
	// value for either acts as a wildcard.
	List(vid, pid uint16) ([]Descriptor, error)

	// Open claims a device identified by a Descriptor previously
	// returned from List.
	Open(d Descriptor) (Transport, error)
}

Enumerator discovers and opens USB devices on the host. One instance represents one platform backend; obtained via DefaultEnumerator.

func DefaultEnumerator

func DefaultEnumerator() Enumerator

DefaultEnumerator returns the platform's USB backend. On platforms without a backend it returns one whose every method yields ErrUnsupportedPlatform; this keeps higher layers compilable across the OS matrix while the driver is iterated PR-by-PR.

type MockEnumerator

type MockEnumerator struct {
	BackendName string       // override "mock" if set
	Devices     []Descriptor // returned by List, filtered by vid/pid
	OpenFunc    func(Descriptor) (*MockTransport, error)
}

MockEnumerator returns a fixed set of Descriptor values and opens each of them as a MockTransport. It is the test fixture used by every higher layer (rtl2832u, tuners) so the RTL-SDR driver can be exercised without a real dongle in CI.

func (*MockEnumerator) List

func (m *MockEnumerator) List(vid, pid uint16) ([]Descriptor, error)

func (*MockEnumerator) Name

func (m *MockEnumerator) Name() string

func (*MockEnumerator) Open

func (m *MockEnumerator) Open(d Descriptor) (Transport, error)

type MockTransport

type MockTransport struct {
	Script        []CtrlExchange
	Step          int
	Err           error
	ClaimedIfaces map[int]bool
	Closed        bool

	// ResetCalls counts invocations of Reset; tests use it to assert
	// the USBDEVFS_RESET recovery path ran. ClaimCalls counts
	// successful ClaimInterface invocations across all interfaces, so
	// tests can verify post-reset re-claim happened.
	ResetCalls int
	ClaimCalls int

	// ClaimErr, when non-nil, is returned by ClaimInterface instead of
	// succeeding — lets tests exercise the driver's claim-failure path
	// (e.g. an EBUSY that survives kernel-driver auto-detach).
	ClaimErr error

	// Diag, when non-empty, is returned by Diagnostics so tests can
	// assert the bring-up path appends transport diagnostics to a failed
	// Open. Mirrors the real winTransport.Diagnostics capability.
	Diag string

	BulkPackets  [][]byte
	BulkInterval time.Duration
	// BulkSimulateDeath, when true, makes the bulk-IN goroutine fire
	// onStreamDead (with BulkDeathErr, or ErrDeviceGone if nil) after
	// dispatching all BulkPackets instead of blocking on StopBulkIn.
	// Tests use it to assert drivers close their consumer channel when
	// the USB reaper dies unexpectedly (issue #345).
	BulkSimulateDeath bool
	BulkDeathErr      error
	// contains filtered or unexported fields
}

MockTransport replays a [Script] of CtrlExchange entries in order. Mismatched calls fail with an error captured in MockTransport.Err; tests inspect Err after running the SUT. Bulk-IN is supported by pushing pre-built byte slices into BulkPackets; the transport spaces them by BulkInterval (default 0 = back-to-back).

func NewMockTransport

func NewMockTransport() *MockTransport

NewMockTransport returns an empty mock that fails every transfer until Script is populated. Useful when a test only cares about lifecycle (Open / ClaimInterface / Close).

func (*MockTransport) ClaimInterface

func (m *MockTransport) ClaimInterface(num int) error

func (*MockTransport) Close

func (m *MockTransport) Close() error

func (*MockTransport) ControlIn

func (m *MockTransport) ControlIn(bRequest uint8, wValue, wIndex uint16, n int, timeoutMs int) ([]byte, error)

func (*MockTransport) ControlOut

func (m *MockTransport) ControlOut(bRequest uint8, wValue, wIndex uint16, data []byte, timeoutMs int) error

func (*MockTransport) Diagnostics added in v0.2.9

func (m *MockTransport) Diagnostics() string

Diagnostics implements the usb.Diagnoser optional interface, returning the canned Diag string. Lets tests assert the bring-up path appends transport diagnostics on a failed Open.

func (*MockTransport) ReleaseInterface

func (m *MockTransport) ReleaseInterface(num int) error

func (*MockTransport) Remaining

func (m *MockTransport) Remaining() int

Remaining returns the number of unconsumed CtrlExchange entries in the script. Tests typically assert it is zero after the SUT runs.

func (*MockTransport) Reset

func (m *MockTransport) Reset() error

func (*MockTransport) StartBulkIn

func (m *MockTransport) StartBulkIn(epAddr byte, ringBufs, bufLen int, onPacket func([]byte), onStreamDead func(error)) error

func (*MockTransport) StopBulkIn

func (m *MockTransport) StopBulkIn() error

type Transport

type Transport interface {
	// ControlIn issues a vendor-IN control transfer and returns up to n
	// bytes of response. The returned slice's length equals the number
	// of bytes the device actually delivered (≤ n).
	ControlIn(bRequest uint8, wValue, wIndex uint16, n int, timeoutMs int) ([]byte, error)

	// ControlOut issues a vendor-OUT control transfer.
	ControlOut(bRequest uint8, wValue, wIndex uint16, data []byte, timeoutMs int) error

	// ClaimInterface tells the kernel/driver that this transport owns
	// the given USB interface number. Must be called before any I/O.
	ClaimInterface(num int) error

	// ReleaseInterface relinquishes the interface; the device stays
	// open. Idempotent; calling on an unclaimed interface returns nil.
	ReleaseInterface(num int) error

	// StartBulkIn submits a ring of `ringBufs` bulk-IN URBs of `bufLen`
	// bytes each on endpoint epAddr (e.g. 0x81 for RTL-SDR's IQ stream).
	// onPacket is invoked from a dedicated reaper goroutine each time a
	// URB completes; the slice passed to it is owned by the transport
	// and reused once onPacket returns — callers must copy any bytes
	// they wish to retain.
	//
	// onStreamDead, when non-nil, is invoked exactly once when every URB
	// has died of an unrecoverable USB error (host controller hang,
	// ENODEV, EPROTO storm under load, ...) — i.e. the stream ended
	// **without** StopBulkIn being called. Implementations must not
	// invoke it on the normal StopBulkIn path, and must dispatch the
	// callback from a fresh goroutine — never from the reaper itself,
	// because the callback typically calls StopBulkIn (to close the
	// driver's consumer channel) and StopBulkIn waits for the reaper to
	// exit. Drivers use this hook to surface "stream died" to their IQ
	// consumer; without it, a silent reaper exit leaves the consumer
	// blocked forever (issue #345).
	StartBulkIn(epAddr byte, ringBufs, bufLen int, onPacket func([]byte), onStreamDead func(error)) error

	// StopBulkIn cancels every in-flight URB, drains the reaper, and
	// returns once the goroutine has exited. Safe to call concurrently
	// with reaper execution; idempotent.
	StopBulkIn() error

	// Reset performs a USB port reset. The device re-enumerates with
	// the same address (best effort) but configuration is lost.
	Reset() error

	// Close releases the device handle. Implies StopBulkIn and
	// ReleaseInterface for any still-held interfaces.
	Close() error
}

Transport is a claimed handle on a single USB device. Methods are not goroutine-safe by default — the RTL-SDR driver serializes control transfers through its own mutex — except that Transport.StopBulkIn must be safe to call while a foreign goroutine is blocked inside the bulk-IN reaper.

func MaybeWrapDebug added in v0.1.6

func MaybeWrapDebug(t Transport, desc Descriptor) Transport

MaybeWrapDebug returns t wrapped in a debug-logging Transport when RTLSDR_DEBUG_USB is set in the environment; otherwise it returns t unchanged. Wrapping is gated at Open time so the rest of the code path stays unaffected when debugging is off.

The wrapped transport logs every ControlIn/ControlOut/Reset call to stderr (or the sink set via SetDebugSink) in a format that's diffable against `LIBUSB_DEBUG=4` traces from osmocom librtlsdr's rtl_test:

rtlsdr-usb: ControlOut bmReqType=0x40 bReq=0x00 wValue=0x0034 wIndex=0x0610 wLength=17 timeout=300ms
rtlsdr-usb:   data=83 32 75 c0 ...
rtlsdr-usb:   → ok (after 1.2ms)

The label embeds the descriptor's bus/address (or serial when available) so multi-dongle traces remain attributable.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL