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 Affiliation
- type AffiliationResponse
- type AffiliationTracker
- type AffiliationTrackerOptions
- type Cache
- type CachedSystem
- type CallComplete
- type CallEncryption
- type CallEnd
- type CallSourceUpdate
- 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) Patches() []PatchGroup
- func (e *Engine) Run(ctx context.Context) error
- func (e *Engine) ScanMode() ScanMode
- func (e *Engine) SetScanMode(m ScanMode) ScanMode
- func (e *Engine) TalkgroupForDevice(deviceSerial string) *TalkGroup
- func (e *Engine) Touch(deviceSerial string)
- type EngineOptions
- type FrequencyChecker
- type Grant
- type HuntFailed
- type HuntProgress
- type Hunter
- type HunterOptions
- type Location
- type LockResult
- type LockedPayload
- type P25BandPlanEntry
- type P25Phase2Decode
- type Patch
- type PatchGroup
- type PatchRegistry
- type Protocol
- type RID
- type RIDDB
- func (d *RIDDB) Add(r *RID)
- func (d *RIDDB) All() []*RID
- func (d *RIDDB) Delete(id uint32) bool
- func (d *RIDDB) Len() int
- func (d *RIDDB) LoadCSV(r io.Reader) (int, error)
- func (d *RIDDB) LoadCSVFile(path string) (int, error)
- func (d *RIDDB) LoadJSON(r io.Reader) (int, error)
- func (d *RIDDB) LoadJSONFile(path string) (int, error)
- func (d *RIDDB) Lookup(id uint32) *RID
- func (d *RIDDB) UpdateFields(id uint32, fn func(*RID)) bool
- type ReacquireFunc
- type RegistrationResponse
- type ScanMode
- type System
- type TalkGroup
- type TalkerAlias
- 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 UnitActivity
- type UnitRegistration
- 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) FindFreeForFrequency(hz uint32) *VoiceDevice
- func (p *VoicePool) HasCapableDevice(hz uint32) bool
- func (p *VoicePool) LowestPriorityActive() *ActiveCall
- func (p *VoicePool) LowestPriorityActiveForFrequency(hz uint32) *ActiveCall
- func (p *VoicePool) Release(serial string) *ActiveCall
- func (p *VoicePool) SetReacquire(fn ReacquireFunc)
- func (p *VoicePool) Touch(serial string, now time.Time)
- func (p *VoicePool) UpdateEncryption(serial string, algID uint8, keyID uint16) (Grant, bool)
- func (p *VoicePool) UpdateSource(serial string, sourceID uint32, encrypted bool) (Grant, bool)
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 Affiliation ¶ added in v0.1.7
type Affiliation struct {
System string // System name, matches trunking.System.Name
Protocol string // "p25" / "dmr" / "nxdn"
SourceID uint32 // radio unit being affiliated
GroupID uint32 // talkgroup the unit is joining
AnnouncementGroup uint32 // optional announcement-group association (0 if unused)
Response AffiliationResponse // accepted / failed / denied / refused
At time.Time
}
Affiliation is published on the events bus when a radio unit affiliates with a talkgroup. Emitted by P25 control-channel decoders on opcode 0x28 (Group Affiliation Response).
type AffiliationResponse ¶ added in v0.1.7
type AffiliationResponse uint8
AffiliationResponse encodes the P25 Group Affiliation Response value (TIA-102.AABF Table 7-37). The integer values are wire constants — do not renumber.
const ( AffiliationAccepted AffiliationResponse = 0 AffiliationFailed AffiliationResponse = 1 AffiliationDenied AffiliationResponse = 2 AffiliationRefused AffiliationResponse = 3 )
func (AffiliationResponse) String ¶ added in v0.1.7
func (r AffiliationResponse) String() string
type AffiliationTracker ¶ added in v0.1.9
type AffiliationTracker struct {
// contains filtered or unexported fields
}
AffiliationTracker maintains a live, protocol-agnostic table of which radio units are active on which talkgroups. It subscribes to the events bus and updates the table from three sources:
- KindGrant — the grant's SourceID is transmitting on its GroupID.
- KindAffiliation — an explicit decoded affiliation message.
- KindUnitRegistration — the unit registered on a site.
Because grants carry SourceID + GroupID for every protocol, the tracker works uniformly across P25, DMR (all tiers and vendors), NXDN and the rest — no per-protocol decoding is required. Entries expire after a configurable idle TTL.
func NewAffiliationTracker ¶ added in v0.1.9
func NewAffiliationTracker(opts AffiliationTrackerOptions) (*AffiliationTracker, error)
NewAffiliationTracker validates opts and returns a tracker that has already subscribed to the bus.
func (*AffiliationTracker) Close ¶ added in v0.1.9
func (t *AffiliationTracker) Close() error
Close releases the bus subscription and waits for Run to drain.
func (*AffiliationTracker) Len ¶ added in v0.1.9
func (t *AffiliationTracker) Len() int
Len returns the number of tracked units.
func (*AffiliationTracker) Run ¶ added in v0.1.9
func (t *AffiliationTracker) Run(ctx context.Context) error
Run drains grant / affiliation / registration events and sweeps expired units until ctx cancels or the bus closes.
func (*AffiliationTracker) Snapshot ¶ added in v0.1.9
func (t *AffiliationTracker) Snapshot() []UnitActivity
Snapshot returns every tracked unit, most-recently-seen first.
func (*AffiliationTracker) UnitsOnTalkgroup ¶ added in v0.1.9
func (t *AffiliationTracker) UnitsOnTalkgroup(talkgroup uint32) []uint32
UnitsOnTalkgroup returns the radio IDs currently associated with the given talkgroup.
type AffiliationTrackerOptions ¶ added in v0.1.9
type AffiliationTrackerOptions struct {
Bus *events.Bus
// TTL is how long a unit stays in the table after it was last
// seen. Default 30 minutes.
TTL time.Duration
// Now is injectable for tests; defaults to time.Now.
Now func() time.Time
}
AffiliationTrackerOptions configure a tracker.
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 CallComplete ¶ added in v0.1.9
type CallComplete struct {
Grant Grant
Talkgroup *TalkGroup
DeviceSerial string
StartedAt time.Time
EndedAt time.Time
Reason EndReason
AudioPath string
SampleRate uint32
}
CallComplete is the payload of an events.KindCallComplete event. The recorder publishes it once a call's WAV has been flushed and closed, so the outbound-streaming subsystem (internal/broadcast) can read the finished file and upload it to call aggregators. AudioPath is the absolute or working-directory-relative path to the .wav the recorder wrote; SampleRate is its PCM rate in Hz.
func (CallComplete) Duration ¶ added in v0.1.9
func (c CallComplete) Duration() time.Duration
Duration returns how long the call ran.
type CallEncryption ¶ added in v0.2.2
type CallEncryption struct {
DeviceSerial string
System string // filled in by the engine on republish
Protocol string
GroupID uint32
AlgorithmID uint8
KeyID uint16
MessageIndicator [9]byte
At time.Time
}
CallEncryption is the payload of an events.KindCallEncryption event. It is published by the voice composer when an in-call Encryption Sync is recovered (P25 Phase 1 LDU2 carries it; the grant TSBK has only the encrypted flag). DeviceSerial keys the update to a specific active call; the engine backfills the bound ActiveCall.Grant's AlgorithmID / KeyID and republishes the event with System / Protocol / GroupID populated so SSE + TUI consumers can patch their live view without re-deriving the call's identity.
MessageIndicator carries the 72-bit per-call cryptographic sync vector — not surfaced in any DTO today, but plumbed through for future key-discovery tooling.
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 CallSourceUpdate ¶ added in v0.2.5
type CallSourceUpdate struct {
DeviceSerial string
System string // filled in by the engine on republish
Protocol string
GroupID uint32 // filled in by the engine on republish from the bound Grant
SourceID uint32
Encrypted bool
At time.Time
}
CallSourceUpdate is the payload of an events.KindCallSourceUpdate event. The voice composer publishes one when it recovers the source radio ID + encryption state from in-call traffic-channel signalling — e.g. a P25 Phase 2 GROUP_VOICE_CHANNEL_USER PDU where the CC grant arrived in a compressed form without those fields (src=0 + enc=false). The engine subscribes, backfills the bound ActiveCall.Grant.SourceID + .Encrypted via the voice pool, and republishes the event with System / Protocol / GroupID populated so SSE + TUI consumers can patch their live view. DeviceSerial keys the update to a specific active call.
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 is the carrier-drop natural end: either the CC // announced a channel release / talk-off, or — far more common // on P25 where no such announcement is ever sent — the watchdog // reaped a call whose Touch advanced past StartedAt (frames were // decoded and then the transmitter stopped). Operator-visible // meaning: the call ended cleanly, no decode problem. EndReasonNormal // EndReasonTimeout is the silent-from-start decode failure: the // watchdog reaped a call whose LastHeardAt never moved past // StartedAt — not a single LDU / voice subframe was delivered. // This is the real failure mode (wrong demod mode, gain too low, // LSM site decoded as C4FM, etc.) — distinct from EndReasonNormal // above, which fires when the radio simply stopped transmitting. EndReasonTimeout 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) Patches ¶ added in v0.1.8
func (e *Engine) Patches() []PatchGroup
Patches returns a snapshot of the engine's active patch groups.
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.
func (*Engine) TalkgroupForDevice ¶ added in v0.1.9
TalkgroupForDevice returns the talkgroup of the active call bound to deviceSerial, or nil when no call is active on that device. The live audio path uses it to honour the per-talkgroup Mute flag. Safe to call from any goroutine.
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 reaps it. Default 30 s. The end reason depends on
// whether the call ever decoded frames: EndReasonNormal when
// frames arrived and the carrier later dropped (P25's natural
// end-of-call mechanism, since the CC has no explicit channel
// release); EndReasonTimeout when no frames ever arrived (silent
// decode failure).
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 FrequencyChecker ¶ added in v0.2.4
FrequencyChecker is implemented by Tuners that can serve only a limited range of centre frequencies — e.g. a virtual voice tuner backed by a wideband DDC tap can only follow grants inside the wideband dongle's IQ window. FindFreeForFrequency consults this interface to skip incapable tuners; physical SDRs that don't implement it are treated as universally tunable.
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
// AlgorithmID and KeyID carry the encryption parameters the
// protocol's privacy header advertises (the DMR PI header, etc.).
// They are meaningful only when Encrypted is true and stay zero
// until a privacy header has been parsed. Persisted to the call
// log so an operator can see which key a recorded call needs.
AlgorithmID uint8
KeyID uint16
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
// PatchedGroups, when non-empty, lists the member talkgroups of a
// patch / dynamic-regroup super-group: the call on GroupID is
// physically the shared traffic of these groups. The engine fills
// it from its PatchRegistry so the call can be attributed to every
// member. Empty for an ordinary (non-patched) grant.
PatchedGroups []uint32
// P25Phase1DemodMode mirrors the system-level
// trunking.System.P25Phase1DemodMode setting so the voice composer
// can pick the matching symbol-recovery path on grants for the
// system (C4FM vs CQPSK / LSM). The control-channel decoder
// already honours the setting via the ccdecoder connector; without
// this field every voice grant landed in a hardcoded C4FM voice
// receiver and never decoded on LSM-modulated simulcast sites
// (issue #356 follow-up). Populated by the protocol layer when it
// publishes the grant; ignored for non-P25-Phase-1 grants.
P25Phase1DemodMode string
// P25Phase2Decode carries the per-channel FEC parameters the voice
// composer's Phase 2 chain needs to decode MAC PDUs that
// interleave with voice subframes on the traffic channel (talker
// alias, in-call signalling). Populated by the Phase 2 control
// channel when publishing the grant; zero on non-Phase-2 grants.
P25Phase2Decode P25Phase2Decode
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 Location ¶ added in v0.1.9
type Location struct {
System string // trunking system name
Protocol string // "p25", "dmr", ...
RadioID uint32 // reporting subscriber unit
Talkgroup uint32 // associated talkgroup; 0 when not call-associated
Latitude float64 // decimal degrees, positive north
Longitude float64 // decimal degrees, positive east
SpeedKnots float64 // 0 when not reported
HeadingDeg float64 // 0 when not reported
At time.Time
}
Location is the payload of an events.KindLocation event: a geographic fix a subscriber unit reported over the air. P25 Motorola Unit GPS, P25 L3Harris Talker GPS, and DMR LRRP all ultimately resolve to one of these. Latitude is positive north, Longitude positive east, both in decimal degrees.
The storage layer persists every Location to the location_log table and the API surfaces recent fixes for map display.
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 P25BandPlanEntry ¶ added in v0.2.2
type P25BandPlanEntry struct {
ChannelID uint8 // 0..15; matches the 4-bit IDEN_UP slot index
BaseHz uint64 // downlink base frequency for channel 0, Hz
SpacingHz uint32 // channel-to-channel step, Hz
TxOffsetHz int64 // signed uplink offset (uplink = downlink + offset), Hz
BandwidthHz uint32 // informational; not consulted by BandPlan.Frequency
}
P25BandPlanEntry is one operator-supplied IDEN_UP slot seed for the P25 Phase 1 receiver — mirrors the on-air phase1.IdentifierUpdate fields without the trunking package having to import phase1. The pipeline factory translates each entry into the receiver's runtime shape and calls BandPlan.Apply before the decoder runs.
type P25Phase2Decode ¶ added in v0.2.5
P25Phase2Decode is the protocol-neutral mirror of p25/phase2.MACDecodeConfig: primitive fields so this struct lives in the trunking package without pulling a Phase 2 import. The composer translates back to phase2.MACDecodeConfig at use time.
Trellis / RS / Interleave / Scrambler are numerically aligned to the phase2 enum constants of the same name (TrellisOff = 0, TrellisOn = 1, etc.). The composer round-trips them by casting.
type Patch ¶ added in v0.1.8
type Patch struct {
System string
Protocol string
SuperGroup uint32
Members []uint32
Vendor string
Add bool // true = patch now active, false = patch cancelled
At time.Time
}
Patch is the events.KindPatch payload — a patch add or cancel a trunked system announced.
type PatchGroup ¶ added in v0.1.8
type PatchGroup struct {
SuperGroup uint32
Members []uint32
Vendor string // "motorola" | "harris"
UpdatedAt time.Time
}
PatchGroup associates a P25 super-group / dynamic-regroup talkgroup with the member talkgroups merged into it. A patch makes the member groups share one RF channel, so a call on the super-group physically IS the members' traffic — "following" a patch means attributing the call to every member, not retuning.
type PatchRegistry ¶ added in v0.1.8
type PatchRegistry struct {
// contains filtered or unexported fields
}
PatchRegistry is a thread-safe live table of active patch groups keyed by super-group. The engine maintains one and consults it when dispatching grants so a call on a patched super-group is attributed to its member talkgroups.
func NewPatchRegistry ¶ added in v0.1.8
func NewPatchRegistry() *PatchRegistry
NewPatchRegistry returns an empty registry.
func (*PatchRegistry) Active ¶ added in v0.1.8
func (r *PatchRegistry) Active() []PatchGroup
Active returns a snapshot of every active patch group.
func (*PatchRegistry) Apply ¶ added in v0.1.8
func (r *PatchRegistry) Apply(pg PatchGroup)
Apply records (or replaces) a patch group.
func (*PatchRegistry) Delete ¶ added in v0.1.8
func (r *PatchRegistry) Delete(superGroup uint32)
Delete removes a patch group by its super-group address.
func (*PatchRegistry) MembersOf ¶ added in v0.1.8
func (r *PatchRegistry) MembersOf(group uint32) []uint32
MembersOf returns a copy of the member talkgroups of the patch keyed by group, or nil if group is not an active super-group.
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 RID ¶ added in v0.2.4
type RID struct {
ID uint32 `json:"id"`
Alias string `json:"alias"`
Description string `json:"description,omitempty"`
Tag string `json:"tag,omitempty"` // department / role
Group string `json:"group,omitempty"` // top-level grouping (agency)
Owner string `json:"owner,omitempty"` // operator/badge assigned to the radio
Priority int `json:"priority,omitempty"`
// Lockout marks the radio as stale / decommissioned / known-bad so
// the UI can de-emphasise it. RIDs are not gated like talkgroups
// today — Lockout is informational only.
Lockout bool `json:"lockout,omitempty"`
// Watch flags RIDs of operator interest so they can be surfaced in
// a watch-list UI. Defaults to true on every loader so a plain
// catalogue without a Watch column keeps every RID visible.
Watch bool `json:"watch"`
// Icon is an optional glyph identifier used by the operator UIs to
// render a per-RID icon.
Icon string `json:"icon,omitempty"`
}
RID describes one radio unit (subscriber unit identifier) loaded from disk. RIDs are the per-radio analogue of talkgroups — a way to give operator-meaningful names, owners, and grouping to the numeric SourceID that rides on every grant/affiliation/registration.
The schema intentionally mirrors TalkGroup's fields where it makes sense (Alias↔AlphaTag, Tag, Group, Priority, Lockout, Icon) so the CSV/JSON loaders share the same conventions, and an operator who already maintains a talkgroup catalog can drop a parallel RID file next to it.
type RIDDB ¶ added in v0.2.4
type RIDDB struct {
// contains filtered or unexported fields
}
RIDDB is a thread-safe lookup over loaded RIDs.
func (*RIDDB) Delete ¶ added in v0.2.4
Delete removes the RID with the given id. Returns false if no such RID exists.
func (*RIDDB) LoadCSV ¶ added in v0.2.4
LoadCSV reads RIDs from a CSV. Required column: a numeric Decimal/DEC column with the radio ID. Optional columns (case-insensitive, matched by header): Alias / Alpha Tag, Description, Tag, Group, Owner, Priority, Lockout, Watch, Icon.
func (*RIDDB) LoadCSVFile ¶ added in v0.2.4
LoadCSVFile is a thin wrapper over LoadCSV for a path on disk.
func (*RIDDB) LoadJSON ¶ added in v0.2.4
LoadJSON reads a JSON array of RID records. Records missing the "watch" key resolve to Watch=true so legacy JSON dumps keep every RID visible; explicit `"watch": false` opts a record out of the watch list.
func (*RIDDB) LoadJSONFile ¶ added in v0.2.4
LoadJSONFile is a thin wrapper over LoadJSON for a path on disk.
type ReacquireFunc ¶ added in v0.2.2
ReacquireFunc asks the SDR pool to re-open the device with the given serial and return its fresh Tuner handle. Implementations (typically the daemon's bridge to sdr.Pool.Reacquire) close the stale handle, re-enumerate the driver, open the matching serial, re-apply per-device tuning, and swap the entry in place — see sdr.Pool.Reacquire for the contract.
type RegistrationResponse ¶ added in v0.1.7
type RegistrationResponse uint8
RegistrationResponse encodes the P25 Unit Registration Response value (TIA-102.AABF Table 7-43). The integer values are wire constants — do not renumber.
const ( RegistrationAccepted RegistrationResponse = 0 RegistrationFailed RegistrationResponse = 1 RegistrationDenied RegistrationResponse = 2 RegistrationRefused RegistrationResponse = 3 )
func (RegistrationResponse) String ¶ added in v0.1.7
func (r RegistrationResponse) String() string
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
// P25BandPlan seeds the Phase 1 receiver's BandPlan with operator-
// supplied IdentifierUpdate entries before the decoder runs.
// Useful for sites that don't broadcast an IDEN_UP TSBK for every
// channel ID their grants reference (issue #345 — Mt Anakie
// broadcasts grants on id=10 but never the matching IDEN_UP, so
// the receiver has nothing to resolve the frequency against).
// Over-the-air IDEN_UPs naturally override these via the same
// BandPlan.Apply path; entries here are the floor, not the ceiling.
// Ignored for non-P25-Phase-1 protocols.
P25BandPlan []P25BandPlanEntry
// P25Phase1DemodMode selects the symbol-recovery path for the
// P25 Phase 1 receiver. Recognised values (case-insensitive):
// "" / "c4fm" / "fm" → DemodC4FM (the default — FM
// discriminator + 4-level slicer; matches every previously
// shipping config and works on conventional non-simulcast P25
// transmitters); "cqpsk" / "lsm" / "linear" → DemodCQPSK (the
// linear / LSM path — complex RRC + Gardner + differential
// QPSK; required for simulcast P25 deployments whose control
// channel transmits Linear Simulcast Modulation rather than
// straight C4FM, see issue #275 and TIA-102.BAAA). Forwarded
// into p25phase1rx.Options.DemodMode by the ccdecoder connector
// after parsing via p25phase1rx.ParseDemodMode.
P25Phase1DemodMode 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
// P25Phase2InterleaveMode enables the TIA-102.BBAC per-burst block
// deinterleaver applied to the MAC-burst dibits before trellis
// decoding. Recognised values (case-insensitive): "" / "off" /
// "false" / "0" → InterleaveOff (the default); "on" / "true" / "1"
// → InterleaveOn. Forwarded into p25phase2.ControlChannel.
// SetInterleaveMode by the ccdecoder connector after parsing via
// p25phase2.ParseInterleaveMode.
P25Phase2InterleaveMode 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): "" / "on" / "true" /
// "1" → ScramblerOn (the default — live Phase 2 traffic is
// always scrambled); "off" / "false" / "0" → ScramblerOff
// (opt-out for unscrambled fixtures). 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
// NXDNDeviationHz overrides the peak frequency deviation (Hz)
// the NXDN receiver's slicer is calibrated against. Spec value
// is 1800 Hz (matches the FM-discriminator output level so live
// captures slice correctly out of the box). Operators with
// on-air captures whose dibit distribution is bimodal (outer
// ±3 dominate, inner ±1 underrepresented) can override here —
// see samples/nxdn/README.md for the calibration recipe.
// Forwarded into nxdnrx.Options.DeviationHz by the ccdecoder
// connector; values <= 0 fall back to the spec default.
NXDNDeviationHz float64
// 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"`
// Stream gates whether completed calls on this talkgroup are
// uploaded to the outbound broadcast aggregators
// (internal/broadcast). Defaults to true on every loader so a
// legacy CSV/JSON without a Stream column keeps streaming every
// call; an explicit "no"/"false" in the CSV (or `"stream": false`
// in JSON) opts a sensitive talkgroup out of all feeds.
Stream bool `json:"stream"`
// Record gates whether calls on this talkgroup are written to
// disk by the per-call WAV recorder. Defaults to true on every
// loader; an explicit "no"/"false" excludes a talkgroup from
// recording (the call is still followed and played live).
Record bool `json:"record"`
// Mute, when true, suppresses this talkgroup's calls from the
// live audio player — the call is still followed, recorded, and
// streamed, just not played on the host's speakers. Defaults to
// false (not muted).
Mute bool `json:"mute,omitempty"`
// Icon is an optional icon name assigned to this talkgroup, used
// by the operator UIs to render a per-talkgroup glyph (the data
// model behind SDRtrunk's Icon Manager). Empty = default icon.
Icon string `json:"icon,omitempty"`
}
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 TalkerAlias ¶ added in v0.1.8
type TalkerAlias struct {
System string // System name, matches trunking.System.Name
Protocol string // "p25-phase2"
SourceID uint32 // radio unit the alias names
Alias string // the reassembled display name
At time.Time
}
TalkerAlias is published on the events bus when a radio's display name (the "talker alias") has been fully reassembled from the multi-fragment vendor MAC PDUs that carry it. Emitted by the P25 Phase 2 decoder. Keyed by SourceID so a consumer can associate it with the active call whose Grant.SourceID matches.
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 UnitActivity ¶ added in v0.1.9
type UnitActivity struct {
RadioID uint32 `json:"radio_id"`
Talkgroup uint32 `json:"talkgroup"`
System string `json:"system"`
Protocol string `json:"protocol"`
// Explicit is true when the association came from a decoded
// affiliation message (P25 Group Affiliation Response); false when
// it was observed from a voice-channel grant naming the unit as
// the source. Both are ground truth — a grant proves the unit is
// transmitting on that talkgroup — but the distinction is useful
// in the UI.
Explicit bool `json:"explicit"`
// Registered is true once the unit has been seen in a unit
// registration message.
Registered bool `json:"registered"`
FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"`
// CallCount counts grants observed for this unit since it entered
// the table — a simple "recurring radio" signal for the RID list
// (issue #376).
CallCount uint64 `json:"call_count"`
// TalkerAlias is the most-recently-decoded over-the-air talker
// alias for this unit (the radio's own display name), distinct
// from any operator-assigned alias in the RIDDB.
TalkerAlias string `json:"talker_alias,omitempty"`
TalkerAliasAt time.Time `json:"talker_alias_at,omitempty"`
}
UnitActivity records the last-observed talkgroup activity of one radio unit. It is the row type of the affiliation tracker's snapshot — the "who is on which talkgroup" view SDRtrunk surfaces.
type UnitRegistration ¶ added in v0.1.7
type UnitRegistration struct {
System string // System name, matches trunking.System.Name
Protocol string // "p25" / "dmr" / "nxdn"
SourceID uint32 // radio unit's WUID
WACN uint32 // 20-bit Wide Area Communications Network ID
SystemID uint16 // 12-bit system identifier
Response RegistrationResponse // accepted / failed / denied / refused
At time.Time
}
UnitRegistration is published on the events bus when a radio unit registers (or attempts to register) on a site. Emitted by P25 control-channel decoders on opcode 0x2C (Unit Registration Response).
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. When SetReacquire is wired, a SetCenterFreq failure triggers one reacquire attempt against the SDR pool — the stale handle is swapped for a fresh one and the tune is retried. Recovers from a USB disconnect that happened while the device was idle between calls (issue #345).
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) FindFreeForFrequency ¶ added in v0.2.4
func (p *VoicePool) FindFreeForFrequency(hz uint32) *VoiceDevice
FindFreeForFrequency returns the first free device whose Tuner either doesn't implement FrequencyChecker (physical SDR — accepted unconditionally) or reports CanTune(hz)=true (virtual tuner whose wideband window covers the target). Order matches the device list, so the daemon's preference (physical voice SDRs first, virtual taps after) is preserved. Returns nil when every free device rejects the target — the engine then falls back to preemption.
func (*VoicePool) HasCapableDevice ¶ added in v0.2.6
HasCapableDevice reports whether any device in the pool — busy or free — can tune hz. A device with no FrequencyChecker (physical SDR) counts as universally capable. The engine uses this to tell a coverage gap (e.g. every voice device is a wideband tap whose IQ window excludes hz) apart from a genuine all-busy or empty-pool condition, so an out-of-window grant isn't mislogged as an engine bug.
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) LowestPriorityActiveForFrequency ¶ added in v0.2.6
func (p *VoicePool) LowestPriorityActiveForFrequency(hz uint32) *ActiveCall
LowestPriorityActiveForFrequency returns the lowest-priority active call among devices that can tune hz, or nil when no such device has an active call. It mirrors LowestPriorityActive but skips devices whose Tuner rejects hz — preempting one of those would end a call to free a device that then can't bind the incoming grant. Physical SDRs (no FrequencyChecker) are always eligible, so for a pool with no frequency-constrained tuners this matches LowestPriorityActive.
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).
func (*VoicePool) SetReacquire ¶ added in v0.2.2
func (p *VoicePool) SetReacquire(fn ReacquireFunc)
SetReacquire installs the SDR-pool reacquire callback. After this is set, Bind retries SetCenterFreq once via the callback when the initial tune fails — recovering from a USB disconnect / re- enumerate without dropping the call. Idempotent; passing nil disables the retry (matches the legacy behaviour).
func (*VoicePool) Touch ¶
Touch updates the LastHeardAt timestamp for the given device. The engine watchdog uses this to detect calls that have ended without an explicit release announcement.
func (*VoicePool) UpdateEncryption ¶ added in v0.2.2
UpdateEncryption backfills ALGID/KID on the active call bound to serial — used by the engine when an in-call Encryption Sync arrives after the original grant (P25 Phase 1 LDU2). Returns a copy of the updated Grant for the caller to publish in an enriched event, plus ok=true when a matching call was found. The mutation runs under the pool's mutex so it stays consistent with concurrent Touch / Release.
func (*VoicePool) UpdateSource ¶ added in v0.2.5
UpdateSource backfills SourceID + Encrypted on the active call bound to serial — used by the engine when an in-call GROUP_VOICE_CHANNEL_USER PDU arrives on the traffic channel after a compressed grant whose SOURCE_ID / SVC_OPTIONS were absent (e.g. P25 Phase 2 MMR). SourceID is only overwritten when the new value is non-zero so a later compressed-form update doesn't blank out a legitimate source. Encrypted is OR-merged so an in-call PDU can flip a non-encrypted grant to encrypted but never the other way (the spec doesn't define mid-call decryption). Returns a copy of the updated Grant + ok=true when a matching call was found.