usb

package
v0.2.2 Latest Latest
Warning

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

Go to latest
Published: May 25, 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_GEN_FAILURE on Windows.
	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")

	// 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 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 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

	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) 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