trunking

package
v0.3.0 Latest Latest
Warning

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

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

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

Constants

This section is empty.

Variables

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

func CanPreempt(active Grant, activeTG *TalkGroup, incoming Grant, incomingTG *TalkGroup) bool

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

func EffectivePriority(g Grant, tg *TalkGroup) int

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

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

func OpenCache(path string) (*Cache, error)

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.

func (*Cache) Names

func (c *Cache) Names() []string

Names returns the system names currently in the cache, sorted for deterministic iteration.

func (*Cache) Set

func (c *Cache) Set(name string, entry CachedSystem) error

Set updates the cached entry for name and persists the file. The write is atomic via rename.

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.

func (CallEnd) Duration

func (c CallEnd) Duration() time.Duration

Duration returns how long the call ran.

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
)

func (EndReason) String

func (r EndReason) String() string

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

func (e *Engine) EndCall(deviceSerial string, reason EndReason) bool

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

func (e *Engine) EndSyntheticCall(deviceSerial string, reason EndReason) bool

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

func (e *Engine) HandleGrant(g Grant)

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

func (e *Engine) HandleSyntheticCall(g Grant, deviceSerial string)

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

func (e *Engine) Run(ctx context.Context) error

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

func (e *Engine) ScanMode() ScanMode

ScanMode returns the engine's current scan mode. Safe to call from any goroutine.

func (*Engine) SetScanMode

func (e *Engine) SetScanMode(m ScanMode) ScanMode

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

func (e *Engine) TalkgroupForDevice(deviceSerial string) *TalkGroup

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.

func (*Engine) Touch

func (e *Engine) Touch(deviceSerial string)

Touch refreshes the LastHeardAt timestamp on the active call bound to deviceSerial. The protocol decoder calls this every time it sees voice activity on the followed frequency so the watchdog doesn't time it out.

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

type FrequencyChecker interface {
	CanTune(hz uint32) bool
}

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.

func (Grant) String

func (g Grant) String() string

String renders a one-line summary of a Grant for log output.

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

type LockResult struct {
	System    string
	Frequency uint32
	NAC       uint16
	At        time.Time
}

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

type LockedPayload interface {
	LockedFrequencyHz() uint32
	LockedNAC() uint16
}

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

type P25Phase2Decode struct {
	Trellis    uint8
	RS         uint8
	Interleave uint8
	Scrambler  uint8
	Seed       uint64
}

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

func ParseProtocol(s string) (Protocol, error)

ParseProtocol maps a string ("p25", "dmr", "nxdn", "dpmr", "edacs", "motorola", "ltr", "mpt1327", "p25-phase2", "tetra") to a Protocol value.

func (Protocol) String

func (p Protocol) String() string

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 NewRIDDB added in v0.2.4

func NewRIDDB() *RIDDB

NewRIDDB returns an empty DB.

func (*RIDDB) Add added in v0.2.4

func (d *RIDDB) Add(r *RID)

Add or replace a single RID record.

func (*RIDDB) All added in v0.2.4

func (d *RIDDB) All() []*RID

All returns a snapshot of every RID in the DB.

func (*RIDDB) Delete added in v0.2.4

func (d *RIDDB) Delete(id uint32) bool

Delete removes the RID with the given id. Returns false if no such RID exists.

func (*RIDDB) Len added in v0.2.4

func (d *RIDDB) Len() int

Len returns the number of loaded RIDs.

func (*RIDDB) LoadCSV added in v0.2.4

func (d *RIDDB) LoadCSV(r io.Reader) (int, error)

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

func (d *RIDDB) LoadCSVFile(path string) (int, error)

LoadCSVFile is a thin wrapper over LoadCSV for a path on disk.

func (*RIDDB) LoadJSON added in v0.2.4

func (d *RIDDB) LoadJSON(r io.Reader) (int, error)

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

func (d *RIDDB) LoadJSONFile(path string) (int, error)

LoadJSONFile is a thin wrapper over LoadJSON for a path on disk.

func (*RIDDB) Lookup added in v0.2.4

func (d *RIDDB) Lookup(id uint32) *RID

Lookup returns the RID record for id, or nil if unknown.

func (*RIDDB) UpdateFields added in v0.2.4

func (d *RIDDB) UpdateFields(id uint32, fn func(*RID)) bool

UpdateFields applies fn to the RID with the given id under the write lock. Returns false if no such RID exists.

type ReacquireFunc added in v0.2.2

type ReacquireFunc func(serial string) (Tuner, error)

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.
const (
	ScanModeAll ScanMode = iota
	ScanModeList
)

func ParseScanMode

func ParseScanMode(s string) ScanMode

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.

func (ScanMode) String

func (m ScanMode) String() string

String renders the wire form used by config + REST + TUI clients.

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.

func (System) HuntOrder

func (s System) HuntOrder(lastKnown uint32) []uint32

HuntOrder returns the candidate frequency list with `lastKnown` (if non-zero and present in ControlChannels) moved to the front. This biases the hunter toward the most-recently-locked CC, falling back to the configured order.

func (System) Validate

func (s System) Validate() error

Validate returns an error if the System lacks required fields.

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 NewTalkgroupDB

func NewTalkgroupDB() *TalkgroupDB

NewTalkgroupDB returns an empty DB.

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

type Tuner interface {
	SetCenterFreq(hz uint32) error
}

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

type VoiceDevice struct {
	Tuner  Tuner
	Serial string
}

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

func (p *VoicePool) HasCapableDevice(hz uint32) bool

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

func (p *VoicePool) Touch(serial string, now time.Time)

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

func (p *VoicePool) UpdateEncryption(serial string, algID uint8, keyID uint16) (Grant, bool)

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

func (p *VoicePool) UpdateSource(serial string, sourceID uint32, encrypted bool) (Grant, bool)

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.

Jump to

Keyboard shortcuts

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