siglab

package
v0.3.2 Latest Latest
Warning

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

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

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func DiscoverMetadata

func DiscoverMetadata(capturePath string) string

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 Fixtures

func Fixtures() []trunking.Protocol

Fixtures returns the protocols with a registered synthesis fixture, in stable order, for the `gen` usage message and protocol picker.

func HasFixture

func HasFixture(p trunking.Protocol) bool

HasFixture reports whether a synthesis fixture is registered for p.

func ParseProtocolCLI

func ParseProtocolCLI(s string) (trunking.Protocol, error)

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

func SortedDecodeErrors(m map[string]int) []string

SortedDecodeErrors returns the decode-error stages in a stable order, for deterministic text/CSV rendering by callers.

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

func WriteMetadata(path string, m *Metadata) error

WriteMetadata writes m to path as JSON (the sidecar `test` auto-discovers).

func WriteResult

func WriteResult(w io.Writer, r *Result, f Format) error

WriteResult serializes r to w in the requested format. FormatTextSummary is not handled here (the subcommands own their text rendering); it returns an error so a miswired caller is caught.

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"`
}

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 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"`
}

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

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

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

func ParseFormat

func ParseFormat(s string) (Format, error)

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"`
	Emergency   bool    `json:"emergency" yaml:"emergency"`
}

GrantRecord mirrors trunking.Grant, flattened with the stream-time offset at which it was observed.

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"`
	Candidates   []CandidateScore `json:"candidates"`
}

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.

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

func LoadMetadata(path string) (*Metadata, error)

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

func (*Metadata) Config

func (m *Metadata) Config(collectIQDiag bool) (Config, error)

Config builds an engine Config from the metadata for the given capture. collectIQDiag is threaded through because the harness wants the analyzer's decode-error rate available to the verdict.

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 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 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"`

	// Verdict (nil unless Config.Acceptance supplied).
	Verdict *Verdict `json:"verdict,omitempty" yaml:"verdict,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

func Run(path string, cfg Config) (*Result, error)

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

type SampleDecoder func(buf []byte, out []complex64)

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
)

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.

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"`

	// 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"`
}

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

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

Jump to

Keyboard shortcuts

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