symbolscope

package
v0.7.0 Latest Latest
Warning

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

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

Documentation

Overview

Package symbolscope recovers a live stream of demodulated symbols (the pre-slicer soft waveform plus the sliced dibit decisions) from a wideband IQ feed, for the web console's "Symbol" scope — GopherTrunk's take on OP25's Symbol plot.

It deliberately reuses the production DSP rather than re-implementing demod: the same down-converter the live decoder channelizes with (ccdecoder.Downconverter) feeds the same protocol receiver, and the receiver's existing SoftSink / DibitSink taps surface the symbols. Run a separate Engine on an iqtap broker subscription (the diag_provider.go pattern) and production control-channel decode is never touched.

Phase 1 supports P25 Phase 1: C4FM emits the FM-discriminator soft waveform aligned index-for-index with the sliced dibits; CQPSK emits the complex symbol-decision points (SymI/SymQ) — the true constellation — alongside the dibits, but no real soft track (its quality view is the constellation, not a 4-level eye). Other protocols — and a soft waveform for them — are a follow-up that adds a uniform soft tap to the remaining receivers; the Frame shape and this API are designed to absorb that without change.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Engine

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

Engine channelizes a wideband IQ feed and runs a protocol receiver, surfacing the recovered symbols as Frames. Not safe for concurrent Process calls — drive it from a single goroutine (the broker drain loop), exactly like diag.Decimator.

func New

func New(opts Options) (*Engine, error)

New constructs an Engine. Returns an error for an unsupported protocol, an absent Emit, or a non-positive input rate.

func (*Engine) Close

func (e *Engine) Close() error

Close releases the engine. Idempotent.

func (*Engine) Process

func (e *Engine) Process(iq []complex64)

Process channelizes one raw IQ chunk and runs the receiver over it. Completed frames are delivered via the Emit callback.

func (*Engine) SymbolRateHz

func (e *Engine) SymbolRateHz() float64

SymbolRateHz reports the recovered symbol rate (informational).

type Frame

type Frame struct {
	TimestampNs  int64     `json:"ts_ns"`
	SymbolRateHz float64   `json:"symbol_rate_hz"`
	CenterHz     uint32    `json:"center_hz"`
	OffsetHz     int32     `json:"offset_hz"`
	Soft         []float32 `json:"soft"`
	SymI         []float32 `json:"sym_i"`
	SymQ         []float32 `json:"sym_q"`
	// EyeSoft is a window of the oversampled, AGC-scaled matched-filter
	// output (EyeSPS samples per symbol) on the C4FM path — fold it over
	// the symbol period for the 4-level eye diagram. Empty on paths
	// without an eye tap (CQPSK). It is NOT aligned with Dibits (it runs
	// EyeSPS× faster) and is a bounded recent window, not the full
	// stream. EyeSPS is the integer samples-per-symbol to fold on.
	EyeSoft []float32 `json:"eye_soft"`
	EyeSPS  int       `json:"eye_sps"`
	Dibits  []uint8   `json:"dibits"`
	IsBits  bool      `json:"is_bits"`
	BaseIdx int       `json:"base_idx"`

	// Receiver-state metrics, read from the live receiver each frame —
	// the data behind the Tuning panel (OP25's Mixer / Tuner-FLL). The
	// getters are demod-specific: CarrierOffsetHz is the AFC estimate on
	// C4FM and the carrier-recovery estimate on CQPSK; ClockMu/ClockSPS
	// come from Mueller-Müller (C4FM) or Gardner (CQPSK); AGCLevel is the
	// C4FM symbol-AGC mean|x| or the CQPSK matched-filter AGC gain, with
	// AGCTarget set only on C4FM; CMAError is the CQPSK equalizer
	// convergence proxy (0 on C4FM).
	CarrierOffsetHz float64 `json:"carrier_offset_hz"`
	AGCLevel        float64 `json:"agc_level"`
	AGCTarget       float64 `json:"agc_target"`
	ClockMu         float64 `json:"clock_mu"`
	ClockSPS        float64 `json:"clock_sps"`
	CMAError        float64 `json:"cma_error"`
}

Frame is one batch of recovered symbols. Soft is the pre-slicer soft waveform (empty when the demod path exposes no soft tap, e.g. P25 CQPSK); Dibits are the sliced decisions (values 0..3 when IsBits is false, 0..1 when true). When Soft is non-empty it is aligned index-for-index with Dibits. BaseIdx is the absolute symbol index the batch starts at, so a client can detect gaps.

SymI/SymQ are the complex symbol-decision points — the true symbol-domain constellation sampled at each recovered symbol instant. They are populated only on the linear/CQPSK path (the post-Costas π/4-DQPSK points, which cluster at the four ±45°/±135° positions on a clean signal); the C4FM path leaves them empty because its symbol domain is the real 4-level soft already carried in Soft. When non-empty they are aligned index-for-index with Dibits.

type MixerFrame added in v0.4.0

type MixerFrame struct {
	TimestampNs     int64
	CenterHz        uint32
	SampleRateHz    uint32
	OffsetHz        int32
	CarrierOffsetHz float64
	RawBins         []float32
	TunedBins       []float32
}

MixerFrame is one snapshot of the channel baseband spectrum, the data behind GopherTrunk's Mixer plot (OP25's Raw Mixer / Tuned Mixer tabs).

RawBins is the FFT of the channelized baseband as the receiver sees it — the carrier sits wherever the residual offset leaves it. TunedBins is the same window re-mixed by the receiver's own carrier-offset estimate, so a locked loop pulls the carrier to 0 Hz (centre bin). Both are dBFS, FFT-shifted (index 0 = -SampleRateHz/2, index n/2 = DC), length the configured FFT size. The slices are freshly allocated per frame; the callee owns them.

type Options

type Options struct {
	// Protocol selects the receiver. Phase 1 supports trunking.ProtocolP25.
	Protocol trunking.Protocol
	// DemodMode is the P25 demod path: "c4fm" (default) or "cqpsk".
	// Ignored for other protocols.
	DemodMode string
	// InRateHz is the wideband input rate (the SDR's sample rate).
	InRateHz float64
	// OffsetHz tunes a channel sitting at this offset (relative to the
	// SDR centre) down to baseband before channelizing — pulls an
	// off-centre control/voice channel out from under the DC spike.
	OffsetHz int32
	// CenterHz stamps each frame with the SDR centre (informational).
	CenterHz uint32
	// SystemName / FrequencyHz are informational labels passed to the
	// receiver construction.
	SystemName  string
	FrequencyHz uint32
	// FrameSymbols is the per-frame symbol batch size. Zero picks
	// defaultFrameSymbols.
	FrameSymbols int
	// NowNs is an injectable clock for tests. Nil uses time.Now.
	NowNs func() int64
	// Emit receives each completed Frame. Required. The slices it
	// carries are freshly allocated per frame; the callee owns them.
	Emit func(Frame)

	// MixerEmit, when non-nil, enables the Mixer plot: the engine runs a
	// rate-limited FFT over the channelized baseband (raw) and over that
	// baseband re-mixed by the receiver's carrier-offset estimate (tuned),
	// delivering each pair as a MixerFrame. Nil disables the work entirely.
	MixerEmit func(MixerFrame)
	// MixerFFTSize is the FFT length for the Mixer plot (power of two).
	// Zero picks defaultMixerFFTSize. Ignored when MixerEmit is nil.
	MixerFFTSize int
	// MixerFPS caps the Mixer frame rate. Zero picks defaultMixerFPS.
	// Ignored when MixerEmit is nil.
	MixerFPS float64
}

Options configures an Engine.

Jump to

Keyboard shortcuts

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