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 Fixtures() []trunking.Protocol
- func HasFixture(p trunking.Protocol) bool
- func ParseProtocolCLI(s string) (trunking.Protocol, error)
- func SortedDecodeErrors(m map[string]int) []string
- 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 CCStatsBreakdown
- type CandidateScore
- type Config
- type EventRecord
- type FECStat
- type Format
- type GrantRecord
- type IdentifyConfig
- type IdentifyResult
- type LockInfo
- type Metadata
- type NIDDecode
- type P25P1Detail
- type ProtocolDetail
- type RailStat
- type ReceiverState
- type Result
- type RotationStat
- type SampleDecoder
- type SampleFormat
- type SignalQuality
- type SoftEye
- type SpreadStat
- type SyncLandscape
- type SyncStat
- type SyncVariant
- type SynthOptions
- type Verdict
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 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 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"`
}
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 ¶
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 ¶
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 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 ¶
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 ¶
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.