Documentation
¶
Overview ¶
Package trunking holds the cross-protocol orchestration: System definitions, control-channel hunting, talkgroup priority, voice grant following, and (later) multi-site neighbor tracking.
Index ¶
- Variables
- func CanPreempt(active Grant, activeTG *TalkGroup, incoming Grant, incomingTG *TalkGroup) bool
- func EffectivePriority(g Grant, tg *TalkGroup) int
- type ActiveCall
- type Cache
- type CachedSystem
- type CallEnd
- type CallStart
- type EndReason
- type Engine
- func (e *Engine) ActiveCalls() []*ActiveCall
- func (e *Engine) Close()
- func (e *Engine) EndCall(deviceSerial string, reason EndReason) bool
- func (e *Engine) EndSyntheticCall(deviceSerial string, reason EndReason) bool
- func (e *Engine) HandleGrant(g Grant)
- func (e *Engine) HandleSyntheticCall(g Grant, deviceSerial string)
- func (e *Engine) Run(ctx context.Context) error
- func (e *Engine) ScanMode() ScanMode
- func (e *Engine) SetScanMode(m ScanMode) ScanMode
- func (e *Engine) Touch(deviceSerial string)
- type EngineOptions
- type Grant
- type HuntFailed
- type HuntProgress
- type Hunter
- type HunterOptions
- type LockResult
- type LockedPayload
- type Protocol
- type ScanMode
- type System
- type TalkGroup
- type TalkgroupDB
- func (d *TalkgroupDB) Add(tg *TalkGroup)
- func (d *TalkgroupDB) All() []*TalkGroup
- func (d *TalkgroupDB) Delete(id uint32) bool
- func (d *TalkgroupDB) Len() int
- func (d *TalkgroupDB) LoadCSV(r io.Reader) (int, error)
- func (d *TalkgroupDB) LoadCSVFile(path string) (int, error)
- func (d *TalkgroupDB) LoadJSON(r io.Reader) (int, error)
- func (d *TalkgroupDB) Lookup(id uint32) *TalkGroup
- func (d *TalkgroupDB) UpdateFields(id uint32, fn func(*TalkGroup)) bool
- type Tuner
- type VoiceDevice
- type VoicePool
- func (p *VoicePool) Active() []*ActiveCall
- func (p *VoicePool) Bind(d *VoiceDevice, g Grant, tg *TalkGroup, now time.Time) (*ActiveCall, error)
- func (p *VoicePool) Devices() []*VoiceDevice
- func (p *VoicePool) FindFree() *VoiceDevice
- func (p *VoicePool) LowestPriorityActive() *ActiveCall
- func (p *VoicePool) Release(serial string) *ActiveCall
- func (p *VoicePool) Touch(serial string, now time.Time)
Constants ¶
This section is empty.
Variables ¶
var ErrNoControlChannel = errors.New("trunking/hunter: no control channel found")
ErrNoControlChannel is returned when every candidate frequency exhausts its dwell window without locking.
Functions ¶
func CanPreempt ¶
CanPreempt reports whether a new grant should kick an active call off a Voice device. The rule is strict-higher: equal priority does NOT preempt (so a stable call holds the device against same-priority grants).
Returns false if the new grant is locked out by talkgroup policy — the engine handles lockout earlier in the dispatch path, but the predicate is defensive here so callers can compose freely.
func EffectivePriority ¶
EffectivePriority returns the runtime priority used by the engine when comparing grants. Lower number = higher priority.
Types ¶
type ActiveCall ¶
type ActiveCall struct {
Device *VoiceDevice
Grant Grant
Talkgroup *TalkGroup
StartedAt time.Time
LastHeardAt time.Time
}
ActiveCall describes a grant currently being followed on a specific Voice device. The engine creates these via VoicePool.Bind.
type Cache ¶
type Cache struct {
// contains filtered or unexported fields
}
Cache persists the last-known control-channel frequency per system to a JSON file. The hunter consults this cache on startup so it can re-tune to a known-good CC before scanning the full frequency list.
func OpenCache ¶
OpenCache loads (or creates) a cache file at path. A non-existent file is treated as an empty cache.
func (*Cache) Get ¶
func (c *Cache) Get(name string) (CachedSystem, bool)
Get returns the cached entry for the named system, if any.
type CachedSystem ¶
type CachedSystem struct {
LastFrequencyHz uint32 `json:"last_frequency_hz"`
LastLockAt time.Time `json:"last_lock_at,omitempty"`
NAC uint16 `json:"nac,omitempty"`
}
CachedSystem is the on-disk record for one system.
type CallEnd ¶
type CallEnd struct {
Grant Grant
Talkgroup *TalkGroup
DeviceSerial string
StartedAt time.Time
EndedAt time.Time
Reason EndReason
}
CallEnd is the payload of an events.KindCallEnd event.
type CallStart ¶
type CallStart struct {
Grant Grant
Talkgroup *TalkGroup // resolved via the engine's TalkgroupDB; nil if unknown
DeviceSerial string // which Voice SDR is following the call
StartedAt time.Time
}
CallStart is the payload of an events.KindCallStart event. The engine publishes this once a Voice device has been retuned to the grant's frequency; downstream pipelines (the demod composer, the recorder) subscribe and start consuming IQ.
type EndReason ¶
type EndReason uint8
EndReason classifies why a call ended; carried in CallEnd events so the API layer can surface the cause to UIs.
const ( EndReasonUnknown EndReason = iota EndReasonNormal // CC announced channel release / talk-off EndReasonTimeout // engine watchdog fired (no recent activity) EndReasonPreempted // higher-priority grant kicked us off EndReasonLockout // talkgroup is locked out by policy EndReasonNoVoiceSDR // every Voice-role SDR was busy EndReasonError EndReasonManual // operator ended the call via API / TUI )
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine is the central trunking state machine. It subscribes to events.KindGrant, looks up the talkgroup, dispatches to the voice pool (preempting lower-priority active calls when necessary), and emits events.KindCallStart / events.KindCallEnd.
The engine deliberately knows nothing about the demod pipeline — it just tunes Voice devices and publishes structured events. Downstream consumers (the voice composer + recorder, the SQLite call log) subscribe to the CallStart / CallEnd events to do their work.
func NewEngine ¶
func NewEngine(opts EngineOptions) (*Engine, error)
NewEngine validates opts and returns a ready-to-Run engine.
func (*Engine) ActiveCalls ¶
func (e *Engine) ActiveCalls() []*ActiveCall
ActiveCalls returns a snapshot of every active call — trunked calls allocated through the voice pool plus synthetic calls owned by external scanners (the conventional FM scanner publishes these through HandleSyntheticCall).
func (*Engine) Close ¶
func (e *Engine) Close()
Close releases the engine's subscription. Safe to call concurrently with Run; idempotent on repeat calls. Subscription.Close is itself idempotent so we don't need to nil the field — that nil-write was previously a race with Run's read of e.sub.C.
func (*Engine) EndCall ¶
EndCall is the explicit external signal that a call has ended (e.g. the protocol decoder saw a channel-release announcement, or an upstream test wants to release the device). reason is published in the CallEnd event payload.
func (*Engine) EndSyntheticCall ¶
EndSyntheticCall is the conventional scanner's "carrier dropped" signal. Publishes CallEnd and forgets the call. Returns false if the engine has no synthetic call bound to deviceSerial.
func (*Engine) HandleGrant ¶
HandleGrant is the engine's grant-dispatch entrypoint. It is exported so tests can drive it directly without a running event loop.
func (*Engine) HandleSyntheticCall ¶
HandleSyntheticCall registers a call originated by a non-trunked source (the conventional FM scanner is the canonical example) that already owns its SDR — no VoicePool binding, no re-tune, no preemption logic. The engine publishes CallStart and adds the call to ActiveCalls() so the API + TUI surfaces light up like any other call. Pair with EndSyntheticCall to release.
deviceSerial must be unique across the daemon's call set so the recorder can route WritePCM samples to the right WAV.
func (*Engine) Run ¶
Run drains grant events from the bus and runs the watchdog until ctx cancels. Returns ctx.Err(). Run does NOT close the engine's subscription; call Close when you're done with the engine.
func (*Engine) ScanMode ¶
ScanMode returns the engine's current scan mode. Safe to call from any goroutine.
func (*Engine) SetScanMode ¶
SetScanMode swaps the engine's scan mode at runtime — the API cockpit calls this when the operator flips the global scan_mode from the TUI. Returns the previous mode so the caller can log / audit the change.
type EngineOptions ¶
type EngineOptions struct {
Bus *events.Bus
Log *slog.Logger
VoicePool *VoicePool
Talkgroups *TalkgroupDB
// CallTimeout is how long a call can run without a Touch before the
// watchdog ends it as EndReasonTimeout. Default 30 s.
CallTimeout time.Duration
// Now is injectable for tests; defaults to time.Now.
Now func() time.Time
// ScanMode controls whether HandleGrant respects the per-talkgroup
// Scan flag. Default ScanModeAll keeps every non-locked-out grant
// flowing through; ScanModeList enforces the talkgroup scan list.
ScanMode ScanMode
}
EngineOptions configure a new Engine.
type Grant ¶
type Grant struct {
System string // System name, matches trunking.System.Name
Protocol string // "p25" / "dmr" / "nxdn"
GroupID uint32 // talkgroup or destination subscriber address
SourceID uint32 // originator (subscriber unit)
FrequencyHz uint32 // voice channel frequency
ChannelID uint8 // raw channel ID (P25 band-plan ID, DMR LCN high)
ChannelNum uint16 // raw channel number within the ID
Encrypted bool
Emergency bool
DataCall bool // false = voice call (default)
// ProVoice marks the grant as an EDACS ProVoice (digital) call. The
// vocoder is patent + trade-secret encumbered so we cannot ship a
// built-in decoder; the recorder treats this flag as a directive to
// emit a `.raw` frame sidecar regardless of its global WriteRaw
// setting, so researchers can decode out-of-band.
ProVoice bool
At time.Time
}
Grant is the protocol-agnostic voice-channel grant payload published on the events bus by P25/DMR/NXDN control-channel decoders. The trunking engine subscribes to events of kind events.KindGrant and dispatches them through the priority + voice-device pool.
FrequencyHz must be filled in by the protocol layer (P25 derives it from IdentifierUpdate band-plan TSBKs, DMR/NXDN from the configured System). If FrequencyHz is zero, the engine logs and drops the grant.
type HuntFailed ¶
type HuntFailed struct {
System string `json:"system"`
At time.Time `json:"at"`
BackoffMs int `json:"backoff_ms"`
}
HuntFailed is the payload for events.KindHuntFailed — published when a system's CC candidate list exhausts without locking. BackoffMs is the supervisor's next sleep window so the TUI can show "retry in 5 s".
type HuntProgress ¶
type HuntProgress struct {
System string `json:"system"`
AttemptedFreqHz uint32 `json:"attempted_freq_hz"`
AttemptIndex int `json:"attempt_index"`
TotalCandidates int `json:"total_candidates"`
At time.Time `json:"at"`
}
HuntProgress is the payload published with events.KindHuntProgress. One event fires per CC candidate the hunter tries; the TUI uses AttemptIndex / TotalCandidates to render a position indicator.
type Hunter ¶
type Hunter struct {
// contains filtered or unexported fields
}
Hunter scans a System's candidate control channels and parks on the first frequency that produces a matching cc.locked event within the per-frequency dwell timeout.
The hunter is intentionally protocol-agnostic at the wiring level: it retunes the SDR and watches the events.Bus. The downstream demod pipeline (channelizer + C4FM/H-DQPSK demod + protocol decoder) publishes cc.locked events; the hunter parks on the first match.
func NewHunter ¶
func NewHunter(o HunterOptions) (*Hunter, error)
func (*Hunter) Hunt ¶
func (h *Hunter) Hunt(ctx context.Context) (LockResult, error)
Hunt scans the candidate frequencies until either a CC locks (success) or ctx cancels (returns ctx.Err()) or the candidate list is exhausted (returns ErrNoControlChannel).
On success the locked frequency and NAC are persisted to the cache and returned to the caller.
type HunterOptions ¶
type HunterOptions struct {
System System
Tuner Tuner
Bus *events.Bus
Cache *Cache
Log *slog.Logger
// Dwell is how long to wait on each candidate before giving up.
// Defaults to 3 seconds.
Dwell time.Duration
}
HunterOptions configure a Hunter at construction.
type LockResult ¶
LockResult is returned by a successful Hunt.
func (LockResult) String ¶
func (r LockResult) String() string
String renders a one-line summary of a LockResult for logs.
type LockedPayload ¶
LockedPayload is the protocol-neutral shape the hunter expects on CCLocked events. Each radio package's LockState satisfies it via methods, so the hunter can stay protocol-agnostic instead of importing every radio package (which would also create import cycles, since some radio packages now import this `trunking` package to publish `Grant` events).
type Protocol ¶
type Protocol uint8
Protocol is the trunking protocol family in use on a System.
const ( ProtocolUnknown Protocol = iota ProtocolP25 // P25 Phase 1 (config "p25" — Phase 2 uses ProtocolP25Phase2) ProtocolDMR // DMR Tier II / III ProtocolNXDN // NXDN ProtocolDPMR // dPMR Mode 3 (digital PMR446 trunking) ProtocolEDACS // EDACS / GE-Marc ProtocolMotorola // Motorola Type II / SmartZone ProtocolLTR // Logic Trunked Radio (LTR / LTR-Net) ProtocolMPT1327 // MPT 1327 (UK / Commonwealth utility trunking) ProtocolP25Phase2 // P25 Phase 2 (H-DQPSK TDMA, config "p25-phase2") ProtocolTETRA // TETRA TMO (π/4-DQPSK, ETSI EN 300 392-2) ProtocolYSF // System Fusion (C4FM, amateur trunked variant — config "ysf") ProtocolDStar // D-STAR (GMSK 4800 bps, amateur — header-only repeater protocol; config "dstar") ProtocolDMRTier2 // DMR Tier II conventional (per-repeater; config "dmr-tier2") )
func ParseProtocol ¶
ParseProtocol maps a string ("p25", "dmr", "nxdn", "dpmr", "edacs", "motorola", "ltr", "mpt1327", "p25-phase2", "tetra") to a Protocol value.
type ScanMode ¶
type ScanMode uint8
ScanMode controls how Engine.HandleGrant filters incoming grants against the talkgroup database's `Scan` flag.
- ScanModeAll (default): every non-locked-out grant is dispatched, regardless of TalkGroup.Scan. This is the backwards-compatible behavior — pre-scanner configs see no change.
- ScanModeList: only grants whose talkgroup carries Scan==true (or whose grant is flagged Emergency, parallel to the Lockout exception) are dispatched. Unknown talkgroup IDs are dropped because there's no way to know they're scannable.
func ParseScanMode ¶
ParseScanMode is the inverse of String. Empty string maps to the safe default (all); unknown values also map to all so a typo in config doesn't accidentally silence the daemon.
type System ¶
type System struct {
Name string
Protocol Protocol
ControlChannels []uint32 // candidate CC frequencies in Hz
WACN uint32 // 20-bit Wide-Area Communication Network ID (P25)
SystemID uint16 // 12-bit system identifier (P25 SYSID)
RFSS uint8 // RF SubSystem ID (P25)
Site uint8 // Site ID
// TETRAColourCode is the low 30 bits of the extended colour code
// the TETRA scrambler uses to seed its LFSR per ETSI EN 300 392-2
// §8.2.5 ("ec" in the spec). The ccdecoder connector forwards this
// into tetra.ControlChannel.SetColourCode under ChannelCodingOn.
// Zero is valid only for BSCH (§8.2.5.2). For all other channel
// types the colour code is the per-cell secret the descrambler
// needs to recover the type-3 stream — leaving it at zero with
// channel coding on produces garbage. Bits 30..31 are silently
// ignored downstream.
TETRAColourCode uint32
// TETRAChannel selects which TETRA logical channel lives in each
// burst window under ChannelCodingOn. Recognised values:
// "sch/hd" | "sch/f" | "sch/hu" | "bsch" | "aach" (case-insensitive,
// "/" optional). Empty defaults to "sch/hd" — the most common
// signaling carrier for cc.locked / Grant events. Forwarded into
// tetra.ControlChannel.SetExpectedChannel by the ccdecoder
// connector after parsing via tetra.ParseChannelType.
TETRAChannel string
// TETRAChannelCoding gates the full ETSI EN 300 392-2 §8.3.1
// channel-coding chain (descramble + deinterleave + depuncture +
// Viterbi + CRC-16 verify + tail strip). Recognised values
// (case-insensitive): "" / "on" / "true" / "1" → ChannelCodingOn
// (the new default; required for live on-air captures); "off" /
// "false" / "0" → ChannelCodingOff (legacy raw-dibit path, opt-out
// for operators feeding pre-stripped DSD-FME / OP25 fixtures).
// Forwarded into tetra.ControlChannel.SetChannelCoding by the
// ccdecoder connector after parsing via tetra.ParseChannelCoding.
TETRAChannelCoding string
// LTRFCSMode enables CRC-7 FCS verification on the LTR Status
// Ingest path (per DSheirer/sdrtrunk's CRCLTR.java layout).
// Recognised values (case-insensitive): "" / "on" / "true" / "1" →
// FCSOn (the new default; drop Status words whose 7-bit FCS
// trailer doesn't match the CRC over the 24-bit message vector);
// "off" / "false" / "0" → FCSOff (no verification — opt-out for
// pre-stripped fixtures). Forwarded into
// ltr.ControlChannel.SetFCSMode by the ccdecoder connector after
// parsing via ltr.ParseFCSMode.
LTRFCSMode string
// LTRManchesterMode controls Manchester decoding of the LTR
// sub-audible bit stream. Recognised values (case-insensitive):
// "" / "on" / "soft" → ManchesterSoft (the new default —
// majority-decode each pair; matches the dominant on-air
// encoding for sub-audible LTR signaling); "strict" —
// require a mid-bit transition per pair, drop transition-less
// pairs; "off" / "nrz" → ManchesterOff (raw NRZ — opt-out for
// synthesized NRZ fixtures). Forwarded into
// ltr.ControlChannel.SetManchesterMode by the ccdecoder
// connector after parsing via ltr.ParseManchesterMode.
LTRManchesterMode string
// P25Phase2TrellisMode enables the 4-state ½-rate trellis FEC
// decoder on the P25 Phase 2 MAC PDU window. Recognised values
// (case-insensitive): "" / "on" / "true" / "1" → TrellisOn (the
// new default — 146 channel dibits via the TIA-102.AABF trellis
// decoder); "off" / "false" / "0" → TrellisOff (legacy 72-dibit
// raw-MAC-PDU path, opt-out for pre-stripped fixtures). Forwarded
// into p25phase2.ControlChannel.SetTrellisMode by the ccdecoder
// connector after parsing via p25phase2.ParseTrellisMode.
P25Phase2TrellisMode string
// P25Phase2RSMode enables the outer Reed-Solomon RS(24, 16, 9)
// verification layer on top of the trellis-decoded MAC PDU.
// Recognised values (case-insensitive): "" / "off" / "false" /
// "0" → RSOff (the default — no outer RS verification; matches
// historical decoder behaviour); "on" / "true" / "1" → RSOn
// (verify RS syndromes per TIA-102.BAAA-A §5.9; drop MAC PDUs
// whose syndromes are non-zero before parsing). Forwarded into
// p25phase2.ControlChannel.SetRSMode by the ccdecoder connector
// after parsing via p25phase2.ParseRSMode.
P25Phase2RSMode string
// P25Phase2ScramblerMode enables the PN44 descrambler per
// TIA-102.BBAC-1 §7.2.5 on the trellis-decoded MAC PDU bits.
// Recognised values (case-insensitive): "" / "off" / "false" /
// "0" → ScramblerOff (the default — no PN44 descrambling);
// "on" / "true" / "1" → ScramblerOn. The seed is computed from
// (WACN, SystemID, low 12 bits of Site as the spec's Color
// Code = NAC) per spec equation (5). Forwarded into
// p25phase2.ControlChannel.SetScramblerMode +
// SetScramblerSeed by the ccdecoder connector.
P25Phase2ScramblerMode string
// P25Phase2ClockMode selects the symbol-timing-recovery strategy
// for the P25 Phase 2 receiver. Recognised values (case-
// insensitive): "" / "gardner" / "on" → ClockGardner (the new
// default — non-data-aided Gardner loop; recommended for live
// SDR captures); "naive" / "off" → ClockNaive (decimate every
// sps-th sample; works on sample-aligned synthesized IQ).
// Forwarded into p25phase2rx.Options.ClockMode by the ccdecoder
// connector after parsing via p25phase2rx.ParseClockMode.
P25Phase2ClockMode string
// TETRAClockMode mirrors P25Phase2ClockMode for the TETRA
// receiver. Same recognised values + parser semantics; the
// underlying ClockMode enums in the two receivers share the
// same name + values but are independent types.
TETRAClockMode string
// NXDNViterbiMode enables the K=5 ½-rate Viterbi FEC decoder
// on the NXDN CAC region. Recognised values (case-insensitive):
// "" / "spec" → ViterbiSpec (the new default — full NXDN-TS-1-A
// §4.5.1.1 outbound CAC chain); "on" / "true" / "1" → ViterbiOn
// (intermediate 92-dibit K=5 Viterbi path for older
// MMDVMHost / DSDcc fixtures); "off" / "false" / "0" → ViterbiOff
// (legacy 44-dibit raw-CAC path, opt-out for pre-stripped
// fixtures). Forwarded into nxdn.ControlChannel.SetViterbiMode
// by the ccdecoder connector after parsing via
// nxdn.ParseViterbiMode.
NXDNViterbiMode string
// EDACSBCHMode enables the BCH(40, 28, 2) FEC layer on the
// EDACS CCW. Recognised values (case-insensitive): "" / "on" /
// "true" / "1" → BCHOn (the new default — 40-bit on-wire
// BCH(40, 28, 2) decode + single/double-bit correction); "off" /
// "false" / "0" → BCHOff (legacy pre-stripped 40-bit CCW path,
// opt-out for pre-stripped fixtures). Forwarded into
// edacs.ControlChannel.SetBCHMode by the ccdecoder connector
// after parsing via edacs.ParseBCHMode.
EDACSBCHMode string
// MPT1327BCHMode enables the BCH(63, 38) FEC layer on the MPT
// 1327 codeword. Recognised values (case-insensitive): "" /
// "on" / "true" / "1" → BCHOn (the new default — 64-bit on-wire
// BCH(63, 38) decode); "off" / "false" / "0" → BCHOff (legacy
// 38-bit pre-stripped codeword path, opt-out for pre-stripped
// fixtures). Forwarded into mpt1327.ControlChannel.SetBCHMode
// by the ccdecoder connector after parsing via
// mpt1327.ParseBCHMode.
MPT1327BCHMode string
// MPT1327CWSCTolerance sets the Hamming-distance threshold the
// MPT 1327 Process adapter uses when matching the 16-bit
// Codeword Synchronisation Code. Recognised values
// (case-insensitive): "" → 2-bit tolerance (the new default,
// matches commercial MPT 1327 receivers on noisy on-air
// captures); "0" / "exact" / "off" → exact match (for
// pre-stripped synthesized fixtures); a decimal integer in
// [0, 15]. Forwarded into mpt1327.ControlChannel.SetCWSCTolerance
// by the ccdecoder connector after parsing via
// mpt1327.ParseCWSCTolerance.
MPT1327CWSCTolerance string
// MotorolaBCHMode enables the BCH(64, 16, 11) FEC layer on the
// Motorola Type II OSW. Recognised values (case-insensitive):
// "" / "on" / "true" / "1" → BCHOn (the new default — two
// 64-bit BCH(64, 16, 11) codewords reassembled into the 32-bit
// OSW, with up to 11 bit errors corrected per codeword); "off" /
// "false" / "0" → BCHOff (legacy 32-bit raw-OSW path, opt-out
// for pre-stripped fixtures). Forwarded into
// motorola.ControlChannel.SetBCHMode by the ccdecoder
// connector after parsing via motorola.ParseBCHMode.
MotorolaBCHMode string
// DStarFECMode enables the JARL DV-mode header FEC chain on the
// D-STAR Process adapter. Recognised values (case-insensitive):
// "" / "off" / "false" / "0" → FECOff (the default — reads 328
// info bits straight off the wire, matches synthesized fixtures
// + pre-FEC-stripped inputs); "on" / "true" / "1" → FECOn (660
// on-wire bits → deinterleave 22×30 → PN15 descramble →
// depuncture → K=5 R=1/2 Viterbi → 328 info bits → ParseHeader).
// Forwarded into dstar.ControlChannel.SetFECMode by the
// ccdecoder connector after parsing via dstar.ParseFECMode.
DStarFECMode string
}
System describes one trunked radio system the engine should track.
type TalkGroup ¶
type TalkGroup struct {
ID uint32 `json:"id"`
AlphaTag string `json:"alpha_tag"`
Description string `json:"description,omitempty"`
Tag string `json:"tag,omitempty"` // department / category
Group string `json:"group,omitempty"` // top-level group
Mode string `json:"mode,omitempty"` // D=digital, A=analog, M=mixed
Priority int `json:"priority,omitempty"` // 1 = highest, 10 = lowest, 0 = unset
Lockout bool `json:"lockout,omitempty"`
Scan bool `json:"scan"`
}
TalkGroup describes one talkgroup loaded from disk. The schema follows the Trunk Recorder / RadioReference talkgroup CSV convention.
Scan participates in the engine's per-talkgroup scan list when the engine runs in ScanModeList — only talkgroups with Scan == true get their grants followed (Emergency grants bypass the gate). In ScanModeAll (the default for backwards compat with pre-scanner configs) the field is moot. Defaults to true on every loader so a legacy CSV without a Scan column keeps the existing behavior.
type TalkgroupDB ¶
type TalkgroupDB struct {
// contains filtered or unexported fields
}
TalkgroupDB is a thread-safe lookup over loaded talkgroups.
func (*TalkgroupDB) Add ¶
func (d *TalkgroupDB) Add(tg *TalkGroup)
Add or replace a single talkgroup record.
func (*TalkgroupDB) All ¶
func (d *TalkgroupDB) All() []*TalkGroup
All returns a snapshot of every talkgroup in the DB.
func (*TalkgroupDB) Delete ¶
func (d *TalkgroupDB) Delete(id uint32) bool
Delete removes the talkgroup with the given id. Returns false if no such talkgroup exists.
func (*TalkgroupDB) Len ¶
func (d *TalkgroupDB) Len() int
Len returns the number of loaded talkgroups.
func (*TalkgroupDB) LoadCSV ¶
func (d *TalkgroupDB) LoadCSV(r io.Reader) (int, error)
LoadCSV reads talkgroups from a Trunk-Recorder-style CSV. Required column: a numeric DEC/Decimal column. Optional columns (matched by header, case-insensitive): Alpha Tag, Description, Mode, Tag, Group, Priority, Lockout.
A "Y" / "yes" / "true" value in Lockout sets the flag. Lockout is also inferred when Priority is set to a sentinel "L" value, matching common community CSVs.
func (*TalkgroupDB) LoadCSVFile ¶
func (d *TalkgroupDB) LoadCSVFile(path string) (int, error)
LoadCSVFile is a thin wrapper over LoadCSV for a path on disk.
func (*TalkgroupDB) LoadJSON ¶
func (d *TalkgroupDB) LoadJSON(r io.Reader) (int, error)
LoadJSON reads a JSON array of TalkGroup records. Records missing the "scan" key resolve to Scan=true so legacy JSON dumps keep the "follow every grant" behavior; explicit `"scan": false` turns off participation in the scan list.
func (*TalkgroupDB) Lookup ¶
func (d *TalkgroupDB) Lookup(id uint32) *TalkGroup
Lookup returns the talkgroup record for id, or nil if unknown.
func (*TalkgroupDB) UpdateFields ¶
func (d *TalkgroupDB) UpdateFields(id uint32, fn func(*TalkGroup)) bool
UpdateFields applies fn to the talkgroup with the given id under the write lock. Returns false if no such talkgroup exists. Used by the API to mutate Priority / Lockout without exposing the raw pointer to outside callers.
type Tuner ¶
Tuner is the subset of sdr.Device the hunter needs. Decoupling from the full Device interface keeps the hunter testable without an IQ source.
type VoiceDevice ¶
VoiceDevice is one Voice-role SDR available to the engine. The engine retunes it via the embedded Tuner interface and tracks an optional active call.
type VoicePool ¶
type VoicePool struct {
// contains filtered or unexported fields
}
VoicePool manages the set of Voice-role devices and the call currently (if any) bound to each. It is safe for concurrent use.
func NewVoicePool ¶
func NewVoicePool(devices []*VoiceDevice) *VoicePool
NewVoicePool returns a pool over the supplied devices. The order of devices determines allocation preference (first-fit).
func (*VoicePool) Active ¶
func (p *VoicePool) Active() []*ActiveCall
Active returns a snapshot of every currently-bound call.
func (*VoicePool) Bind ¶
func (p *VoicePool) Bind(d *VoiceDevice, g Grant, tg *TalkGroup, now time.Time) (*ActiveCall, error)
Bind retunes the device to grant.FrequencyHz and records an active call. Returns an error if the device is already busy or the tune fails.
func (*VoicePool) Devices ¶
func (p *VoicePool) Devices() []*VoiceDevice
Devices returns a snapshot of the device list.
func (*VoicePool) FindFree ¶
func (p *VoicePool) FindFree() *VoiceDevice
FindFree returns the first device with no active call, or nil if every device is busy. The pool lock is held only during the scan.
func (*VoicePool) LowestPriorityActive ¶
func (p *VoicePool) LowestPriorityActive() *ActiveCall
LowestPriorityActive returns the active call with the lowest priority among all devices, or nil if no calls are active. Used by the engine when deciding which call to preempt.
func (*VoicePool) Release ¶
func (p *VoicePool) Release(serial string) *ActiveCall
Release marks the device free. Returns the freed ActiveCall (or nil if the device wasn't busy).