trunking

package
v0.1.8 Latest Latest
Warning

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

Go to latest
Published: May 21, 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 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 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 CallStart

type CallStart struct {
	Grant        Grant
	Talkgroup    *TalkGroup // resolved via the engine's TalkgroupDB; nil if unknown
	DeviceSerial string     // which Voice SDR is following the call
	StartedAt    time.Time
}

CallStart is the payload of an events.KindCallStart event. The engine publishes this once a Voice device has been retuned to the grant's frequency; downstream pipelines (the demod composer, the recorder) subscribe and start consuming IQ.

type EndReason

type EndReason uint8

EndReason classifies why a call ended; carried in CallEnd events so the API layer can surface the cause to UIs.

const (
	EndReasonUnknown    EndReason = iota
	EndReasonNormal               // CC announced channel release / talk-off
	EndReasonTimeout              // engine watchdog fired (no recent activity)
	EndReasonPreempted            // higher-priority grant kicked us off
	EndReasonLockout              // talkgroup is locked out by policy
	EndReasonNoVoiceSDR           // every Voice-role SDR was busy
	EndReasonError
	EndReasonManual // operator ended the call via API / TUI
)

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) 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 ends it as EndReasonTimeout. Default 30 s.
	CallTimeout time.Duration
	// Now is injectable for tests; defaults to time.Now.
	Now func() time.Time
	// ScanMode controls whether HandleGrant respects the per-talkgroup
	// Scan flag. Default ScanModeAll keeps every non-locked-out grant
	// flowing through; ScanModeList enforces the talkgroup scan list.
	ScanMode ScanMode
}

EngineOptions configure a new Engine.

type Grant

type Grant struct {
	System      string // System name, matches trunking.System.Name
	Protocol    string // "p25" / "dmr" / "nxdn"
	GroupID     uint32 // talkgroup or destination subscriber address
	SourceID    uint32 // originator (subscriber unit)
	FrequencyHz uint32 // voice channel frequency
	ChannelID   uint8  // raw channel ID (P25 band-plan ID, DMR LCN high)
	ChannelNum  uint16 // raw channel number within the ID
	Encrypted   bool
	Emergency   bool
	// 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
	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 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 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 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

	// 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): "" / "off" / "false" /
	// "0" → ScramblerOff (the default — no PN44 descrambling);
	// "on" / "true" / "1" → ScramblerOn. The seed is computed from
	// (WACN, SystemID, low 12 bits of Site as the spec's Color
	// Code = NAC) per spec equation (5). Forwarded into
	// p25phase2.ControlChannel.SetScramblerMode +
	// SetScramblerSeed by the ccdecoder connector.
	P25Phase2ScramblerMode string
	// P25Phase2ClockMode selects the symbol-timing-recovery strategy
	// for the P25 Phase 2 receiver. Recognised values (case-
	// insensitive): "" / "gardner" / "on" → ClockGardner (the new
	// default — non-data-aided Gardner loop; recommended for live
	// SDR captures); "naive" / "off" → ClockNaive (decimate every
	// sps-th sample; works on sample-aligned synthesized IQ).
	// Forwarded into p25phase2rx.Options.ClockMode by the ccdecoder
	// connector after parsing via p25phase2rx.ParseClockMode.
	P25Phase2ClockMode string
	// TETRAClockMode mirrors P25Phase2ClockMode for the TETRA
	// receiver. Same recognised values + parser semantics; the
	// underlying ClockMode enums in the two receivers share the
	// same name + values but are independent types.
	TETRAClockMode string
	// NXDNViterbiMode enables the K=5 ½-rate Viterbi FEC decoder
	// on the NXDN CAC region. Recognised values (case-insensitive):
	// "" / "spec" → ViterbiSpec (the new default — full NXDN-TS-1-A
	// §4.5.1.1 outbound CAC chain); "on" / "true" / "1" → ViterbiOn
	// (intermediate 92-dibit K=5 Viterbi path for older
	// MMDVMHost / DSDcc fixtures); "off" / "false" / "0" → ViterbiOff
	// (legacy 44-dibit raw-CAC path, opt-out for pre-stripped
	// fixtures). Forwarded into nxdn.ControlChannel.SetViterbiMode
	// by the ccdecoder connector after parsing via
	// nxdn.ParseViterbiMode.
	NXDNViterbiMode string
	// 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"`
}

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 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.

func (*VoicePool) Devices

func (p *VoicePool) Devices() []*VoiceDevice

Devices returns a snapshot of the device list.

func (*VoicePool) FindFree

func (p *VoicePool) FindFree() *VoiceDevice

FindFree returns the first device with no active call, or nil if every device is busy. The pool lock is held only during the scan.

func (*VoicePool) LowestPriorityActive

func (p *VoicePool) LowestPriorityActive() *ActiveCall

LowestPriorityActive returns the active call with the lowest priority among all devices, or nil if no calls are active. Used by the engine when deciding which call to preempt.

func (*VoicePool) Release

func (p *VoicePool) Release(serial string) *ActiveCall

Release marks the device free. Returns the freed ActiveCall (or nil if the device wasn't busy).

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.

Jump to

Keyboard shortcuts

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