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
- Variables
- func DebugLogf(component, format string, args ...interface{})
- func SetDebugSink(w io.Writer) io.Writer
- type CtrlExchange
- type Descriptor
- type Diagnoser
- type DriverBinding
- type DriverInspector
- type Enumerator
- type MockEnumerator
- type MockTransport
- func (m *MockTransport) ClaimInterface(num int) error
- func (m *MockTransport) Close() error
- func (m *MockTransport) ControlIn(bRequest uint8, wValue, wIndex uint16, n int, timeoutMs int) ([]byte, error)
- func (m *MockTransport) ControlOut(bRequest uint8, wValue, wIndex uint16, data []byte, timeoutMs int) error
- func (m *MockTransport) Diagnostics() string
- func (m *MockTransport) ReleaseInterface(num int) error
- func (m *MockTransport) Remaining() int
- func (m *MockTransport) Reset() error
- func (m *MockTransport) StartBulkIn(epAddr byte, ringBufs, bufLen int, onPacket func([]byte), ...) error
- func (m *MockTransport) StopBulkIn() error
- type Transport
Constants ¶
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.
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 ¶
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.
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) ControlOut ¶
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 (*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.