Documentation
¶
Overview ¶
Package siglab is the offline signal replay / testing / analysis core shared by the gophertrunk replay, analyze, gen, test and siglab-TUI subcommands. It drives any protocol GopherTrunk can decode through the same production receiver + control-channel pipelines the daemon runs (via the ccdecoder factory map), collects a protocol-agnostic structured Result, and exposes signal-quality analysis, synthesis, and a metadata-driven acceptance harness on top of it.
The engine deliberately mirrors the daemon's IQ → DDC → pipeline chain so a replay lock implies an on-air lock and a replay failure makes the offline capture a reproducible fixture (the same contract the original replay subcommand established for P25 in issue #402).
Index ¶
- func DiscoverMetadata(capturePath string) string
- func EncodeCapture(iq []complex64, format SampleFormat) []byte
- func Fixtures() []trunking.Protocol
- func HasFixture(p trunking.Protocol) bool
- func ParseProtocolCLI(s string) (trunking.Protocol, error)
- func SortedDecodeErrors(m map[string]int) []string
- func SynthesizeAndAnalyze(synth SynthOptions, decode Config, onEvent func(EventRecord)) (*Result, *Metadata, error)
- func WriteCapture(path string, iq []complex64, format SampleFormat) error
- func WriteMetadata(path string, m *Metadata) error
- func WriteResult(w io.Writer, r *Result, f Format) error
- type Acceptance
- type BandPlanSlot
- type CCStatsBreakdown
- type CandidateScore
- type CarrierResult
- type CarrierRole
- type ChannelRef
- type Config
- type DemodMetrics
- type EventRecord
- type FECStat
- type Format
- type GrantRecord
- type IQPoint
- type IQTaps
- type IdentifyConfig
- type IdentifyResult
- type LockInfo
- type Metadata
- type NIDDecode
- type NeighborRef
- type P25P1Detail
- type PDURecord
- type ProtocolDetail
- type RailStat
- type ReceiverState
- type Result
- func Run(path string, cfg Config) (*Result, error)
- func RunAutoTuneMulti(path string, cfg Config, maxCandidates int) (*Result, error)
- func RunReader(r io.Reader, source string, cfg Config) (*Result, error)
- func RunReaderAutoTuneMulti(r io.ReadSeeker, source string, cfg Config, maxCandidates int) (*Result, error)
- func RunReaderMonitor(r io.Reader, source string, cfg Config, tick time.Duration, ...) (*Result, error)
- func RunReaderStream(r io.Reader, source string, cfg Config, onEvent func(EventRecord)) (*Result, error)
- func RunStream(path string, cfg Config, onEvent func(EventRecord)) (*Result, error)
- type RotationStat
- type SampleDecoder
- type SampleFormat
- type SignalQuality
- type SoftEye
- type SpreadStat
- type SurveySpectrum
- type SyncLandscape
- type SyncStat
- type SyncVariant
- type SynthOptions
- type SystemRollup
- type TopologyProvider
- type TopologySnapshot
- type Verdict
- type WidebandConfig
- type WidebandResult
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func DiscoverMetadata ¶
DiscoverMetadata finds the sidecar for a capture by stem: it tries <stem>.metadata.json, <stem>.metadata.yaml, then <capture>.json/.yaml. Returns "" when none exists.
func EncodeCapture ¶ added in v0.3.3
func EncodeCapture(iq []complex64, format SampleFormat) []byte
EncodeCapture returns IQ encoded in the given on-disk format (u8 or f32). It is the pure (no-I/O) core shared by WriteCapture and the in-memory synth→decode bridge (SynthesizeAndAnalyze), so a synthesized signal can be fed straight back into the engine via a bytes.Reader without touching disk.
func Fixtures ¶
Fixtures returns the protocols with a registered synthesis fixture, in stable order, for the `gen` usage message and protocol picker.
func HasFixture ¶
HasFixture reports whether a synthesis fixture is registered for p.
func ParseProtocolCLI ¶
ParseProtocolCLI maps a command-line protocol name to a trunking.Protocol. It first honours the legacy replay aliases ("p25p1" → P25 Phase 1, "dmr-tier3" → DMR Tier III) so existing replay invocations keep working, then falls back to the canonical trunking.ParseProtocol (which covers all config spellings: p25, p25-phase2, dmr, dmr-tier2, nxdn, dpmr, edacs, motorola, ltr, mpt1327, tetra, ysf, dstar).
func SortedDecodeErrors ¶
SortedDecodeErrors returns the decode-error stages in a stable order, for deterministic text/CSV rendering by callers.
func SynthesizeAndAnalyze ¶ added in v0.3.3
func SynthesizeAndAnalyze(synth SynthOptions, decode Config, onEvent func(EventRecord)) (*Result, *Metadata, error)
SynthesizeAndAnalyze synthesizes an idealized (optionally impaired) signal for synth.Protocol and immediately analyzes it through the production pipeline, all in memory — no capture file is written to disk. It is the one-call path behind the web interface's "compare against a generated idealized signal" feature: the returned Result (and its IQTaps, when decode.CaptureIQ is set) can be diffed against an uploaded capture's Result.
The decode template carries the analysis knobs the caller wants (CollectIQDiag, CaptureIQ, CaptureIQMaxPoints, Conjugate, IQCorrect, DemodMode and the P25 deep knobs); the protocol, sample rate, format and per-protocol system knobs are taken from the synthesis fixture so the decode always matches the signal that was generated. onEvent (when non-nil) receives each EventRecord live, exactly like RunReaderStream.
func WriteCapture ¶
func WriteCapture(path string, iq []complex64, format SampleFormat) error
WriteCapture writes IQ to path in the given format (u8 or f32).
func WriteMetadata ¶
WriteMetadata writes m to path as JSON (the sidecar `test` auto-discovers).
Types ¶
type Acceptance ¶
type Acceptance struct {
// Lock, when non-nil, requires the run to (true) or to not (false) lock
// the control channel.
Lock *bool `json:"lock,omitempty" yaml:"lock,omitempty"`
// LockLatencyMaxSec caps the time-to-first-lock (0 ⇒ unchecked).
LockLatencyMaxSec float64 `json:"lock_latency_max_sec,omitempty" yaml:"lock_latency_max_sec,omitempty"`
// LockFields are expected lock-payload fields (NAC, ColorCode,
// SystemID, …). Values may be given as numbers or hex strings
// ("0x293"); comparison is hex-tolerant. Matched as a subset of the
// observed lock fields (and the common frequency_hz).
LockFields map[string]any `json:"lock_fields,omitempty" yaml:"lock_fields,omitempty"`
// MinGrants requires at least this many grants (0 ⇒ unchecked).
MinGrants int `json:"min_grants,omitempty" yaml:"min_grants,omitempty"`
// BaudTolerancePct caps |effective − expected| / expected (0 ⇒ unchecked).
BaudTolerancePct float64 `json:"baud_tolerance_pct,omitempty" yaml:"baud_tolerance_pct,omitempty"`
// MaxDecodeErrorRate caps decode errors per 1000 symbols (0 ⇒ unchecked).
MaxDecodeErrorRate float64 `json:"max_decode_error_rate,omitempty" yaml:"max_decode_error_rate,omitempty"`
// MaxEVMPct caps the demod error-vector magnitude percentage (0 ⇒
// unchecked). Grades demod quality directly — a capture whose eye is too
// closed fails even if it happens to lock.
MaxEVMPct float64 `json:"max_evm_pct,omitempty" yaml:"max_evm_pct,omitempty"`
// MinSNRdB requires the estimated demod SNR to be at least this many dB
// (0 ⇒ unchecked).
MinSNRdB float64 `json:"min_snr_db,omitempty" yaml:"min_snr_db,omitempty"`
}
Acceptance is the expected-outcome contract a capture is graded against by the `test` harness. A nil field means "don't check this dimension". It generalizes the per-test assertions the integration_cc_*_test.go files and samples/README.md acceptance criteria describe into one schema.
type BandPlanSlot ¶ added in v0.3.5
type BandPlanSlot = trunking.TopoBandPlanSlot
BandPlanSlot is one band-plan entry. See trunking.TopoBandPlanSlot.
type CCStatsBreakdown ¶
type CCStatsBreakdown struct {
NIDTrusted int64 `json:"nid_trusted" yaml:"nid_trusted"`
NIDMarginal int64 `json:"nid_marginal" yaml:"nid_marginal"`
NIDFailed int64 `json:"nid_failed" yaml:"nid_failed"`
TSBKDecoded int64 `json:"tsbk_decoded" yaml:"tsbk_decoded"`
TSBKTrellisFailed int64 `json:"tsbk_trellis_failed" yaml:"tsbk_trellis_failed"`
TSBKCRCFailed int64 `json:"tsbk_crc_failed" yaml:"tsbk_crc_failed"`
// NetStatusSeen, RFSSStatusSeen, and AdjacentSeen count the identity
// broadcasts decoded (0x3B / 0x3A / 0x3C). NetStatusSeen == 0 on a locked
// CC explains a blank WACN/System ID: the periodic NSB never landed.
NetStatusSeen int64 `json:"net_status_seen" yaml:"net_status_seen"`
RFSSStatusSeen int64 `json:"rfss_status_seen" yaml:"rfss_status_seen"`
AdjacentSeen int64 `json:"adjacent_seen" yaml:"adjacent_seen"`
// LocRegSeen counts accepted Location Registration Responses (0x2B), the
// high-rate RFSS/Site source. RFSS/Site resolved via LocRegSeen while
// NetStatusSeen == 0 is the normal shape of a site that names itself in
// registrations but withholds the WACN-bearing NSB.
LocRegSeen int64 `json:"loc_reg_seen" yaml:"loc_reg_seen"`
}
CCStatsBreakdown mirrors p25phase1.CCStats: the per-frame decode outcomes.
type CandidateScore ¶
type CandidateScore struct {
Protocol string `json:"protocol"`
Locked bool `json:"locked"`
LockLatencySec float64 `json:"lock_latency_sec"`
SyncHits int `json:"sync_hits"`
SyncVariant string `json:"sync_variant"`
ModalSpacing int `json:"modal_spacing"`
FECPassRate float64 `json:"fec_pass_rate"`
Score float64 `json:"score"`
// contains filtered or unexported fields
}
CandidateScore is one protocol's identification evidence and composite score.
type CarrierResult ¶ added in v0.4.3
type CarrierResult struct {
OffsetHz float64 `json:"offset_hz" yaml:"offset_hz"`
FreqHz uint32 `json:"freq_hz" yaml:"freq_hz"`
SNRDb float32 `json:"snr_db" yaml:"snr_db"`
Protocol string `json:"protocol" yaml:"protocol"`
Confidence float64 `json:"confidence" yaml:"confidence"`
Inconclusive bool `json:"inconclusive" yaml:"inconclusive"`
Locked bool `json:"locked" yaml:"locked"`
Role CarrierRole `json:"role" yaml:"role"`
ColorCode uint8 `json:"color_code,omitempty" yaml:"color_code,omitempty"`
SystemID uint32 `json:"system_id,omitempty" yaml:"system_id,omitempty"`
Grants []GrantRecord `json:"grants,omitempty" yaml:"grants,omitempty"`
Score float64 `json:"score" yaml:"score"`
// Blind / ReferenceMatches name a carrier that did NOT decode, via the
// offline signal-ID reference DB (Protocol is blanked for such carriers and
// Role is RoleUnknown). Populated from the per-carrier identify fallback.
Blind *blind.BlindEstimate `json:"blind,omitempty" yaml:"blind,omitempty"`
ReferenceMatches []sigref.Match `json:"reference_matches,omitempty" yaml:"reference_matches,omitempty"`
// contains filtered or unexported fields
}
CarrierResult is one detected carrier surveyed end-to-end.
func (*CarrierResult) Identify ¶ added in v0.4.3
func (c *CarrierResult) Identify() *IdentifyResult
Identify returns the carrier's ranked identification.
func (*CarrierResult) Result ¶ added in v0.4.3
func (c *CarrierResult) Result() *Result
Result returns the winning protocol's full run Result (topology + grants), or nil. Used by the offline hunt bridge to fold each carrier into a system.
type CarrierRole ¶ added in v0.4.3
type CarrierRole string
CarrierRole labels a surveyed carrier within a trunked system.
const ( RoleUnknown CarrierRole = "" RoleControl CarrierRole = "control" RoleVoice CarrierRole = "voice" )
type ChannelRef ¶ added in v0.3.5
type ChannelRef = trunking.TopoChannelRef
ChannelRef is a band-plan channel coordinate. See trunking.TopoChannelRef.
type Config ¶
type Config struct {
// Protocol selects the production pipeline to drive. Every protocol the
// ccdecoder factory map registers is supported.
Protocol trunking.Protocol
// System carries per-protocol knobs (demod mode, colour code, band
// plan, FEC modes, …) exactly as the daemon reads them off a configured
// trunking.System. SystemName/FrequencyHz are surfaced separately
// because every factory consumes them.
System trunking.System
SystemName string
FrequencyHz uint32
// SampleRateHz is the capture's IQ sample rate (input rate, before any
// down-conversion). Required.
SampleRateHz float64
// Format is the on-disk sample encoding.
Format SampleFormat
// TuneHz frequency-shifts the capture so a channel at +TuneHz lands at
// 0 Hz before demod. AutoTune estimates it from a prefix of the file
// (and overrides TuneHz when set).
TuneHz float64
AutoTune bool
// Conjugate negates Q before channelization (spectrum-inverted /
// I-Q-swapped front-end, issue #264). IQCorrect applies blind
// I/Q-imbalance correction to the raw IQ before decimation (issue #402).
Conjugate bool
IQCorrect bool
// CollectIQDiag enables the protocol-agnostic signal-quality analyzer
// (symbol histogram, IQ imbalance, soft-sample distribution where
// available). Off by default since it buffers per-symbol observations.
CollectIQDiag bool
// DemodMode is the P25 Phase 1 symbol-recovery path: "c4fm" (default) or
// "cqpsk". Empty ⇒ c4fm.
DemodMode string
// NIDSearchSpan overrides the NID-alignment search radius in dibits
// (issue #275 bisect knob). 0 ⇒ the production default.
NIDSearchSpan int
// EnableDDA opts the C4FM path into the experimental decision-directed
// AFC (issue #402; off in production).
EnableDDA bool
// EnableAdaptiveSlicer opts the C4FM path into the adaptive slicer
// (issue #402; off in production).
EnableAdaptiveSlicer bool
// EnableSoftSync opts the P25 control channel into the wider FSW-fallback
// tolerance: sync words carrying 5–8 symbol errors are decoded if the
// frame's TSBK CRC corroborates the NID, extending lock reach into
// marginal-SNR captures. Gated by the existing NID/CRC validation, so it
// cannot manufacture a false lock (issue #771; off in production).
EnableSoftSync bool
// CollectReceiverState captures a per-(stream-)second snapshot of the P25
// receiver's AFC/AGC/clock/slicer state into the Result (the per-second
// state log replay emitted to stderr, now structured + exportable).
CollectReceiverState bool
// ChunkSamples is the read-loop chunk size in IQ samples; 0 ⇒ default.
ChunkSamples int
// RecordDDCPath, when non-empty, writes the post-DDC narrowband IQ (the
// exact channelized complex stream the receiver decodes, at the DDC output
// rate — 144 kHz for TETRA, ~48 kHz for the C4FM family) to a two-channel
// 16-bit baseband WAV at that path. This is the offline half of the "record
// the digital down-converter output" tooling: point it at a fat 2.5/10 MS/s
// cfile and it emits a small narrowband WAV that `replay -format wav`
// decodes bit-for-bit the same way — a shareable, reproducible fixture that
// is orders of magnitude smaller than the wideband capture. Empty ⇒ no
// recording.
RecordDDCPath string
// CaptureIQ enables buffering a stride-decimated copy of the
// post-DDC (channelized) IQ and the pre-slicer soft samples into
// Result.IQTaps, so a web/visualization consumer can render the
// constellation, spectrogram, PSD and eye diagram client-side. Off
// by default since it is O(captured points) memory. The capture is
// always decimated and hard-capped (see CaptureIQMaxPoints), so it
// never ships full-rate IQ.
CaptureIQ bool
// CaptureIQMaxPoints caps the number of decimated IQ points (and,
// separately, soft samples) retained when CaptureIQ is set. 0 ⇒
// defaultCaptureIQMaxPoints. The read loop strides the channelized
// stream so the retained density targets visualization fidelity
// rather than faithful reconstruction.
CaptureIQMaxPoints int
// MaxSamples caps the number of input IQ samples processed (0 ⇒ whole
// file). The signal identifier sets this to scan only a prefix of a
// capture, bounding per-candidate cost regardless of capture length.
MaxSamples int64
// CollectPDUs retains a per-signaling-block dissection (Result.PDUs) of the
// data PDUs the decoder parses — the data-over-RF inspector feed. Off by
// default; P25 Phase 1 (TSBK) only for now. Capped at pduMaxRecords.
CollectPDUs bool
// Acceptance, when non-nil, is evaluated against the Result to produce a
// pass/fail Verdict (the `test` harness sets it).
Acceptance *Acceptance
// Log receives the production pipeline's structured diagnostics. nil ⇒
// a discard logger (the engine never wants a nil *slog.Logger reaching a
// factory that does not nil-guard it).
Log *slog.Logger
}
Config drives a single offline run of the engine over one capture (or a synthesized stream). The zero value is not runnable — Protocol, a sample source, and SampleRateHz must be set.
type DemodMetrics ¶ added in v0.3.7
type DemodMetrics struct {
// Modulation names the estimator that produced these numbers: "c4fm" (the
// 4-level soft eye) or "cqpsk" (the complex π/4-DQPSK constellation).
Modulation string `json:"modulation" yaml:"modulation"`
// EVMPct is the RMS error-vector magnitude as a percentage of the ideal
// symbol level. A clean lock sits at a few percent; it climbs toward the
// inter-rail half-spacing (~33% for C4FM) as the eye closes.
EVMPct float64 `json:"evm_pct" yaml:"evm_pct"`
// SNREstimateDB is the symbol SNR implied by the residual, in dB. For
// C4FM this is the post-discriminator soft-axis SNR (lower than the input
// Es/N0 by the FM detection loss); for CQPSK it is the constellation SNR.
// Capped at demodSNRCapDB for a noise-free synthetic input.
SNREstimateDB float64 `json:"snr_estimate_db" yaml:"snr_estimate_db"`
// SymbolsAnalyzed is the soft-sample count the estimate was formed over
// (after the warmup skip).
SymbolsAnalyzed int64 `json:"symbols_analyzed" yaml:"symbols_analyzed"`
// PeakEVMPct is the worst single-symbol EVM (≥ EVMPct).
PeakEVMPct float64 `json:"peak_evm_pct" yaml:"peak_evm_pct"`
// MagErrPct / PhaseErrDeg split the error vector into its amplitude
// (RMS magnitude error, %) and angular (RMS phase error, degrees) parts.
MagErrPct float64 `json:"mag_err_pct" yaml:"mag_err_pct"`
PhaseErrDeg float64 `json:"phase_err_deg" yaml:"phase_err_deg"`
// CarrierFreqErrorHz is the residual carrier-frequency error the receiver's
// AFC/CQPSK loop settled at, in Hz (surfaced from the receiver, not
// re-estimated). Zero when the deep receiver did not expose it.
CarrierFreqErrorHz float64 `json:"carrier_freq_error_hz" yaml:"carrier_freq_error_hz"`
// IQGainImbalanceDB / QuadratureErrorDeg are the I/Q gain imbalance and
// quadrature-skew measured on the *recovered* constellation — distinct from
// SignalQuality's pre-DDC front-end IQ figures.
IQGainImbalanceDB float64 `json:"iq_gain_imbalance_db" yaml:"iq_gain_imbalance_db"`
QuadratureErrorDeg float64 `json:"quadrature_error_deg" yaml:"quadrature_error_deg"`
// OriginOffsetPct is the residual DC (carrier-leakage) offset as a percent
// of the reference radius.
OriginOffsetPct float64 `json:"origin_offset_pct" yaml:"origin_offset_pct"`
// EVMTrace is the per-symbol EVM% time series, stride-decimated to the IQ
// cap. ErrorVectorSpectrum is the FFT-shifted magnitude spectrum (dB) of
// the residual error vectors — a spur in it points at a periodic
// impairment rather than white noise. Both omitempty so the CSV/summary
// path is unaffected.
EVMTrace []float32 `json:"evm_trace,omitempty" yaml:"evm_trace,omitempty"`
ErrorVectorSpectrum []float32 `json:"error_vector_spectrum,omitempty" yaml:"error_vector_spectrum,omitempty"`
}
DemodMetrics is the demodulator-quality summary computed from the recovered soft symbols: error-vector magnitude (constellation dispersion) and the SNR implied by the residual about the ideal symbol positions. It is the siglab-surfaced face of internal/radio/p25/phase1/metrics.
type EventRecord ¶
type EventRecord struct {
Seq int `json:"seq" yaml:"seq"`
OffsetSec float64 `json:"offset_sec" yaml:"offset_sec"`
Kind string `json:"kind" yaml:"kind"`
Fields map[string]any `json:"fields" yaml:"fields"`
}
EventRecord is one bus event captured generically — Kind plus the reflection-flattened payload — so the JSONL stream and the events CSV represent every protocol's events without a typed switch per protocol.
type FECStat ¶
type FECStat struct {
Stage string `json:"stage" yaml:"stage"`
Frames int `json:"frames" yaml:"frames"`
Clean int `json:"clean" yaml:"clean"`
Corrected int `json:"corrected" yaml:"corrected"`
Uncorrectable int `json:"uncorrectable" yaml:"uncorrectable"`
CRCPass int `json:"crc_pass,omitempty" yaml:"crc_pass,omitempty"`
CRCFail int `json:"crc_fail,omitempty" yaml:"crc_fail,omitempty"`
}
FECStat tallies one FEC stage's outcomes across the frames found at clean sync hits — the direct analog of P25's NID trusted/marginal/uncorrectable.
type Format ¶
type Format int
Format selects an output encoding for a Result.
const ( // FormatTextSummary is the human-readable summary (handled by the // caller's renderer, not WriteResult). Defined so callers share one // Format enum. FormatTextSummary Format = iota // FormatJSON is a single indented JSON object. FormatJSON // FormatJSONL is one JSON object per line: every captured event, then a // final summary object tagged "summary". FormatJSONL // FormatYAML is a single YAML document. FormatYAML // FormatCSVSummary is a two-line CSV (header + one summary row). FormatCSVSummary // FormatCSVEvents is a CSV with one row per captured event. FormatCSVEvents // FormatCSVPDUs is a CSV with one row per dissected signaling PDU. FormatCSVPDUs )
func ParseFormat ¶
ParseFormat maps a -format flag value to a Format. csv defaults to the summary shape; use "csv-events" for the per-event shape.
type GrantRecord ¶
type GrantRecord struct {
OffsetSec float64 `json:"offset_sec" yaml:"offset_sec"`
GroupID uint32 `json:"group_id" yaml:"group_id"`
SourceID uint32 `json:"source_id" yaml:"source_id"`
ChannelID uint8 `json:"channel_id" yaml:"channel_id"`
ChannelNum uint16 `json:"channel_num" yaml:"channel_num"`
Timeslot uint8 `json:"timeslot" yaml:"timeslot"`
FrequencyHz uint32 `json:"frequency_hz" yaml:"frequency_hz"`
Encrypted bool `json:"encrypted" yaml:"encrypted"`
// AlgorithmID / KeyID mirror trunking.Grant: the encryption algorithm id and
// key selector when Encrypted (0 / 0 otherwise). They let the survey name the
// encryption type (e.g. AES-256, ADP/RC4) rather than just "encrypted".
AlgorithmID uint8 `json:"algorithm_id,omitempty" yaml:"algorithm_id,omitempty"`
KeyID uint16 `json:"key_id,omitempty" yaml:"key_id,omitempty"`
Emergency bool `json:"emergency" yaml:"emergency"`
// Individual mirrors trunking.Grant.Individual: the GroupID is a
// unit-to-unit / telephone / data destination, not a talkgroup. Hunt
// skips these when accumulating the talkgroup list.
Individual bool `json:"individual,omitempty" yaml:"individual,omitempty"`
}
GrantRecord mirrors trunking.Grant, flattened with the stream-time offset at which it was observed.
type IQPoint ¶ added in v0.3.3
IQPoint is one complex sample on the wire. Float32 keeps the captured stream compact; web consumers render it directly without conversion. The JSON tags match internal/api's IQPoint (the live Constellation panel) so the offline siglab taps and the live diag stream share a wire shape.
type IQTaps ¶ added in v0.3.3
type IQTaps struct {
// DecimatedIQ is the channelized IQ sampled every Stride samples.
DecimatedIQ []IQPoint `json:"decimated_iq" yaml:"decimated_iq"`
// SoftSamples is the pre-slicer soft-sample stream (deep P25 path);
// empty for protocols/paths that do not emit soft samples.
SoftSamples []float32 `json:"soft_samples" yaml:"soft_samples"`
// DecimatedRateHz is the effective sample rate of DecimatedIQ
// (PipelineRate / Stride).
DecimatedRateHz float64 `json:"decimated_rate_hz" yaml:"decimated_rate_hz"`
// Stride is the input-to-output downsample ratio applied to the
// channelized stream.
Stride int `json:"stride" yaml:"stride"`
// SymbolDibits is the recovered-symbol decision stream (values
// 0..SymbolCardinality-1), decimated to the same cap as the IQ. It
// backs the offline Symbol-scope viz (the OP25-style time series).
// Empty for protocols/paths that buffer no symbols.
SymbolDibits []uint8 `json:"symbol_dibits,omitempty" yaml:"symbol_dibits,omitempty"`
// SymbolSoft is the pre-slicer soft waveform aligned index-for-index
// with SymbolDibits (deep P25 C4FM path). Empty when the demod path
// emits no soft samples (e.g. CQPSK); the viz then shows the dibit
// rows only.
SymbolSoft []float32 `json:"symbol_soft,omitempty" yaml:"symbol_soft,omitempty"`
// SymbolCardinality is 4 for the dibit (4-level) protocols, 2 for the
// bit (2-level) protocols.
SymbolCardinality int `json:"symbol_cardinality,omitempty" yaml:"symbol_cardinality,omitempty"`
// DiffPhase is the per-symbol differential phase (radians, in (-π, π]) of
// the complex constellation — angle(z[i]·conj(z[i-1])) — decimated to the
// same cap as the IQ. It is the rotation signal that backs the offline
// rotation-tracker viz: a π/4-DQPSK control channel clusters these around
// ±π/4 and ±3π/4. Populated only on the CQPSK / π4-DQPSK path (the deep
// constellation buffer); empty for C4FM and the 2-level paths.
DiffPhase []float32 `json:"diff_phase,omitempty" yaml:"diff_phase,omitempty"`
}
IQTaps is the optional decimated signal capture attached to a Result when Config.CaptureIQ is set. It carries a stride-decimated copy of the post-DDC (channelized) IQ — what the constellation, spectrogram and PSD views render — plus the pre-slicer soft samples (deep P25 C4FM path) the eye diagram folds. Both slices are independently hard-capped at Config.CaptureIQMaxPoints, so the taps never grow without bound and never ship full-rate IQ.
It is deliberately heavy (potentially MBs), so the API/export layer strips it from the Result before serializing the summary or running the CSV/JSON exporters; it is retrieved separately (GET .../iq) when a view needs it.
type IdentifyConfig ¶
type IdentifyConfig struct {
SampleRateHz float64
Format SampleFormat
AutoTune bool
Conjugate bool
IQCorrect bool
// MaxSamples caps the input samples each candidate processes (0 ⇒ whole
// capture). Set it to scan a fast prefix when identifying.
MaxSamples int64
// Candidates restricts the protocols tried. nil ⇒ every registered
// protocol whose channel rate fits the capture sample rate.
Candidates []trunking.Protocol
Log *slog.Logger
}
IdentifyConfig configures a signal-identification scan over a capture.
type IdentifyResult ¶
type IdentifyResult struct {
Source string `json:"source"`
SampleRateHz float64 `json:"sample_rate_hz"`
Winner string `json:"winner"`
Confidence float64 `json:"confidence"`
Inconclusive bool `json:"inconclusive"`
// TuneHz is the carrier offset the winning candidate locked at — the one
// the decode should reuse (under -auto-tune this may be an off-centre,
// non-dominant carrier, not the band's loudest). 0 when no shift was used.
TuneHz float64 `json:"tune_hz"`
Candidates []CandidateScore `json:"candidates"`
// Blind / ReferenceMatches are the offline-signal-ID fallback, populated
// only when the decode-based identification is Inconclusive: the blind
// symbol-rate/modulation estimate and the reference-database candidates it
// ranks (so an undecodable carrier is still named a best guess).
Blind *blind.BlindEstimate `json:"blind,omitempty"`
ReferenceMatches []sigref.Match `json:"reference_matches,omitempty"`
}
IdentifyResult ranks the candidates and names the most likely protocol.
func Identify ¶
func Identify(path string, cfg IdentifyConfig) (*IdentifyResult, error)
Identify scans path against candidate protocols and returns a ranked result. Each candidate is run through the engine — over a bounded prefix when MaxSamples is set — and scored on lock + sync-landscape evidence + FEC pass rate. The winner's run result is retained for WinnerResult.
It is a thin wrapper over IdentifyReader: the file is opened once and each candidate seeks back to the start, so the capture is read from disk a single time rather than re-opened per protocol.
func IdentifyIQ ¶ added in v0.3.5
func IdentifyIQ(iq []complex64, source string, cfg IdentifyConfig) (*IdentifyResult, error)
IdentifyIQ identifies an in-memory IQ buffer without any disk round-trip. It encodes the samples once in cfg.Format and delegates to IdentifyReader. This is the path the live hunt sweeper uses: capture a short slice off the SDR at a candidate frequency, then identify it in memory.
func IdentifyReader ¶ added in v0.3.5
func IdentifyReader(r io.ReadSeeker, source string, cfg IdentifyConfig) (*IdentifyResult, error)
IdentifyReader scans the capture read from r against candidate protocols and returns a ranked result. r is read once per candidate and rewound between candidates via Seek, so the underlying bytes are materialized a single time (an *os.File or *bytes.Reader both satisfy io.ReadSeeker, and the seek keeps AutoTune working). It is the shared core behind Identify and IdentifyIQ.
func (*IdentifyResult) WinnerProtocol ¶
func (r *IdentifyResult) WinnerProtocol() (trunking.Protocol, error)
WinnerProtocol parses the winning protocol name back to a trunking.Protocol.
func (*IdentifyResult) WinnerResult ¶
func (r *IdentifyResult) WinnerResult() *Result
WinnerResult returns the run Result for the winning candidate (the prefix scan), or nil. Callers wanting a full-capture analysis should re-run siglab.Run with MaxSamples=0 for the winning protocol.
type LockInfo ¶
type LockInfo struct {
FrequencyHz uint32 `json:"frequency_hz" yaml:"frequency_hz"`
Fields map[string]any `json:"fields" yaml:"fields"`
}
LockInfo is the flattened, protocol-agnostic view of a KindCCLocked payload: the frequency every LockState carries, plus a Fields map of the remaining protocol-specific fields (NAC/DUID, ColorCode/SystemID, MCC/MNC, RAN, …) so one struct represents all 13 protocols' distinct LockState types.
type Metadata ¶
type Metadata struct {
// Protocol is the CLI/config protocol name (p25, p25-phase2, dmr,
// dmr-tier2, nxdn, dpmr, edacs, motorola, ltr, mpt1327, tetra, ysf,
// dstar; the replay aliases p25p1/dmr-tier3 are also accepted).
Protocol string `json:"protocol" yaml:"protocol"`
// Source / ToolCrossCheck are free-text provenance (where the capture
// came from, which reference receiver it was validated against).
Source string `json:"source,omitempty" yaml:"source,omitempty"`
ToolCrossCheck string `json:"tool_cross_check,omitempty" yaml:"tool_cross_check,omitempty"`
// SampleRateHz is required — a raw cfile/u8 carries no rate of its own.
SampleRateHz float64 `json:"sample_rate_hz" yaml:"sample_rate_hz"`
// CenterFreqHz is informational (the capture's nominal centre).
CenterFreqHz uint32 `json:"center_freq_hz,omitempty" yaml:"center_freq_hz,omitempty"`
// Format is the on-disk encoding (u8|f32); empty defaults to u8.
Format string `json:"format,omitempty" yaml:"format,omitempty"`
// TuneHz / AutoTune / Conjugate / IQCorrect mirror the engine knobs for
// captures that need them.
TuneHz float64 `json:"tune_hz,omitempty" yaml:"tune_hz,omitempty"`
AutoTune bool `json:"auto_tune,omitempty" yaml:"auto_tune,omitempty"`
Conjugate bool `json:"conjugate,omitempty" yaml:"conjugate,omitempty"`
IQCorrect bool `json:"iq_correct,omitempty" yaml:"iq_correct,omitempty"`
// System carries per-protocol decoder knobs by their YAML config-key
// names (e.g. "tetra_colour_code": "1", "p25_phase1_demod_mode":
// "cqpsk"), applied onto the trunking.System the engine drives.
System map[string]string `json:"system,omitempty" yaml:"system,omitempty"`
// Expected is the acceptance contract graded against the decode.
Expected Acceptance `json:"expected" yaml:"expected"`
}
Metadata is the sidecar that accompanies a capture (real-air or synthesized) and describes how to decode it plus what a correct decode must produce. It generalizes the ad-hoc per-test loaders and the acceptance-criteria documented in samples/README.md into one schema the `test` harness consumes. It loads from JSON or YAML.
func LoadMetadata ¶
LoadMetadata reads a Metadata document from path (JSON or YAML, chosen by extension; .json → JSON, everything else → YAML).
func Synthesize ¶
func Synthesize(opts SynthOptions) ([]complex64, *Metadata, error)
Synthesize builds a known-good (optionally impaired) capture for a protocol and returns the IQ plus the Metadata describing how to decode and grade it. Returns an error when no synthesis fixture is registered for the protocol (see Fixtures for the supported set).
type NIDDecode ¶
type NIDDecode struct {
Pos int `json:"pos" yaml:"pos"`
Errs int `json:"errs" yaml:"errs"`
NAC uint16 `json:"nac" yaml:"nac"`
DUID uint8 `json:"duid" yaml:"duid"`
OK bool `json:"ok" yaml:"ok"`
}
NIDDecode is one NID BCH decode attempt at an FSW hit.
type NeighborRef ¶ added in v0.3.5
type NeighborRef = trunking.TopoNeighborRef
NeighborRef is an adjacent site. See trunking.TopoNeighborRef.
type P25P1Detail ¶
type P25P1Detail struct {
DibitsBuffered int `json:"dibits_buffered" yaml:"dibits_buffered"`
DibitHistogram [4]int64 `json:"dibit_histogram" yaml:"dibit_histogram"`
Rotations [4]RotationStat `json:"rotations" yaml:"rotations"`
WinningRotation int `json:"winning_rotation" yaml:"winning_rotation"`
WinningHits int `json:"winning_hits" yaml:"winning_hits"`
// NIDDecodes are the NAC/DUID results from BCH-decoding the NID at the
// cleanest (distance-0) FSW hits under the winning rotation. A mix of
// successes and failures points at signal quality; zero successes
// despite perfect FSW alignment points at framing.
NIDDecodes []NIDDecode `json:"nid_decodes" yaml:"nid_decodes"`
// CCStats is the per-frame NID/TSBK outcome breakdown the control channel
// accumulates (the replay EOF summary's "nid trusted/marginal/…" lines).
// Non-nil only on the deep path.
CCStats *CCStatsBreakdown `json:"cc_stats,omitempty" yaml:"cc_stats,omitempty"`
// SoftEye is the pre-slicer soft-sample analysis (DC, per-rail
// distribution, true-symbol outer-rail eye + mechanism verdict). Non-nil
// only when soft samples were captured (the deep C4FM path).
SoftEye *SoftEye `json:"soft_eye,omitempty" yaml:"soft_eye,omitempty"`
// ReceiverStates is the per-(stream-)second receiver-state series the
// deep path captures when Config.CollectReceiverState is set.
ReceiverStates []ReceiverState `json:"receiver_states,omitempty" yaml:"receiver_states,omitempty"`
}
P25P1Detail is the P25-Phase-1-specific demod deep-dive, surfaced in Result.P25P1 when the protocol is P25 Phase 1 and CollectIQDiag is set. It reuses the dibit-domain analysis the historical iqdiag report pioneered (issue #275): a per-rotation Frame-Sync-Word correlation landscape that tells whether the demod produces canonical dibits at all and which rotation aligns the stream, plus NID decode attempts at the cleanest FSW hits. It operates purely on the recovered dibit stream (no soft samples), so it is available through the generic SymbolTap for every P25 P1 capture.
type PDURecord ¶ added in v0.5.8
type PDURecord struct {
Seq int `json:"seq" yaml:"seq"`
OffsetSec float64 `json:"offset_sec" yaml:"offset_sec"`
Protocol string `json:"protocol" yaml:"protocol"`
Opcode uint8 `json:"opcode" yaml:"opcode"`
OpcodeName string `json:"opcode_name" yaml:"opcode_name"`
MFID uint8 `json:"mfid,omitempty" yaml:"mfid,omitempty"`
NAC uint16 `json:"nac,omitempty" yaml:"nac,omitempty"`
SourceID uint32 `json:"source_id,omitempty" yaml:"source_id,omitempty"`
DestID uint32 `json:"dest_id,omitempty" yaml:"dest_id,omitempty"`
Talkgroup uint32 `json:"talkgroup,omitempty" yaml:"talkgroup,omitempty"`
Fields map[string]any `json:"fields,omitempty" yaml:"fields,omitempty"`
RawHex string `json:"raw_hex" yaml:"raw_hex"`
CRCOK bool `json:"crc_ok" yaml:"crc_ok"`
FECMetric int `json:"fec_metric" yaml:"fec_metric"`
DibitStart int `json:"dibit_start" yaml:"dibit_start"`
DibitLen int `json:"dibit_len" yaml:"dibit_len"`
}
PDURecord is one decoded (or attempted) data/signaling PDU, surfaced for SigLab's data-over-RF inspector. It is a flattened, export-friendly view of a decoder SignalingBlock with the common entity IDs promoted out of Fields for filtering.
type ProtocolDetail ¶
type ProtocolDetail struct {
Protocol string `json:"protocol" yaml:"protocol"`
SymbolsBuffered int `json:"symbols_buffered" yaml:"symbols_buffered"`
SymbolCardinality int `json:"symbol_cardinality" yaml:"symbol_cardinality"`
SymbolHistogram []int64 `json:"symbol_histogram" yaml:"symbol_histogram"`
SymbolHistogramPct []float64 `json:"symbol_histogram_pct" yaml:"symbol_histogram_pct"`
Sync *SyncLandscape `json:"sync,omitempty" yaml:"sync,omitempty"`
FEC []FECStat `json:"fec,omitempty" yaml:"fec,omitempty"`
Notes string `json:"notes,omitempty" yaml:"notes,omitempty"`
}
ProtocolDetail is the protocol-specific deep dive for every protocol other than P25 Phase 1 (which keeps its richer P25P1Detail with soft eye + receiver state). It is computed from the buffered recovered-symbol stream: a symbol histogram, a sync-correlation landscape against the protocol's own sync word(s), and — where tractable — a per-stage FEC-decode tally. This is the symbol-domain analog of P25's FSW landscape + NID-BCH probe, the most valuable half of a deep dive, available without any receiver changes.
type RailStat ¶
type RailStat struct {
Label string `json:"label" yaml:"label"`
N int `json:"n" yaml:"n"`
Mean float64 `json:"mean" yaml:"mean"`
Std float64 `json:"std" yaml:"std"`
P10 float64 `json:"p10" yaml:"p10"`
P50 float64 `json:"p50" yaml:"p50"`
P90 float64 `json:"p90" yaml:"p90"`
}
RailStat is the distribution of soft samples on one decision rail.
type ReceiverState ¶
type ReceiverState struct {
TimeSec float64 `json:"time_sec" yaml:"time_sec"`
CQPSK bool `json:"cqpsk" yaml:"cqpsk"`
// C4FM path.
AFCHzEst float64 `json:"afc_hz_est" yaml:"afc_hz_est"`
AGCLevel float64 `json:"agc_level" yaml:"agc_level"`
AGCTarget float64 `json:"agc_target" yaml:"agc_target"`
MMMu float64 `json:"mm_mu" yaml:"mm_mu"`
MMSPS float64 `json:"mm_sps" yaml:"mm_sps"`
DDAActive bool `json:"dda_active" yaml:"dda_active"`
SlicerLevels [4]float64 `json:"slicer_levels" yaml:"slicer_levels"`
SlicerThresholds [3]float64 `json:"slicer_thresholds" yaml:"slicer_thresholds"`
// CQPSK path.
CarrierHzEst float64 `json:"carrier_hz_est,omitempty" yaml:"carrier_hz_est,omitempty"`
GardnerMu float64 `json:"gardner_mu,omitempty" yaml:"gardner_mu,omitempty"`
GardnerSPS float64 `json:"gardner_sps,omitempty" yaml:"gardner_sps,omitempty"`
CQPSKAGCGain float64 `json:"cqpsk_agc_gain,omitempty" yaml:"cqpsk_agc_gain,omitempty"`
CMAError float64 `json:"cma_error,omitempty" yaml:"cma_error,omitempty"`
}
ReceiverState is one snapshot of the P25 receiver's internal loops at a point in stream time. C4FM and CQPSK populate disjoint field sets (the other path's fields read zero); CQPSK is true on the CQPSK path.
type Result ¶
type Result struct {
// Provenance.
Source string `json:"source" yaml:"source"`
Protocol string `json:"protocol" yaml:"protocol"`
SampleRateHz float64 `json:"sample_rate_hz" yaml:"sample_rate_hz"`
PipelineRateHz float64 `json:"pipeline_rate_hz" yaml:"pipeline_rate_hz"`
TuneHz float64 `json:"tune_hz" yaml:"tune_hz"`
DurationSec float64 `json:"duration_sec" yaml:"duration_sec"`
// Throughput.
TotalSamples int64 `json:"total_samples" yaml:"total_samples"`
Symbols int64 `json:"symbols" yaml:"symbols"`
EffectiveBaud float64 `json:"effective_baud" yaml:"effective_baud"`
ExpectedBaud float64 `json:"expected_baud" yaml:"expected_baud"`
BaudDeviationPct float64 `json:"baud_deviation_pct" yaml:"baud_deviation_pct"`
// Lock.
Locked bool `json:"locked" yaml:"locked"`
LockLatencySec float64 `json:"lock_latency_sec" yaml:"lock_latency_sec"`
Lock *LockInfo `json:"lock,omitempty" yaml:"lock,omitempty"`
// Grants + events.
Grants []GrantRecord `json:"grants" yaml:"grants"`
Events []EventRecord `json:"events" yaml:"events"`
EventCounts map[string]int `json:"event_counts" yaml:"event_counts"`
DecodeErrors map[string]int `json:"decode_errors" yaml:"decode_errors"`
// Analysis (nil unless CollectIQDiag).
Signal *SignalQuality `json:"signal,omitempty" yaml:"signal,omitempty"`
// Detail is the protocol-specific deep dive (a *P25P1Detail, *DMRDetail,
// *NXDNDetail, …) populated when CollectIQDiag is set and a per-protocol
// detail builder exists. Consumers type-switch on it (keyed by Protocol).
Detail any `json:"detail,omitempty" yaml:"detail,omitempty"`
// Topology is the protocol-neutral system topology accumulated from the
// control channel during the run — identity (WACN/SYSID/RFSS/Site or the
// per-protocol equivalent), neighbor sites, and band plan. It is the only
// path by which a caller learns WACN/SYSID/RFSS/Site, which never appear in
// event payloads (they live in the decoder's network model). nil when the
// protocol's pipeline does not implement TopologyProvider.
Topology *TopologySnapshot `json:"topology,omitempty" yaml:"topology,omitempty"`
// Verdict (nil unless Config.Acceptance supplied).
Verdict *Verdict `json:"verdict,omitempty" yaml:"verdict,omitempty"`
// NoLockReason is a human-readable classification of why the control
// channel did not lock, populated only when !Locked. It turns a bare "did
// not lock" into a named cause — SNR-limited capture / frame sync not
// aligned / not the control channel / no signal — from the demod and
// frame-decode metrics already gathered, so a capture-quality failure is
// not misread as a tuning or AGC bug (issues #764 / #771).
NoLockReason string `json:"no_lock_reason,omitempty" yaml:"no_lock_reason,omitempty"`
// IQTaps is the optional decimated signal capture (channelized IQ +
// pre-slicer soft samples) populated when Config.CaptureIQ is set. It
// backs the web visualization views (constellation, spectrogram, PSD,
// eye diagram). It can be large, so the API/export layer strips it
// before serializing the summary or running the exporters and serves
// it from a dedicated endpoint instead.
IQTaps *IQTaps `json:"iq_taps,omitempty" yaml:"iq_taps,omitempty"`
// PDUs is the optional per-signaling-block (TSBK) dissection populated when
// Config.CollectPDUs is set — the data-over-RF inspector feed. Capped at
// pduMaxRecords; PDUsTruncated marks an overflow. P25 Phase 1 only for now.
PDUs []PDURecord `json:"pdus,omitempty" yaml:"pdus,omitempty"`
PDUsTruncated bool `json:"pdus_truncated,omitempty" yaml:"pdus_truncated,omitempty"`
}
Result is the protocol-agnostic structured outcome of one engine run. It is the single model every exporter (JSON / JSONL / YAML / CSV) and the acceptance harness render — populated identically for all 13 protocols.
func Run ¶
Run decodes the capture at path through the production pipeline for cfg.Protocol and returns the structured Result. It is the batch entry point behind the replay/analyze/test subcommands.
func RunAutoTuneMulti ¶ added in v0.4.3
RunAutoTuneMulti opens the capture at path and decodes cfg.Protocol against each detected carrier candidate, returning the strongest lock (see RunReaderAutoTuneMulti). It is the file-path entry the replay/analyze subcommands use for multi-candidate -auto-tune.
func RunReader ¶ added in v0.3.3
RunReader decodes the capture read from r through the production pipeline for cfg.Protocol and returns the structured Result. source is recorded as the Result's provenance (its base name). It is the in-memory sibling of Run: a synthesized signal, an uploaded HTTP body, or any io.Reader can be analyzed without a disk round-trip. AutoTune requires r to also satisfy io.ReadSeeker (an *os.File or *bytes.Reader does).
func RunReaderAutoTuneMulti ¶ added in v0.4.3
func RunReaderAutoTuneMulti(r io.ReadSeeker, source string, cfg Config, maxCandidates int) (*Result, error)
RunReaderAutoTuneMulti decodes cfg.Protocol against each detected carrier candidate (each tuned to a fixed offset, resolved here rather than re-estimated) and returns the Result with the strongest lock. It is the multi-candidate counterpart to a single -auto-tune run: when the control channel is off-centre AND not the loudest carrier in a wideband capture, the single dominant-carrier estimate misses it; this tries the ranked candidates strongest-first and stops at the first that locks (or returns the most promising failure). r must be an io.ReadSeeker; cfg.AutoTune is implied and cfg.TuneHz is ignored. maxCandidates ≤ 0 uses the package default.
func RunReaderMonitor ¶ added in v0.4.7
func RunReaderMonitor(r io.Reader, source string, cfg Config, tick time.Duration, onTick func(*TopologySnapshot, float64) bool) (*Result, error)
RunReaderMonitor decodes a (typically live, effectively unbounded) stream, invoking onTick every tick of stream time with the current accumulated topology so the caller can converge-and-stop a long control-channel dwell. It is the streaming sibling of RunReader: memory stays bounded (chunked reads, nothing is buffered whole), and the run ends when onTick returns true, the reader hits EOF, or a read errors. The carrier must already be tuned via cfg.TuneHz — auto-tune (which needs to seek) is not supported.
func RunReaderStream ¶ added in v0.3.3
func RunReaderStream(r io.Reader, source string, cfg Config, onEvent func(EventRecord)) (*Result, error)
RunReaderStream is RunReader with a live per-event sink (see RunStream). It is the single decode path RunStream and the in-memory callers share.
func RunStream ¶
func RunStream(path string, cfg Config, onEvent func(EventRecord)) (*Result, error)
RunStream is Run with a live per-event sink: onEvent (when non-nil) is called for every captured EventRecord as it is observed, in stream order. It backs the JSONL exporter and the TUI's live event feed. The full Result is still returned at EOF.
type RotationStat ¶
type RotationStat struct {
BestDist int `json:"best_dist" yaml:"best_dist"`
BestPos int `json:"best_pos" yaml:"best_pos"`
Hits int `json:"hits" yaml:"hits"` // positions with distance ≤ tolerance
}
RotationStat summarizes the FSW Hamming-distance landscape for one of the four cyclic dibit rotations.
type SampleDecoder ¶
SampleDecoder converts a byte chunk to complex64 IQ in-place into out.
type SampleFormat ¶
type SampleFormat int
SampleFormat is the on-disk IQ encoding of a capture file.
const ( // FormatU8 is rtl_sdr's 8-bit unsigned interleaved IQ. FormatU8 SampleFormat = iota // FormatF32 is GNU Radio's interleaved little-endian float32 cfile. FormatF32 // FormatWAV is a two-channel 16-bit signed PCM RIFF/WAVE baseband // recording (I in channel 1, Q in channel 2) — the layout GopherTrunk's // own IQWriter and SDRtrunk/SDR++ write. The sample rate is read from the // WAV header, overriding -sample-rate, and the 44-byte header is skipped // before decoding. FormatWAV )
func ParseSampleFormat ¶
func ParseSampleFormat(s string) (SampleFormat, error)
ParseSampleFormat maps a -format flag value to a SampleFormat. It accepts the same spellings the replay subcommand always has (u8, f32, plus the float32/cfile aliases) so existing muscle memory keeps working, plus wav (aka sw16) for two-channel 16-bit baseband recordings.
func (SampleFormat) Decoder ¶
func (f SampleFormat) Decoder() (SampleDecoder, int)
Decoder returns the sample decoder + bytes-per-IQ-pair for the format. This is the shared replacement for replay.go's pickSampleDecoder / decodeU8Replay / decodeF32Replay so the replay subcommand and the siglab engine read captures through one implementation.
func (SampleFormat) String ¶
func (f SampleFormat) String() string
String renders the format as the flag value operators type.
type SignalQuality ¶
type SignalQuality struct {
// SymbolCardinality is 4 for the dibit (4-level) protocols and 2 for
// the bit (2-level) protocols.
SymbolCardinality int `json:"symbol_cardinality" yaml:"symbol_cardinality"`
// SymbolHistogram counts recovered symbols per value (len ==
// SymbolCardinality). A clean 4-level C4FM control channel sits near
// 25% per bin; a near-empty bin means the slicer collapsed, a single
// dominant bin means the signal is below the slicer thresholds.
SymbolHistogram []int64 `json:"symbol_histogram" yaml:"symbol_histogram"`
// SymbolHistogramPct is SymbolHistogram normalised to percentages.
SymbolHistogramPct []float64 `json:"symbol_histogram_pct" yaml:"symbol_histogram_pct"`
// Raw (pre-DDC) front-end I/Q imbalance. A clean front-end is balanced
// (≈0 dB gain, ≈0° phase) with image rejection ≳ 40 dB. Populated only
// when raw IQ was observed.
IQGainImbalanceDB float64 `json:"iq_gain_imbalance_db" yaml:"iq_gain_imbalance_db"`
IQPhaseImbalanceDeg float64 `json:"iq_phase_imbalance_deg" yaml:"iq_phase_imbalance_deg"`
IQImageRejectionDB float64 `json:"iq_image_rejection_db" yaml:"iq_image_rejection_db"`
IQObserved bool `json:"iq_observed" yaml:"iq_observed"`
// RMSPowerDBFS is the raw-capture RMS power in dBFS (20·log10 of the RMS
// amplitude; 0 dBFS = a full-scale signal). It is the capture-level power
// figure the `spectrum` CLI reports; populated only when raw IQ was observed.
RMSPowerDBFS float64 `json:"rms_power_dbfs" yaml:"rms_power_dbfs"`
// DecodeErrorRate is decode-error events per recovered 1000 symbols — a
// protocol-neutral proxy for FEC stress.
DecodeErrorRate float64 `json:"decode_error_rate_per_ksym" yaml:"decode_error_rate_per_ksym"`
// Demod is the demodulator-quality measurement (EVM + estimated SNR)
// derived from the per-symbol soft samples on the P25 deep path. Nil for
// protocols / runs that do not surface the soft stream. It is what turns
// "the audio sounds bad" into "the eye is N% open / the demod is M dB from
// the noise floor."
Demod *DemodMetrics `json:"demod,omitempty" yaml:"demod,omitempty"`
}
SignalQuality is the protocol-agnostic demod-quality summary the analyzer produces when Config.CollectIQDiag is set. It generalizes the parts of the historical P25-only iqdiag report that apply to every protocol: the recovered-symbol distribution (which exposes a collapsed or mis-calibrated slicer) and the raw front-end I/Q imbalance (the leading cause of an asymmetric eye). The deeper P25-specific FSW/NID landscape lives in P25P1Detail.
type SoftEye ¶
type SoftEye struct {
SoftSamples int `json:"soft_samples" yaml:"soft_samples"`
SignedMeanDC float64 `json:"signed_mean_dc" yaml:"signed_mean_dc"`
StdDev float64 `json:"std_dev" yaml:"std_dev"`
MeanAbs float64 `json:"mean_abs" yaml:"mean_abs"`
MaxAbs float64 `json:"max_abs" yaml:"max_abs"`
// MagnitudeHistogram bins |soft| into 10 deciles of MaxAbs — bimodal ⇒ a
// clean 4-level eye, unimodal-saturated ⇒ everything piled near ±max.
MagnitudeHistogram [10]int64 `json:"magnitude_histogram" yaml:"magnitude_histogram"`
// PerDecidedSymbol is the soft distribution grouped by the slicer's
// decision (rails -3,-1,+1,+3). Clean ⇒ symmetric, |outer| ≈ 3·|inner|.
PerDecidedSymbol []RailStat `json:"per_decided_symbol" yaml:"per_decided_symbol"`
// TrueOuterRail is the slicer-independent outer-rail (+3,-3) distribution
// from the known FSW symbols.
TrueOuterRail []RailStat `json:"true_outer_rail" yaml:"true_outer_rail"`
// OuterSpread splits each true outer rail into steady vs post-transition
// standard deviation — the ISI / timing discriminator.
OuterSpread []SpreadStat `json:"outer_spread" yaml:"outer_spread"`
// Verdict points at the dominant outer-spread mechanism.
Verdict string `json:"verdict" yaml:"verdict"`
}
SoftEye is the structured pre-slicer soft-sample analysis for the P25 deep C4FM path — the part of the historical iqdiag report that worked on the matched-filter output. It exposes the eye-skew the dibit histogram folds away (issue #402): the DC offset, the per-decided-symbol rail distribution, and the slicer-independent true-outer-rail eye with a mechanism verdict.
type SpreadStat ¶
type SpreadStat struct {
Label string `json:"label" yaml:"label"`
SteadyStd float64 `json:"steady_std" yaml:"steady_std"`
SteadyN int `json:"steady_n" yaml:"steady_n"`
TransStd float64 `json:"trans_std" yaml:"trans_std"`
TransN int `json:"trans_n" yaml:"trans_n"`
Ratio float64 `json:"ratio" yaml:"ratio"`
}
SpreadStat compares steady-state vs post-transition spread on one outer rail.
type SurveySpectrum ¶ added in v0.4.3
type SurveySpectrum struct {
CenterHz uint32 `json:"center_hz" yaml:"center_hz"`
SampleRateHz uint32 `json:"sample_rate_hz" yaml:"sample_rate_hz"`
Bins []float32 `json:"bins" yaml:"bins"`
}
SurveySpectrum is the Welch-averaged power spectrum of the whole band, in the same FFT-shifted dBFS layout as spectrum.Frame: Bins[0] is the lowest frequency (CenterHz - SampleRateHz/2) and successive bins step by SampleRateHz/len(Bins). Populated only when WidebandConfig.CollectSpectrum is set; it lets a UI plot the band with the detected carriers marked.
type SyncLandscape ¶
type SyncLandscape struct {
SyncLen int `json:"sync_len" yaml:"sync_len"`
Tolerance int `json:"tolerance" yaml:"tolerance"`
Stats []SyncStat `json:"stats" yaml:"stats"`
WinnerVariant string `json:"winner_variant" yaml:"winner_variant"`
WinnerRotation int `json:"winner_rotation" yaml:"winner_rotation"`
WinnerHits int `json:"winner_hits" yaml:"winner_hits"`
// ModalSpacing is the most common symbol delta between consecutive clean
// hits under the winner — a real control channel emits sync at a fixed
// frame interval, so a single dominant spacing means genuine frames while
// scattered spacings mean false positives.
ModalSpacing int `json:"modal_spacing" yaml:"modal_spacing"`
// contains filtered or unexported fields
}
SyncLandscape summarizes sync-word correlation across a recovered-symbol stream — the generalization of P25's FSW-correlation landscape. It answers "does the demod produce canonical symbols, which sync/polarity aligns the stream, and at what cadence" for any protocol.
type SyncStat ¶
type SyncStat struct {
Variant string `json:"variant" yaml:"variant"`
Rotation int `json:"rotation" yaml:"rotation"`
BestDist int `json:"best_dist" yaml:"best_dist"`
BestPos int `json:"best_pos" yaml:"best_pos"`
Hits int `json:"hits" yaml:"hits"`
}
SyncStat is the best correlation of one (variant, rotation) against the recovered-symbol stream. rotation is the cyclic symbol offset applied to the stream before comparison: for 4-level streams rotation 2 is the discriminator-polarity flip and 1/3 the I/Q-swap rotations; for 2-level streams rotation 1 is bit inversion.
type SyncVariant ¶
type SyncVariant struct {
Name string `json:"name" yaml:"name"`
Pattern []uint8 `json:"-" yaml:"-"`
}
SyncVariant is one named candidate sync pattern (e.g. one of DMR's 9 ETSI sync words, or a protocol's single FSW), as a slice of symbols (dibits in 0..3, or bits in 0..1).
type SynthOptions ¶
type SynthOptions struct {
Protocol trunking.Protocol
Format SampleFormat
// Impairments applied to the ideal IQ (SNR, carrier offset, DC spike,
// I/Q imbalance, multipath). The zero value is a clean capture.
Impairments demod.Impairments
// Modulation optionally overrides the fixture's default modulator. Empty
// uses the per-protocol default. For P25 Phase 1, "lsm" (or "cqpsk")
// selects the linear π/4-DQPSK (LSM) waveform instead of C4FM, so a
// capture can exercise the linear CQPSK demod path (issue #492).
Modulation string
}
SynthOptions configures a synthesis run: which protocol to build, the front-end impairments to overlay on the ideal modulator output, and the on-disk capture format.
type SystemRollup ¶ added in v0.4.3
type SystemRollup struct {
Protocol string `json:"protocol" yaml:"protocol"`
Tier3 bool `json:"tier3" yaml:"tier3"`
SystemID uint32 `json:"system_id,omitempty" yaml:"system_id,omitempty"`
ColorCode uint8 `json:"color_code,omitempty" yaml:"color_code,omitempty"`
ControlCount int `json:"control_count" yaml:"control_count"`
VoiceCount int `json:"voice_count" yaml:"voice_count"`
LoHz uint32 `json:"lo_hz" yaml:"lo_hz"`
HiHz uint32 `json:"hi_hz" yaml:"hi_hz"`
CarrierIdx []int `json:"carrier_idx" yaml:"carrier_idx"`
Verdict string `json:"verdict" yaml:"verdict"`
}
SystemRollup clusters carriers into one trunked-system verdict.
type TopologyProvider ¶ added in v0.3.5
type TopologyProvider = trunking.TopologyProvider
TopologyProvider is the optional pipeline hook. See trunking.TopologyProvider.
type TopologySnapshot ¶ added in v0.3.5
type TopologySnapshot = trunking.TopologySnapshot
TopologySnapshot is the accumulated system topology attached to a Result.Topology. See trunking.TopologySnapshot.
type Verdict ¶
type Verdict struct {
Pass bool `json:"pass" yaml:"pass"`
Failures []string `json:"failures" yaml:"failures"`
Checks []string `json:"checks" yaml:"checks"`
}
Verdict is the pass/fail outcome of grading a Result against an Acceptance.
type WidebandConfig ¶ added in v0.4.3
type WidebandConfig struct {
SampleRateHz float64 // wideband input rate (required)
CenterHz uint32 // absolute capture center (0 ⇒ offsets only)
Format SampleFormat //
ChannelRateHz float64 // per-carrier target; 0 ⇒ 48 kHz
FFTSize int // spectrum FFT; 0 ⇒ 4096
GuardFrac float64 // band-edge guard; 0 ⇒ 0.05
PeakThresholdDb float32 // 0 ⇒ 10
MinSpacingHz uint32 // 0 ⇒ 6.25 kHz
TunerStrategy string // "" | "auto" | "ddc" | "polyphase"
Candidates []trunking.Protocol // per-carrier identify set; nil ⇒ all
IdentifyMaxSamples int64 // per-carrier identify cap; 0 ⇒ whole carrier
MaxCarriers int // defensive cap on taps; 0 ⇒ no cap
Conjugate bool //
IQCorrect bool //
// CollectSpectrum retains the Welch-averaged power spectrum on the result
// (WidebandResult.Spectrum) so callers can plot the band with the detected
// carriers overlaid. Off by default to keep the offline hunt path lean.
CollectSpectrum bool //
Log *slog.Logger //
}
WidebandConfig configures a wideband multi-carrier survey: the whole-capture sample rate + format, the channel rate each carrier is decimated to, and the per-carrier identify knobs.
type WidebandResult ¶ added in v0.4.3
type WidebandResult struct {
Source string `json:"source" yaml:"source"`
SampleRateHz float64 `json:"sample_rate_hz" yaml:"sample_rate_hz"`
CenterHz uint32 `json:"center_hz" yaml:"center_hz"`
ChannelRateHz float64 `json:"channel_rate_hz" yaml:"channel_rate_hz"`
Strategy string `json:"strategy" yaml:"strategy"`
// Spectrum is the averaged band power spectrum for display; nil unless
// WidebandConfig.CollectSpectrum was set.
Spectrum *SurveySpectrum `json:"spectrum,omitempty" yaml:"spectrum,omitempty"`
// DetectedCount is the number of spectral carrier peaks before the
// per-carrier identify filter dropped the ones that didn't decode to a
// real protocol (leakage / noise peaks).
DetectedCount int `json:"detected_count" yaml:"detected_count"`
Carriers []CarrierResult `json:"carriers" yaml:"carriers"`
Systems []SystemRollup `json:"systems" yaml:"systems"`
}
WidebandResult is the full survey outcome.
func SurveyWideband ¶ added in v0.4.3
func SurveyWideband(r io.ReadSeeker, source string, cfg WidebandConfig) (*WidebandResult, error)
SurveyWideband decodes the whole capture from r into memory and surveys it.
func SurveyWidebandIQ ¶ added in v0.4.3
func SurveyWidebandIQ(iq []complex64, source string, cfg WidebandConfig) (*WidebandResult, error)
SurveyWidebandIQ surveys an already-decoded wideband buffer: averaged power spectrum → carrier peaks → grid-snap → per-carrier DDC → per-carrier identify → cluster into systems. It is the in-memory core the hunt bridge and tests share.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package sigref is SigLab's offline signal-identification reference database — a small, curated catalog (Artemis / sigidwiki in spirit) that names a carrier by its band, bandwidth, modulation, and symbol rate even when no decoder locks.
|
Package sigref is SigLab's offline signal-identification reference database — a small, curated catalog (Artemis / sigidwiki in spirit) that names a carrier by its band, bandwidth, modulation, and symbol rate even when no decoder locks. |