Documentation
¶
Overview ¶
Package hunt is GopherTrunk's site/system "hunting" layer: it turns the output of the signal-lab decoders into a DiscoveredSystem — a structured map of a trunked radio system observed off the air (or from a capture) — and exports that map to standardized files plus a RadioReference.com submission package.
Why this exists: many states have trunked systems that are undocumented on RadioReference. GopherTrunk can already decode a control channel once you know its frequency and identity; this package closes the loop by accumulating what the decoder observes (system identity, per-site control channels, neighbor sites, talkgroups) into a single model that round-trips straight back into GopherTrunk's import bundle and into other scanners.
The model is intentionally protocol-neutral. Full multi-site topology (WACN/SYSID/RFSS/Site/neighbors) is only available for P25 today; for the other protocols the accumulator records the identity fields the decoder surfaces plus the single observed site and its talkgroups, which is still enough to export and submit.
Index ¶
- Variables
- func Accumulate(dst *DiscoveredSystem, obs Observation)
- func Discover(captures []CaptureInput, cfg DiscoverConfig) (*DiscoveredSystem, []CaptureReport, error)
- func RunLiveHunt(ctx context.Context, opts LiveHuntOptions) (*DiscoveredSystem, []CaptureReport, error)
- func RunLiveSurvey(ctx context.Context, opts LiveHuntOptions) (*SignalSurvey, []CaptureReport, error)
- func RunOfflineSurvey(captures []CaptureInput, opts LiveHuntOptions) (*SignalSurvey, []CaptureReport, error)
- func Write(w io.Writer, sys *DiscoveredSystem, f Format, hints []DuplicateHint) error
- func WriteSurvey(w io.Writer, sv *SignalSurvey, f SurveyFormat) error
- type Acquirer
- type Band
- type BandPlanEntry
- type Candidate
- type CaptureInput
- type CaptureReport
- type DetectedSignal
- type DiscoverConfig
- type DiscoveredChannel
- type DiscoveredSite
- type DiscoveredSystem
- type DiscoveredTalkgroup
- type DuplicateHint
- type FileIQSource
- type Format
- type IQSource
- type LiveHuntOptions
- type LiveHuntPhase
- type LiveHuntProgress
- type Manager
- func (m *Manager) Current() (*DiscoveredSystem, []CaptureReport, bool)
- func (m *Manager) CurrentSurvey() (*SignalSurvey, bool)
- func (m *Manager) Export(w io.Writer, f Format, hints []DuplicateHint) error
- func (m *Manager) ExportRun(id int, w io.Writer, f Format, hints []DuplicateHint) error
- func (m *Manager) ExportSurvey(id int, w io.Writer, f SurveyFormat) error
- func (m *Manager) KnownRun(id int) bool
- func (m *Manager) Run(id int) (*DiscoveredSystem, []CaptureReport, bool)
- func (m *Manager) Start(opts LiveHuntOptions) (int, error)
- func (m *Manager) Status() RunStatus
- func (m *Manager) Stop() bool
- func (m *Manager) SurveyRun(id int) (*SignalSurvey, bool)
- type ManagerOptions
- type NeighborRef
- type Observation
- type OnStep
- type Peak
- type PeakOptions
- type RunState
- type RunStatus
- type SignalSurvey
- type SurveyFormat
- type SweepOptions
- type Sweeper
- type TrunkingRef
Constants ¶
This section is empty.
Variables ¶
var ErrNoSuchRun = errors.New("hunt: no such run id")
ErrNoSuchRun is returned by id-addressed lookups when the run id is unknown or has been evicted from the bounded history.
Functions ¶
func Accumulate ¶
func Accumulate(dst *DiscoveredSystem, obs Observation)
Accumulate folds an Observation into dst, creating sites/talkgroups and merging identity. It de-duplicates control channels by frequency, sites by (RFSS, Site), and talkgroups by decimal id, so re-observing the same control channel is idempotent (apart from bumping talkgroup activity counts).
func Discover ¶
func Discover(captures []CaptureInput, cfg DiscoverConfig) (*DiscoveredSystem, []CaptureReport, error)
Discover folds every capture into a single DiscoveredSystem. Captures whose protocol can't be identified with sufficient confidence (and weren't given an explicit protocol) are skipped, not errored, so a wideband sweep that surfaced non-trunked carriers degrades gracefully. The per-capture reports are always returned, even on a nil error, so the caller can show progress.
func RunLiveHunt ¶
func RunLiveHunt(ctx context.Context, opts LiveHuntOptions) (*DiscoveredSystem, []CaptureReport, error)
RunLiveHunt sweeps (or probes a candidate list) on a live IQSource, then identifies, decodes, and maps each candidate into a DiscoveredSystem. It is the on-air sibling of Discover: the sweep replaces operator-supplied captures, but the per-candidate identify→decode→accumulate body is shared (decodeAndAccumulate). Returns the discovered system, a per-candidate report, and an error only for fatal setup/sweep failures (per-candidate failures are recorded in the reports).
func RunLiveSurvey ¶ added in v0.3.7
func RunLiveSurvey(ctx context.Context, opts LiveHuntOptions) (*SignalSurvey, []CaptureReport, error)
RunLiveSurvey sweeps (or probes a candidate list) like RunLiveHunt, but instead of only mapping trunking control channels it classifies every detected carrier and routes it: trunking carriers fold into the discovered system (the existing identify→decode→accumulate path), paging and analog carriers run their conventional decoders, and the rest are recorded as classified-only. It returns the full SignalSurvey (which embeds the trunking map) plus the per-candidate trunking reports for the shared export tail.
func RunOfflineSurvey ¶ added in v0.3.7
func RunOfflineSurvey(captures []CaptureInput, opts LiveHuntOptions) (*SignalSurvey, []CaptureReport, error)
RunOfflineSurvey classifies and routes a set of capture files without an SDR — the offline sibling of RunLiveSurvey, mirroring how Discover is the offline sibling of RunLiveHunt. Each capture is loaded, treated as one baseband candidate (at its CaptureInput.FrequencyHz), classified, and routed through the same body the live survey uses, so an operator can survey recorded IQ (e.g. a wideband grab) the same way they survey on the air.
func Write ¶
func Write(w io.Writer, sys *DiscoveredSystem, f Format, hints []DuplicateHint) error
Write serializes sys to w in the requested format. RRHints, when non-empty, are rendered into the RR submission package (ignored by the other formats).
func WriteSurvey ¶ added in v0.3.7
func WriteSurvey(w io.Writer, sv *SignalSurvey, f SurveyFormat) error
WriteSurvey serializes sv to w in the requested format.
Types ¶
type Acquirer ¶
type Acquirer func(ctx context.Context, opts LiveHuntOptions) (src IQSource, release func(), err error)
Acquirer obtains an IQSource for one run plus a release callback (resume the control-channel hunter, close the SDR subscription, …). The daemon supplies this so the hunt package stays free of SDR/pool dependencies; release is invoked exactly once when the run ends (success, error, or stop). Acquirer obtains an IQSource for one run plus a release callback. opts carries the run's parameters — notably opts.Serial, so the daemon can honor an operator-requested SDR — and is passed by the Manager when a run starts.
type BandPlanEntry ¶
type BandPlanEntry struct {
ChannelID uint8 `json:"channel_id"`
BaseHz uint64 `json:"base_hz"`
SpacingHz uint32 `json:"spacing_hz"`
BandwidthHz uint32 `json:"bandwidth_hz,omitempty"`
TxOffsetHz int64 `json:"tx_offset_hz,omitempty"`
}
BandPlanEntry is one P25 IDEN_UP-equivalent band-plan mapping.
type Candidate ¶
Candidate is a carrier the sweep found, worth identifying.
func SortedCandidates ¶
SortedCandidates returns candidates sorted by descending SNR (test helper).
type CaptureInput ¶
type CaptureInput struct {
Path string
Format siglab.SampleFormat
SampleRateHz float64
// FrequencyHz is the capture's nominal center frequency (informational;
// the decoded lock frequency is what gets recorded).
FrequencyHz uint32
AutoTune bool
Conjugate bool
IQCorrect bool
// Protocol forces a decoder; trunking.ProtocolUnknown (the zero value)
// triggers auto-identification across every protocol.
Protocol trunking.Protocol
// IdentifyMaxSamples caps the prefix the identifier scans (0 ⇒ a built-in
// default prefix). Only consulted when Protocol is unknown.
IdentifyMaxSamples int64
}
CaptureInput is one IQ capture to fold into a discovery — typically a recording centered on a suspected control channel. The hunt orchestrator identifies the protocol (unless Protocol is set), decodes it, and accumulates the result into the running system map.
type CaptureReport ¶
type CaptureReport struct {
Path string `json:"path"`
Protocol string `json:"protocol"`
Confidence float64 `json:"confidence"`
Locked bool `json:"locked"`
ControlHz uint32 `json:"control_hz,omitempty"`
Talkgroups int `json:"talkgroups"`
Skipped bool `json:"skipped"`
SkipReason string `json:"skip_reason,omitempty"`
Error string `json:"error,omitempty"`
}
CaptureReport records what happened to one capture so the CLI/cockpit can explain the outcome (decoded, skipped as not-trunked, errored).
type DetectedSignal ¶ added in v0.3.7
type DetectedSignal struct {
FreqHz uint32 `json:"freq_hz"`
SNRDb float32 `json:"snr_db"`
OccupiedBwHz uint32 `json:"occupied_bw_hz"`
Class survey.SignalClass `json:"class"`
Confidence float64 `json:"confidence"`
BaudHz float64 `json:"baud_hz,omitempty"`
// Decode summary — set by the router for the carriers it could decode.
Trunking *TrunkingRef `json:"trunking,omitempty"`
Analog *survey.AnalogReport `json:"analog,omitempty"`
Pages []survey.PageRef `json:"pages,omitempty"`
// Features carries the raw classifier measurements for diagnostics.
Features survey.ClassFeatures `json:"features"`
// Error notes a per-carrier failure (tune/capture), leaving Class as-is.
Error string `json:"error,omitempty"`
}
DetectedSignal is one classified (and possibly decoded) carrier in a survey. Exactly one of Trunking / Analog / Pages is populated, per the Class.
type DiscoverConfig ¶
type DiscoverConfig struct {
Name string
State string
County string
Location string
MinConfidence float64 // skip auto-identified captures below this (0 ⇒ 0.40)
Log *slog.Logger
}
DiscoverConfig carries operator metadata and thresholds for a discovery run.
type DiscoveredChannel ¶
type DiscoveredChannel struct {
FrequencyHz uint32 `json:"frequency_hz"`
IsControl bool `json:"is_control"`
Confidence float64 `json:"confidence,omitempty"`
}
DiscoveredChannel is one observed frequency on a site.
type DiscoveredSite ¶
type DiscoveredSite struct {
RFSS uint8 `json:"rfss"`
SiteID uint8 `json:"site_id"`
SiteName string `json:"site_name"`
County string `json:"county,omitempty"`
ControlChannels []DiscoveredChannel `json:"control_channels"`
Secondary []uint32 `json:"secondary,omitempty"`
Neighbors []NeighborRef `json:"neighbors,omitempty"`
}
DiscoveredSite is one RF site of the system, keyed by (RFSS, SiteID).
type DiscoveredSystem ¶
type DiscoveredSystem struct {
// Name is operator-assigned, or synthesized from the identity when blank.
Name string `json:"name"`
// Protocol is the trunking.Protocol.String() spelling (e.g. "p25").
Protocol string `json:"protocol"`
// P25 / generic identity. Zero ⇒ unknown.
WACN uint32 `json:"wacn,omitempty"`
SystemID uint16 `json:"system_id,omitempty"`
NAC uint16 `json:"nac,omitempty"`
// Identity carries the remaining per-protocol identity fields the decoder
// surfaced (DMR ColorCode, NXDN RAN, TETRA MCC/MNC, Motorola/EDACS system
// id, …) keyed by the decoder's field name. It is informational and is
// rendered into the RR package.
Identity map[string]any `json:"identity,omitempty"`
// Geography — operator-supplied; used by the RR duplicate check and the
// submission package.
State string `json:"state,omitempty"`
County string `json:"county,omitempty"`
Location string `json:"location,omitempty"`
Sites []DiscoveredSite `json:"sites"`
Talkgroups []DiscoveredTalkgroup `json:"talkgroups"`
BandPlan []BandPlanEntry `json:"band_plan,omitempty"`
// Confidence is the min protocol-identification confidence across the
// control channels folded into this system (0..1).
Confidence float64 `json:"confidence"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
}
DiscoveredSystem is the accumulated map of one trunked system. It is built up incrementally as captures/observations are folded in (see Accumulator), then handed to the exporters. Its fields are shaped to convert cleanly onto the importer's parsedSystem (see cmd/gophertrunk/hunt_export.go) so a discovery exported as a bundle re-imports without loss.
func (*DiscoveredSystem) DisplayName ¶
func (s *DiscoveredSystem) DisplayName() string
DisplayName returns Name when set, otherwise a synthesized identifier from the protocol + system id so an unnamed discovery still has a stable handle.
func (*DiscoveredSystem) RFSS ¶
func (s *DiscoveredSystem) RFSS() uint64
RFSS returns the RFSS id harvested into the Identity map (0 when unknown).
func (*DiscoveredSystem) SiteNum ¶
func (s *DiscoveredSystem) SiteNum() uint64
SiteNum returns the Site id harvested into the Identity map (0 when unknown).
type DiscoveredTalkgroup ¶
type DiscoveredTalkgroup struct {
Dec uint32 `json:"dec"`
Hex string `json:"hex"`
Encrypted bool `json:"encrypted,omitempty"`
Count int `json:"count"`
FirstSeen time.Time `json:"first_seen"`
}
DiscoveredTalkgroup is one talkgroup observed on the control channel. On a blind discovery only the numeric id and activity are known; the descriptive fields are left blank for the operator (or RR) to fill in.
type DuplicateHint ¶
type DuplicateHint struct {
SID int // RadioReference system id
Name string // existing system name
Reason string // why it matched (WACN+SYSID, overlapping CC, name/county)
Confidence float64 // 0..1
}
DuplicateHint is a possible pre-existing RadioReference system match, surfaced by an optional read-only RR API lookup. It is rendered into the RR submission package so an operator doesn't submit a duplicate.
type FileIQSource ¶
type FileIQSource struct {
// contains filtered or unexported fields
}
FileIQSource is a deterministic IQSource over an in-memory IQ buffer, used by tests. Tune records the requested center (so captured slices can be stamped with an absolute frequency) but does not alter the samples — the buffer is assumed to already be baseband IQ for whatever carrier the test models. Capture serves the buffer cyclically so a sweep with many steps never starves.
func NewFileIQSource ¶
func NewFileIQSource(iq []complex64, rateHz uint32) *FileIQSource
NewFileIQSource builds a FileIQSource over one buffer served cyclically at every tuned center.
func NewMappedIQSource ¶
func NewMappedIQSource(perCenter map[uint32][]complex64, rateHz uint32) *FileIQSource
NewMappedIQSource builds a FileIQSource that serves a distinct buffer per tuned center frequency (and an empty/zero stream for untuned centers), so a test can place a "carrier" at specific frequencies and verify the sweep finds exactly those. rateHz is the common sample rate.
func (*FileIQSource) SampleRateHz ¶
func (f *FileIQSource) SampleRateHz() uint32
func (*FileIQSource) Tune ¶
func (f *FileIQSource) Tune(centerHz uint32) error
func (*FileIQSource) TuneCalls ¶
func (f *FileIQSource) TuneCalls() []uint32
TuneCalls returns the centers Tune was called with, in order (test helper).
type Format ¶
type Format int
Format selects an export encoding for a DiscoveredSystem.
const ( // FormatBundle is GopherTrunk's multi-section CSV import bundle (the exact // reverse of cmd/gophertrunk/import_csv.go parseCSVStream — it round-trips // straight back into config.yaml via `import-pdf -csv`). FormatBundle Format = iota // FormatTrunkRecorder is a trunk-recorder JSON system config stanza. FormatTrunkRecorder // FormatRR is a human-readable RadioReference.com submission package. FormatRR )
func ParseFormat ¶
ParseFormat maps a -formats list value to a Format.
func (Format) FileExtension ¶
FileExtension is the conventional extension for a format's output file.
type IQSource ¶
type IQSource interface {
// Tune retunes the source to centerHz. Subsequent Capture calls return IQ
// centered there.
Tune(centerHz uint32) error
// Capture returns the next n complex samples at the current center, or an
// error (including ctx cancellation). Implementations may return fewer than
// n only at end-of-stream, in which case they return what remains.
Capture(ctx context.Context, n int) ([]complex64, error)
// SampleRateHz is the source's IQ sample rate.
SampleRateHz() uint32
}
IQSource is the tunable IQ provider the live sweeper and hunter drive. It abstracts over a live SDR (broker-backed, wired in the daemon) and a file/synthetic source (tests), so the sweep/identify/accumulate logic is the same offline and on-air.
type LiveHuntOptions ¶
type LiveHuntOptions struct {
Source IQSource
// Bands to sweep. Ignored when Candidates is non-empty.
Bands []Band
// Candidates, when set, are explicit control-channel frequencies to probe
// directly (the sweep is skipped). Mirrors the offline -no-sweep path.
Candidates []uint32
// Protocol forces a decoder; trunking.ProtocolUnknown auto-identifies.
Protocol trunking.Protocol
// Survey selects the signal-survey pipeline (RunLiveSurvey): classify and
// decode every detected carrier, not just trunking control channels. The
// daemon Manager reads this to dispatch the right run.
Survey bool
// ClassifyConfig tunes the survey classifier thresholds. The zero value
// uses survey.DefaultClassifyConfig.
ClassifyConfig survey.ClassifyConfig
// IdentifyMinConfidence, when > 0, skips the (expensive) trunking identify
// for a digital carrier whose classifier confidence is below it — unless it
// is at a paging baud. 0 ⇒ always identify (never drop a possible CC).
IdentifyMinConfidence float64
// ClassifyOnly records the classifier verdict for every carrier and skips
// all decoding (trunking identify, paging, analog tone scan) for a fast
// inventory.
ClassifyOnly bool
// SurveyDeep hands narrowband carriers the blind classifier called analog
// (am/nbfm/wfm ≤ NBFM bandwidth) to the authoritative siglab identify before
// the analog tone scan, so a trunked DMR/TETRA/MPT control channel the blind
// classifier missed is still discovered. Slower (an extra identify per
// borderline carrier); the accurate-survey opt-in for digital-dense bands.
SurveyDeep bool
// Serial requests a specific SDR for the run. Empty ⇒ the daemon
// auto-selects (spare SDR, else borrow the control SDR). Consumed by the
// daemon Acquirer; RunLiveHunt itself ignores it (the IQSource is already
// resolved by the time RunLiveHunt runs).
Serial string
FFTSize int
SweepDwell time.Duration
GuardFrac float64
PeakOpts PeakOptions
// DwellSeconds is how much IQ to capture per candidate for identify+decode.
// 0 ⇒ 3 s.
DwellSeconds float64
// MaxDwellSeconds, when > DwellSeconds, lets a survey candidate extend its
// capture (in DwellSeconds chunks) until carrier activity is seen or this
// ceiling is reached — useful for bursty paging. 0 ⇒ a single fixed dwell.
MaxDwellSeconds float64
MinConfidence float64
AutoTune bool
// SurveyAudioDir, when set, makes a survey write a WAV clip per active
// analog-FM carrier into this directory.
SurveyAudioDir string
// System metadata for the resulting map.
Name, State, County, Location string
OnProgress func(LiveHuntProgress)
// OnSignal, when set, is called once per classified carrier during a survey
// run (RunLiveSurvey), after its decode summary is filled in. The daemon
// uses it to publish per-signal events and to persist decoded pages; the CLI
// leaves it nil. Ignored by RunLiveHunt.
OnSignal func(DetectedSignal)
Log *slog.Logger
}
LiveHuntOptions configure a live hunt.
type LiveHuntPhase ¶
type LiveHuntPhase string
LiveHuntPhase labels the stage a live hunt is in, for progress reporting.
const ( PhaseSweeping LiveHuntPhase = "sweeping" PhaseIdentifying LiveHuntPhase = "identifying" PhaseDone LiveHuntPhase = "done" )
type LiveHuntProgress ¶
type LiveHuntProgress struct {
Phase LiveHuntPhase `json:"phase"`
CenterHz uint32 `json:"center_hz,omitempty"`
CandidateN int `json:"candidate_n,omitempty"`
Candidates int `json:"candidates,omitempty"`
Detail string `json:"detail,omitempty"`
}
LiveHuntProgress is one progress notification emitted during a live hunt.
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager owns the daemon's single live-hunt run: it acquires an SDR, runs the sweep→identify→map pipeline in the background, publishes hunt.* bus events, and holds the latest discovered system for export/commit. Safe for concurrent use by the REST/TUI cockpit.
func NewManager ¶
func NewManager(opts ManagerOptions) (*Manager, error)
NewManager builds a Manager. Acquire is required.
func (*Manager) Current ¶
func (m *Manager) Current() (*DiscoveredSystem, []CaptureReport, bool)
Current returns the latest discovered system and its per-candidate reports, or (nil, nil, false) when no run has produced a map yet. The returned system is the live pointer; callers must not mutate it.
func (*Manager) CurrentSurvey ¶ added in v0.3.7
func (m *Manager) CurrentSurvey() (*SignalSurvey, bool)
CurrentSurvey returns the latest run's signal inventory, or (nil, false) when the latest run was a plain hunt or none has produced one yet.
func (*Manager) Export ¶
Export writes the latest discovered system to w in the given format. Returns an error when no system has been discovered yet.
func (*Manager) ExportRun ¶
ExportRun writes a specific run's discovered system (id 0 = latest). Returns ErrNoSuchRun for an unknown/evicted id, or a "no system" error when the run exists but produced nothing.
func (*Manager) ExportSurvey ¶ added in v0.3.7
ExportSurvey writes a run's signal inventory to w in the given format (id 0 = latest). Returns ErrNoSuchRun for an unknown/evicted id, or a "no survey" error when the run exists but was a plain hunt.
func (*Manager) KnownRun ¶
KnownRun reports whether id refers to any run the Manager has seen (active or in history), so callers can distinguish "unknown id" (404) from "run exists but produced nothing" (409).
func (*Manager) Run ¶
func (m *Manager) Run(id int) (*DiscoveredSystem, []CaptureReport, bool)
Run returns a specific run's discovered system + reports. id 0 means the latest (same as Current). Returns ok=false for an unknown/evicted id, or for a run that produced no system.
func (*Manager) Start ¶
func (m *Manager) Start(opts LiveHuntOptions) (int, error)
Start launches a live hunt with opts. It returns the new run id, or an error if a run is already active. The run executes in the background; observe it via Status and the hunt.* bus events.
type ManagerOptions ¶
type ManagerOptions struct {
// Acquire obtains the run's IQSource. Required.
Acquire Acquirer
Bus *events.Bus
Log *slog.Logger
}
ManagerOptions configure a Manager.
type NeighborRef ¶
type NeighborRef struct {
RFSS uint8 `json:"rfss"`
Site uint8 `json:"site"`
ChannelID uint8 `json:"channel_id,omitempty"`
ChannelNumber uint16 `json:"channel_number,omitempty"`
}
NeighborRef is an adjacent site advertised by the control channel.
type Observation ¶
type Observation struct {
// Protocol is the trunking.Protocol.String() spelling of the decoded CC.
Protocol string
// Confidence is the protocol-identification confidence (0..1). When the
// protocol was supplied by the operator rather than auto-identified, pass
// 1.0.
Confidence float64
// Result is the signal-lab decode of the capture. Lock + Grants + Events
// are mined for identity and talkgroups.
Result *siglab.Result
// FallbackFreqHz is the capture's nominal center frequency, used as the
// control-channel frequency when the decoder's lock payload doesn't carry
// one (e.g. a baseband capture). 0 ⇒ no fallback.
FallbackFreqHz uint32
// At is the wall-clock time the capture was taken (defaults to now).
At time.Time
}
Observation is one control-channel decode folded into a DiscoveredSystem. It is the bridge between the signal lab (which produces a siglab.Result per capture) and the discovery model. One Observation typically corresponds to one captured control channel of one site.
type OnStep ¶
OnStep, when set, is called once per tuned step with the step center and the peaks found there — used by the live hunt to publish progress.
type Peak ¶
Peak is a candidate carrier found in a spectrum frame: its absolute frequency, its power, and how far it stands above the estimated noise floor.
func DetectPeaks ¶
func DetectPeaks(frame spectrum.Frame, opts PeakOptions) []Peak
DetectPeaks finds candidate carriers in a spectrum frame. It estimates a robust noise floor (low-quartile of the bin powers), keeps local maxima that stand ThresholdDb above it, drops the DC bin and the band-edge guard bins, and enforces a minimum carrier spacing (stronger peak wins). Bin indices are mapped to absolute Hz using the frame's center and sample rate. Returned peaks are sorted by descending SNR.
type PeakOptions ¶
type PeakOptions struct {
// ThresholdDb is the minimum power above the estimated noise floor for a
// local maximum to count as a carrier. 0 ⇒ a sensible default (10 dB).
ThresholdDb float32
// MinSpacingHz is the minimum separation between reported peaks; when two
// maxima fall closer than this the stronger one wins. 0 ⇒ 6.25 kHz (the
// tightest trunking channel step).
MinSpacingHz uint32
// GuardBins drops this many bins at each band edge (rolloff) from
// consideration. 0 ⇒ a default proportional to the FFT size.
GuardBins int
}
PeakOptions tune the carrier detector.
type RunState ¶
type RunState string
RunState is the lifecycle of a live hunt run, surfaced to the cockpit/REST.
const ( StateRunIdle RunState = "idle" // no run has started yet StateRunActive RunState = "running" // a sweep/identify run is in progress StateRunDone RunState = "done" // last run finished and produced a map StateRunStopped RunState = "stopped" // last run was cancelled by the operator StateRunFailed RunState = "failed" // last run errored (e.g. SDR acquisition) )
type RunStatus ¶
type RunStatus struct {
RunID int `json:"run_id"`
State RunState `json:"state"`
Running bool `json:"running"`
Mode string `json:"mode"` // "hunt" | "survey"
Progress LiveHuntProgress `json:"progress"`
Sites int `json:"sites"`
Talkgroups int `json:"talkgroups"`
SystemName string `json:"system_name,omitempty"`
// Signals is the classified-carrier inventory of a survey run (nil for a
// plain hunt). It is the survey's primary result, rendered by the cockpit.
Signals []DetectedSignal `json:"signals,omitempty"`
Error string `json:"error,omitempty"`
StartedAt time.Time `json:"started_at,omitempty"`
FinishedAt time.Time `json:"finished_at,omitempty"`
}
RunStatus is the read snapshot the cockpit/REST renders.
type SignalSurvey ¶ added in v0.3.7
type SignalSurvey struct {
StartedAt time.Time `json:"started_at"`
FinishedAt time.Time `json:"finished_at"`
Signals []DetectedSignal `json:"signals"`
// System is the trunked system accumulated from the trunk-control carriers,
// or nil when none was found. It is exported exactly like a hunt result.
System *DiscoveredSystem `json:"system,omitempty"`
}
SignalSurvey is the inventory a live survey produces: every carrier the sweep detected, classified by modulation family and (where the class warranted it) decoded. It is the peer artifact of DiscoveredSystem — where DiscoveredSystem maps one trunked system, SignalSurvey catalogues everything on the air across the swept band(s). When the survey finds a trunking control channel it still folds it into System, so a survey is a strict superset of a hunt: it yields the same system map plus the surrounding signal landscape.
func (*SignalSurvey) Counts ¶ added in v0.3.7
func (s *SignalSurvey) Counts() (trunking, analog, paging, other int)
Counts tallies the inventory by broad category for status summaries.
type SurveyFormat ¶ added in v0.3.7
type SurveyFormat int
SurveyFormat selects an export encoding for a SignalSurvey inventory. It is separate from Format (which encodes a DiscoveredSystem) because the survey is a distinct artifact — the full classified-carrier list, not just the trunked map.
const ( // SurveyJSON marshals the whole SignalSurvey (signals + embedded system). SurveyJSON SurveyFormat = iota // SurveyCSV writes one row per detected signal, for spreadsheets/diffs. SurveyCSV )
func ParseSurveyFormat ¶ added in v0.3.7
func ParseSurveyFormat(s string) (SurveyFormat, error)
ParseSurveyFormat maps a CLI/REST value to a SurveyFormat.
func (SurveyFormat) FileExtension ¶ added in v0.3.7
func (f SurveyFormat) FileExtension() string
FileExtension is the conventional extension for a survey export.
func (SurveyFormat) String ¶ added in v0.3.7
func (f SurveyFormat) String() string
type SweepOptions ¶
type SweepOptions struct {
Source IQSource
Bands []Band
// FFTSize is the bins per step (power of two). 0 ⇒ 4096.
FFTSize int
// SweepDwell is how long to accumulate per step before detecting peaks.
// Captures one FFT frame per ~10 ms of dwell, averaged. 0 ⇒ one frame.
SweepDwell time.Duration
// GuardFrac reserves this fraction of each step's bandwidth at the edges
// (where the SDR rolloff lives) when advancing the center. 0 ⇒ 0.1.
GuardFrac float64
PeakOpts PeakOptions
Log *slog.Logger
}
SweepOptions configure a Sweeper.
type Sweeper ¶
type Sweeper struct {
// contains filtered or unexported fields
}
Sweeper walks operator-given bands on an IQSource and returns the candidate carriers it finds. It is the discovery front-end: its candidates feed the identify→decode→accumulate pipeline (see LiveHunter).
func NewSweeper ¶
func NewSweeper(opts SweepOptions) (*Sweeper, error)
NewSweeper validates options and builds a Sweeper.
type TrunkingRef ¶ added in v0.3.7
type TrunkingRef struct {
Protocol string `json:"protocol"`
Confidence float64 `json:"confidence"`
Locked bool `json:"locked"`
ControlHz uint32 `json:"control_hz,omitempty"`
}
TrunkingRef summarises the trunking decode of a carrier the router handed to the siglab identify path. It mirrors the fields of a CaptureReport that are meaningful at the inventory level; the full system map lives in Survey.System.