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 ¶
New constructs an Engine. Returns an error for an unsupported protocol, an absent Emit, or a non-positive input rate.
func (*Engine) Process ¶
Process channelizes one raw IQ chunk and runs the receiver over it. Completed frames are delivered via the Emit callback.
func (*Engine) SymbolRateHz ¶
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.