sdr

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package sdr defines the abstract Device interface for IQ sources and the pool that supervises a fleet of dongles. Concrete drivers (RTL-SDR, mock, future HackRF/Airspy) live in subpackages and register themselves here.

Index

Constants

View Source
const DefaultSampleRateHz uint32 = 2_048_000

DefaultSampleRateHz mirrors librtlsdr's open-time default and is the rate Pool.Open programs when the caller passes 0. Matches the value the rtlsdr driver also programs during bring-up so the two layers agree on a known-good fallback.

View Source
const DefaultWatchdogInterval = 30 * time.Second

DefaultWatchdogInterval is the polling cadence used when the caller doesn't override it. 30s is short enough to catch a transient USB drop within a single failure cycle but long enough that the periodic USB re-enumerate doesn't show up as background load on slow hubs.

View Source
const MockDriverName = "mock"
View Source
const MockFloat32DriverName = "mock-f32"

Variables

This section is empty.

Functions

func NotifyIQDrop added in v0.3.0

func NotifyIQDrop(info Info)

NotifyIQDrop reports that the device described by info dropped one IQ chunk. Backends call it from their stream reaper's overrun branch. It is a no-op when no observer is installed. Safe for any goroutine.

func Register

func Register(d Driver)

func SetIQDropObserver added in v0.3.0

func SetIQDropObserver(fn func(Info))

SetIQDropObserver installs fn as the process-wide IQ-drop observer. fn is called (on the dropping driver's reaper goroutine) once per dropped chunk with the device's Info, so the caller can record the iq_underruns_total metric and emit a rate-limited warning. Passing nil clears the observer. Safe to call concurrently with active streams.

Types

type Device

type Device interface {
	Info() Info
	SetCenterFreq(hz uint32) error
	SetSampleRate(hz uint32) error
	SetGain(tenthDB int) error // -1 selects automatic gain control
	SetPPM(ppm int) error
	// SetBiasTee toggles the dongle's 5V bias-tee output (used to
	// power external LNAs through the antenna SMA). Devices without
	// the circuit silently no-op. Implementations should return nil
	// if the underlying driver doesn't model bias-tee at all.
	SetBiasTee(enable bool) error
	StreamIQ(ctx context.Context) (<-chan []complex64, error)
	Close() error
}

Device is the per-dongle handle. Implementations must be safe for the goroutines that call StreamIQ; concurrent SetCenterFreq during streaming is allowed (the underlying USB transport handles it).

type Driver

type Driver interface {
	Name() string
	Enumerate() ([]Info, error)
	Open(idx int) (Device, error)
}

Driver is the factory each backend exposes.

func DriverByName

func DriverByName(name string) (Driver, error)

func Drivers

func Drivers() []Driver

type Hint

type Hint struct {
	Serial  string
	Role    Role
	PPM     int
	Gain    int // tenths of dB; negative = auto
	BiasTee bool
	// contains filtered or unexported fields
}

Hint guides role assignment when opening devices. Match by serial first; fall back to first-found.

PPM, Gain, and BiasTee carry per-device tuning that Pool.Open applies once the device is opened. Gain follows the Device.SetGain convention: a negative value selects automatic gain control. PPM is in parts-per-million; 0 is fine for the TCXO-equipped NESDR Smart v5 and similar dongles.

func (Hint) WithGain

func (h Hint) WithGain(tenthDB int) Hint

WithGain returns a copy of h with Gain set and the gain-set flag flipped so Pool.Open knows to apply it.

type Info

type Info struct {
	Driver       string
	Index        int
	Serial       string
	Manufacturer string
	Product      string
	TunerName    string
	Gains        []int
}

Info describes a discovered device, returned by drivers' enumeration.

func EnumerateAll

func EnumerateAll() ([]Info, []error)

EnumerateAll asks every registered driver to list its devices. It returns the combined device list plus one error per driver that failed to enumerate, so callers can surface the failure instead of silently reporting an empty list.

type MockDevice

type MockDevice struct {
	// contains filtered or unexported fields
}

MockDevice replays a single .cfile in real time.

func (*MockDevice) Close

func (d *MockDevice) Close() error

func (*MockDevice) Info

func (d *MockDevice) Info() Info

func (*MockDevice) SetBiasTee

func (d *MockDevice) SetBiasTee(bool) error

func (*MockDevice) SetCenterFreq

func (d *MockDevice) SetCenterFreq(uint32) error

func (*MockDevice) SetGain

func (d *MockDevice) SetGain(int) error

func (*MockDevice) SetPPM

func (d *MockDevice) SetPPM(int) error

func (*MockDevice) SetSampleRate

func (d *MockDevice) SetSampleRate(hz uint32) error

func (*MockDevice) StreamIQ

func (d *MockDevice) StreamIQ(ctx context.Context) (<-chan []complex64, error)

StreamIQ reads the file in chunks of ~16 KiB and meters delivery to roughly match the configured sample rate. Closing the channel signals EOF.

type MockDriver

type MockDriver struct {
	Files []string
}

MockDriver replays unsigned-8-bit IQ files (.cfile / .iq) from a directory. Each file becomes one logical "device". Used for tests and offline replay.

func (*MockDriver) Enumerate

func (m *MockDriver) Enumerate() ([]Info, error)

func (*MockDriver) Name

func (m *MockDriver) Name() string

func (*MockDriver) Open

func (m *MockDriver) Open(idx int) (Device, error)

type MockFloat32Driver

type MockFloat32Driver struct {
	Files []string
}

MockFloat32Driver replays interleaved-float32 IQ files (GNU Radio cfile).

func (*MockFloat32Driver) Enumerate

func (m *MockFloat32Driver) Enumerate() ([]Info, error)

func (*MockFloat32Driver) Name

func (m *MockFloat32Driver) Name() string

func (*MockFloat32Driver) Open

func (m *MockFloat32Driver) Open(idx int) (Device, error)

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool holds a fleet of opened SDR devices and assigns roles.

func NewPool

func NewPool(logger *slog.Logger) *Pool

NewPool constructs an empty pool. The optional bus is used to publish events.KindSDRAttached / events.KindSDRDetached as devices come and go; pass nil to disable that side effect (tests and the `gophertrunk sdr list` CLI both run without a bus).

func (*Pool) AllByRole

func (p *Pool) AllByRole(r Role) []*PoolEntry

AllByRole returns every device with the given role.

func (*Pool) Close

func (p *Pool) Close() error

func (*Pool) Entries

func (p *Pool) Entries() []*PoolEntry

func (*Pool) FindBySerial

func (p *Pool) FindBySerial(serial string) *PoolEntry

FindBySerial returns the entry whose info.Serial matches, or nil. Used by the demod-pipeline composer to look up a Voice device that the engine has just bound to a call.

func (*Pool) FirstByRole

func (p *Pool) FirstByRole(r Role) *PoolEntry

FirstByRole returns the first device with the given role, or nil.

func (*Pool) Open

func (p *Pool) Open(sampleRateHz uint32, hints []Hint) error

Open is a backwards-compatible shim over OpenWith. It preserves the historical "open every enumerated device" behaviour; callers that want allowlist semantics should construct PoolOpenOptions and call OpenWith directly.

func (*Pool) OpenWith added in v0.2.6

func (p *Pool) OpenWith(opts PoolOpenOptions) error

OpenWith enumerates every registered driver, opens the devices the options select, programs the IQ sample rate on each one (issue #275 — without this the chip streams at whatever rate its resampler powered up at), and assigns roles. The first opened device gets RoleControl unless a hint says otherwise; subsequent devices get RoleVoice.

When opts.Strict is false, every discovered device is opened. A non-hinted device gets an auto-assigned role and runs with the driver's default PPM / gain.

When opts.Strict is true, only devices whose serial matches a hint are opened. Discovered devices without a matching hint are logged at INFO and skipped. Hints whose serial doesn't match any discovered device produce a WARN. Hints with empty serial are dropped at ingest with a WARN — an empty-serial hint in strict mode is ambiguous (no way to honour an allowlist entry that doesn't name anything).

Strict mode is how operators get "only the devices I listed in config.yaml are touched"; rtl_tcp and baseband replay always originate from explicit config entries, so strict mode applies to them uniformly. An rtl_tcp endpoint without a serial: in config is therefore skipped in strict mode — set serial: on the endpoint to keep it.

A device whose SetSampleRate fails is closed and skipped — a wrong-rate radio produces silent decoder failures, which is worse than no radio at all.

func (*Pool) Reacquire added in v0.2.2

func (p *Pool) Reacquire(serial string, sampleRateHz uint32) (*PoolEntry, error)

Reacquire releases the existing device handle for the given serial and tries to re-open the same serial against the entry's original driver. On success the PoolEntry's Device is swapped in place — Role, Hint, and serial identity are preserved, Info.Index updates to reflect the new enumeration — and KindSDRDetached + KindSDRAttached events are published so consumers (and the API/web snapshot) observe the swap. The configured sample rate plus the original Hint (PPM / gain / bias-tee) are re-applied to the fresh handle.

Designed for recovery from transient USB disconnect/re-enumerate cycles: the kernel assigns a new device number but the dongle reports the same serial. The caller (typically the daemon's ccdecoder retry loop) drives the backoff between attempts. Closing the existing handle is best-effort — a dead handle's Close may return errors which are logged but not surfaced. See issue #345.

Returns the refreshed PoolEntry on success, or an error if the serial is unknown to the pool, the driver re-enumerate misses the serial, or open / sample-rate programming fails.

func (*Pool) RunWatchdog added in v0.2.2

func (p *Pool) RunWatchdog(ctx context.Context, interval time.Duration, sampleRateHz uint32) error

RunWatchdog ticks every interval, re-enumerates every registered driver, and acts on serial-level state changes against the pool:

  • A serial that the pool holds but the enumerate does NOT see transitions to "missing" and surfaces a KindSDRDetached event (the pool's API/TUI/web snapshot consumers see the gap).
  • A serial that was missing in the previous tick and is now back in the enumerate triggers Pool.Reacquire so the freshly re- enumerated USB handle replaces the dead one before the next consumer touches it.

The watchdog only acts on the *transition*: a device that was always present and is still present is left alone (no spurious reacquires on healthy hardware), and a device that's been missing for many ticks waits for the actual reappear before any work happens.

Used by the daemon to keep idle voice / control SDRs warm across flaky USB cycles without waiting for the next consumer to surface the failure. The in-stream IQ-death retry (ccdecoder retry loop, VoicePool.Bind reacquire) still owns the in-use case. See issue #345.

Returns ctx.Err() on shutdown. Pass interval <= 0 to disable the watchdog entirely (returns ctx.Err() after ctx cancels, no ticks).

func (*Pool) SetBus

func (p *Pool) SetBus(bus *events.Bus)

SetBus attaches an events bus so the pool can publish attach/detach events. Idempotent; passing nil silently disables publishing.

func (*Pool) Snapshot

func (p *Pool) Snapshot() []SDRStatus

Snapshot returns a status payload for every entry currently in the pool. Safe to call concurrently with Open / Close.

type PoolEntry

type PoolEntry struct {
	Driver Driver
	Device Device
	Info   Info
	Role   Role
	Hint   Hint
}

PoolEntry tracks a single discovered-and-opened device along with its role.

Hint carries the per-device tuning the pool applied at Open time so a later Snapshot can render gain/PPM/bias-tee state without having to query the underlying chip.

func (*PoolEntry) Snapshot

func (e *PoolEntry) Snapshot(attached bool) SDRStatus

Snapshot returns the wire-format status payload for this entry. Used by the API's GET /api/v1/devices handler and the bus payload on the sdr.attached / sdr.detached events.

attached == true is the normal "device is in the pool" case; the detached snapshot published by Pool.Close passes false.

type PoolOpenOptions added in v0.2.6

type PoolOpenOptions struct {
	// SampleRateHz is the IQ rate to program on every opened device.
	// Zero falls back to DefaultSampleRateHz.
	SampleRateHz uint32
	// Hints carries the per-device tuning the pool applies once each
	// device is opened (PPM, gain, bias-tee, role). Hints are matched
	// to discovered devices by serial.
	Hints []Hint
	// Strict treats Hints as an allowlist: a discovered device whose
	// serial is not present in Hints is logged and skipped instead of
	// being auto-roled. The daemon engages strict mode when the user
	// has populated cfg.SDR.Devices — that's the operator's signal
	// that they want only the devices they named, not whatever else
	// happens to be on the USB bus.
	Strict bool
}

PoolOpenOptions parameterises Pool.OpenWith. Use this when callers need to engage strict mode; the historical Pool.Open(rate, hints) signature still works and remains the default for code paths that want today's open-everything behaviour.

type Role

type Role int
const (
	RoleAuto Role = iota
	RoleControl
	RoleVoice
	// RoleWideband pins a dongle to a single configured centre
	// frequency. Several decoders share the IQ stream — each one is
	// tapped to a different repeater frequency inside the dongle's
	// IQ bandwidth via the internal/dsp/tuner package. Used to cover
	// a cluster of co-band conventional repeaters (e.g. several DMR
	// Tier II carriers around 453 MHz) with a single SDR.
	RoleWideband
)

func ParseRole

func ParseRole(s string) Role

func (Role) String

func (r Role) String() string

type SDRStatus

type SDRStatus struct {
	Driver       string `json:"driver"`
	Serial       string `json:"serial"`
	Manufacturer string `json:"manufacturer,omitempty"`
	Product      string `json:"product,omitempty"`
	TunerName    string `json:"tuner_name,omitempty"`
	Role         string `json:"role"`
	Attached     bool   `json:"attached"`

	// Configured hint values applied at open time. PPM is in
	// parts-per-million; GainTenthDB follows the SetGain convention
	// (negative = AGC). BiasTee reflects whether the YAML asked the
	// pool to enable the 5 V output.
	GainTenthDB int  `json:"gain_tenth_db"`
	GainAuto    bool `json:"gain_auto"`
	PPM         int  `json:"ppm"`
	BiasTee     bool `json:"bias_tee"`

	// Gains is the tuner's quantized gain ladder (tenths of dB),
	// useful for UIs that want to render valid choices.
	Gains []int `json:"gains,omitempty"`
}

SDRStatus is the per-device snapshot the pool publishes on the events bus when a device is opened or closed, and the same payload returned by GET /api/v1/devices. Fields that are unknown at snapshot time (e.g. the daemon never programmed a gain because the YAML left it blank) are zero-valued; consumers should treat that as "default" / "unset" rather than "explicitly zero".

The shape mirrors the `gophertrunk.v1.SDRStatus` proto message but keeps the JSON layer self-contained so the api package doesn't have to import the pb generated types just to render the response.

Directories

Path Synopsis
Package airspy is a pure-Go driver for the Airspy R2 / Airspy Mini software-defined radios, implementing sdr.Driver and sdr.Device.
Package airspy is a pure-Go driver for the Airspy R2 / Airspy Mini software-defined radios, implementing sdr.Driver and sdr.Device.
Package airspyhf is a pure-Go driver for the Airspy HF+ family (Discovery, Dual Port, and the legacy HF+), implementing sdr.Driver and sdr.Device.
Package airspyhf is a pure-Go driver for the Airspy HF+ family (Discovery, Dual Port, and the legacy HF+), implementing sdr.Driver and sdr.Device.
Package baseband adds wideband IQ recording and offline replay to the SDR layer.
Package baseband adds wideband IQ recording and offline replay to the SDR layer.
Package hackrf is a pure-Go driver for the Great Scott Gadgets HackRF One software-defined radio, implementing the sdr.Driver and sdr.Device interfaces.
Package hackrf is a pure-Go driver for the Great Scott Gadgets HackRF One software-defined radio, implementing the sdr.Driver and sdr.Device interfaces.
Package iqtap fans an SDR's IQ stream out to additional observers without disturbing the primary consumer's StreamIQ contract.
Package iqtap fans an SDR's IQ stream out to additional observers without disturbing the primary consumer's StreamIQ contract.
purego
Package purego is the pure-Go RTL-SDR driver — the sdr.Device / sdr.Driver implementation that composes the platform USB transport (internal/sdr/rtlsdr/usb), the RTL2832U register layer (internal/sdr/rtlsdr/rtl2832u), and the per-chip tuner drivers (internal/sdr/rtlsdr/tuners).
Package purego is the pure-Go RTL-SDR driver — the sdr.Device / sdr.Driver implementation that composes the platform USB transport (internal/sdr/rtlsdr/usb), the RTL2832U register layer (internal/sdr/rtlsdr/rtl2832u), and the per-chip tuner drivers (internal/sdr/rtlsdr/tuners).
rtl2832u
Package rtl2832u is the pure-Go register / I2C-bridge layer that sits between the platform USB transport (internal/sdr/rtlsdr/usb) and the per-tuner drivers.
Package rtl2832u is the pure-Go register / I2C-bridge layer that sits between the platform USB transport (internal/sdr/rtlsdr/usb) and the per-tuner drivers.
tuners
Package tuners houses the per-chip tuner drivers that sit between the RTL2832U register layer (internal/sdr/rtlsdr/rtl2832u) and the top-level [sdr.Device].
Package tuners houses the per-chip tuner drivers that sit between the RTL2832U register layer (internal/sdr/rtlsdr/rtl2832u) and the top-level [sdr.Device].
usb
Package usb is the platform-abstraction layer that the pure-Go RTL-SDR driver speaks to.
Package usb is the platform-abstraction layer that the pure-Go RTL-SDR driver speaks to.
Package rtltcp implements an sdr.Driver that talks to a remote rtl_tcp server.
Package rtltcp implements an sdr.Driver that talks to a remote rtl_tcp server.
Package wbvoice puts P25 / DMR voice grants on the same SDR that's hosting a trunked control channel via the wideband channelizer.
Package wbvoice puts P25 / DMR voice grants on the same SDR that's hosting a trunked control channel via the wideband channelizer.

Jump to

Keyboard shortcuts

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