Documentation
¶
Overview ¶
Package api exposes GopherTrunk's read + write control surface, the streaming events feed, and the gRPC mirror of the same state.
The default daemon links both the HTTP+SSE+WebSocket server defined here and the gRPC server in grpc.go. Mutation endpoints (end call, set talkgroup priority/lockout/scan, retention sweep, tone-out reset, scanner cockpit) are gated behind api.allow_mutations so a daemon bound to a trusted interface can expose them while a default build stays read-only.
gRPC bindings (proto/*.proto under the repo root) generate Go code at internal/api/pb/v1 when `make proto` is invoked with protoc and the standard plugins installed.
Layout:
server.go HTTP server lifecycle (Run, Close), routing, mux handlers.go REST read handlers (health/version/systems/talkgroups/calls/devices) handlers_mutations.go REST mutation handlers (end-call, retention, talkgroup, tone-reset) handlers_scanner.go Scanner cockpit REST handlers (status + 6 mutation routes) sse.go Server-Sent Events stream of internal/events bus events ws.go WebSocket bridge that streams the same events as JSON grpc.go gRPC server: SystemService + TalkgroupService + AudioService types.go JSON-friendly DTOs (mirroring the proto definitions)
Index ¶
- type ActiveCallDTO
- type AudioController
- type AudioPublisher
- func (p *AudioPublisher) Close() error
- func (p *AudioPublisher) Run(ctx context.Context) error
- func (p *AudioPublisher) Stats() AudioPublisherStats
- func (p *AudioPublisher) Subscribe(filter AudioSubFilter) *audioSubscriber
- func (p *AudioPublisher) Unsubscribe(sub *audioSubscriber)
- func (p *AudioPublisher) WritePCM(deviceSerial string, samples []int16) error
- type AudioPublisherStats
- type AudioStatusDTO
- type AudioSubFilter
- type AuthConfig
- type AuthMode
- type CORSConfig
- type CallEndDTO
- type CallRow
- type CallStartDTO
- type ConvChannelStatusDTO
- type ConvScannerStatusDTO
- type DevicesProvider
- type EngineMutator
- type EngineSnapshot
- type EventDTO
- type GRPCServer
- func (g *GRPCServer) GetSystem(_ context.Context, req *apiv1.GetSystemRequest) (*apiv1.GetSystemResponse, error)
- func (g *GRPCServer) GetTalkgroup(_ context.Context, req *apiv1.GetTalkgroupRequest) (*apiv1.GetTalkgroupResponse, error)
- func (g *GRPCServer) ListActiveCalls(_ context.Context, _ *apiv1.ListActiveCallsRequest) (*apiv1.ListActiveCallsResponse, error)
- func (g *GRPCServer) ListSystems(_ context.Context, _ *apiv1.ListSystemsRequest) (*apiv1.ListSystemsResponse, error)
- func (g *GRPCServer) ListTalkgroups(_ context.Context, _ *apiv1.ListTalkgroupsRequest) (*apiv1.ListTalkgroupsResponse, error)
- func (g *GRPCServer) Run(ctx context.Context) error
- func (g *GRPCServer) Stop()
- func (g *GRPCServer) StreamAudio(req *apiv1.StreamAudioRequest, srv apiv1.AudioService_StreamAudioServer) error
- type GRPCServerOptions
- type GrantDTO
- type HealthDTO
- type HistoryFilter
- type HistoryQuery
- type ManualTuneRequest
- type RetentionSweeper
- type RuntimeDTO
- type RuntimeProvider
- type ScannerCockpit
- type ScannerStatus
- type Server
- type ServerOptions
- type SystemDTO
- type SystemHuntStatusDTO
- type TalkgroupDTO
- type ToneDetectorReset
- type ToneProfileDTO
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ActiveCallDTO ¶
type ActiveCallDTO struct {
Grant GrantDTO `json:"grant"`
Talkgroup *TalkgroupDTO `json:"talkgroup,omitempty"`
DeviceSerial string `json:"device_serial"`
StartedAt time.Time `json:"started_at"`
LastHeardAt time.Time `json:"last_heard_at"`
}
ActiveCallDTO mirrors trunking.ActiveCall for JSON.
type AudioController ¶
type AudioController interface {
// Volume returns the current software gain (0..1).
Volume() float32
// SetVolume clamps to 0..1 and applies immediately.
SetVolume(v float32)
// Muted reports the mute state.
Muted() bool
// SetMuted toggles mute. Mute is a software-gain bypass, not a
// device-level operation — toggling is instant.
SetMuted(m bool)
// RecordingEnabled reports whether the recorder's "create new
// sessions" gate is open. In-flight sessions are not affected
// by this gate.
RecordingEnabled() bool
// SetRecordingEnabled flips the recorder gate. False stops new
// WAVs from landing on disk; in-flight sessions complete.
SetRecordingEnabled(enabled bool)
// DropsTotal is a monotonically increasing counter of PCM
// samples lost because the playback queue was full. Surfaced
// so operators can spot scheduling-jitter problems from the
// TUI without reaching for /metrics.
DropsTotal() uint64
// SampleRate is the host playback rate the player was opened
// at, in Hz. Read-only; reopening the device with a different
// rate requires a daemon restart.
SampleRate() uint32
// BackendEnabled reports whether a real audio backend is
// attached. False means audio.enabled was off in config or the
// backend failed to init, and writes are silently dropped.
BackendEnabled() bool
}
AudioController is the API surface for the live-audio subsystem (the voice.Player sink + the WAV recorder gate). All four methods are safe to call from any goroutine; the daemon supplies a single adapter that fans into player.Player + voice.Recorder, tests use a fake.
type AudioPublisher ¶
type AudioPublisher struct {
// contains filtered or unexported fields
}
AudioPublisher is the runtime fan-out point between the per-call composer (which produces PCM) and any number of gRPC StreamAudio subscribers (which consume frames over the wire). It satisfies the same WritePCM contract the recorder + player + tone-out detector implement, so the daemon drops it straight into the existing composer.PCMSink fan-out.
The publisher subscribes to the events bus at construction time to keep a per-device-serial Grant map alive — that's how the published AudioFrame can carry talkgroup / system context the subscriber filters against. Slow subscribers don't block fast ones (or the composer): each subscriber has a bounded channel and we drop on full, counting the loss for visibility.
Lifecycle: NewAudioPublisher → Run (subscribes + drains bus until ctx cancels) → Close (releases bus subscription). The daemon spawns Run on a goroutine like every other long-lived component.
func NewAudioPublisher ¶
NewAudioPublisher constructs a publisher backed by the supplied bus. The bus subscription happens at construction time so callers can publish CallStart events before Run begins without losing them.
func (*AudioPublisher) Close ¶
func (p *AudioPublisher) Close() error
Close releases the bus subscription. Safe to call multiple times.
func (*AudioPublisher) Run ¶
func (p *AudioPublisher) Run(ctx context.Context) error
Run drains bus events until ctx cancels, maintaining the per- device-serial Grant map that WritePCM consults. Returns ctx.Err() on shutdown.
func (*AudioPublisher) Stats ¶
func (p *AudioPublisher) Stats() AudioPublisherStats
func (*AudioPublisher) Subscribe ¶
func (p *AudioPublisher) Subscribe(filter AudioSubFilter) *audioSubscriber
Subscribe registers a new subscriber and returns its frame channel. Caller MUST call Unsubscribe(ret) before letting the channel go out of scope — leaked subscribers keep the publisher fanning frames into them forever. Channel capacity defaults to 64 frames (≈ 1 second of audio at typical chunk sizes).
func (*AudioPublisher) Unsubscribe ¶
func (p *AudioPublisher) Unsubscribe(sub *audioSubscriber)
Unsubscribe removes the subscriber. Idempotent. After unsubscribing the channel is closed so any reader sees io.EOF / channel-closed.
func (*AudioPublisher) WritePCM ¶
func (p *AudioPublisher) WritePCM(deviceSerial string, samples []int16) error
WritePCM satisfies composer.PCMSink. Builds one AudioFrame per call and fans it to every subscriber whose filter matches. A missing Grant (composer wrote PCM before CallStart landed) drops the frame silently — the publisher only emits frames that carry full talkgroup context.
type AudioPublisherStats ¶
Stats reports cumulative publisher counters. Useful for the /metrics surface and for diagnosing slow consumers.
type AudioStatusDTO ¶
type AudioStatusDTO struct {
// BackendEnabled is true when a real audio sink is attached.
// False = audio.enabled was off in config or the backend
// failed to init; PATCH still works but takes effect only on
// the recorder gate.
BackendEnabled bool `json:"backend_enabled"`
// SampleRate is the host playback rate in Hz.
SampleRate uint32 `json:"sample_rate"`
// Volume is the software gain (0..1).
Volume float32 `json:"volume"`
// Muted reports the mute state.
Muted bool `json:"muted"`
// RecordingEnabled is the recorder's "create new sessions"
// gate. In-flight sessions are unaffected.
RecordingEnabled bool `json:"recording_enabled"`
// DropsTotal is a monotonically increasing counter of PCM
// samples lost because the playback queue was full.
DropsTotal uint64 `json:"drops_total"`
}
AudioStatusDTO is the JSON shape returned by GET /api/v1/audio. Mirrors the AudioController interface so the TUI doesn't need to know how the daemon plumbed the player + recorder together.
type AudioSubFilter ¶
type AudioSubFilter struct {
DeviceSerials []string
TalkgroupIDs []uint32
// IncludeRaw mirrors the proto flag. Until WriteRawFrame is
// wired into the publisher (digital-voice raw frames are a
// follow-up), this just selects whether to surface PCM frames
// at all — false is the safe default that's never going to
// break a caller that didn't ask for audio.
IncludeRaw bool
}
AudioSubFilter is what callers pass to Subscribe to scope the frames they receive. Empty fields match everything.
type AuthConfig ¶
type AuthConfig struct {
// Mode picks the policy. See AuthMode for the trade-offs.
Mode AuthMode
// Token is the inline bearer token. Compared with
// crypto/subtle.ConstantTimeCompare. Prefer TokenFile so the
// token doesn't live in config.yaml — but inline is supported
// for ephemeral / test setups.
Token string
// TokenFile is a path to a file containing the bearer token
// (whitespace stripped). Read at startup; the daemon reloads it
// on every request so operators can rotate tokens without a
// restart. Empty disables file-based tokens.
TokenFile string
// TrustedNetworks is a list of CIDRs whose source addresses
// bypass the bearer-token check under AuthModeAuto. Loopback
// (127.0.0.1/32 and ::1/128) is implicitly trusted under
// AuthModeAuto and does not need to be listed here.
TrustedNetworks []string
}
AuthConfig configures the bearer-token auth middleware.
type AuthMode ¶
type AuthMode uint8
AuthMode selects the auth policy applied to mutation endpoints.
AuthModeAuto (default): the policy depends on the listener binding. Loopback (127.0.0.1 / ::1) and any address in AuthConfig.TrustedNetworks bypass the bearer-token check — peer-cred via kernel-enforced reachability is treated as a reasonable trust proxy on a single-host operator's box. Anything else (0.0.0.0 / a public interface) requires a valid Bearer token on every mutation request, and the daemon refuses to start without a configured token.
AuthModeRequired: every mutation request must carry a valid Bearer token regardless of source, even loopback. Useful when the daemon shares a host with untrusted users.
AuthModeDisabled: bypass the bearer check entirely (the legacy `allow_mutations: true` behaviour). Mutations are wide open — for backwards-compatible single-host workflows where the operator is the only one with shell access. The daemon logs a warning at startup so this isn't accidentally enabled in a hostile environment.
func ParseAuthMode ¶
ParseAuthMode maps a config string into an AuthMode. Recognised values (case-insensitive): "" / "auto" → AuthModeAuto (the default); "required" / "on" / "true" → AuthModeRequired; "disabled" / "off" / "false" → AuthModeDisabled. Unknown strings return AuthModeAuto with ok=false.
type CORSConfig ¶ added in v0.1.3
type CORSConfig struct {
AllowedOrigins []string
}
CORSConfig configures the cross-origin middleware. AllowedOrigins is the exact list of values the daemon will echo back in Access-Control-Allow-Origin. The special value "*" matches any origin. The literal "null" matches the Origin header browsers send for file:// loads.
When AllowedOrigins is empty the middleware is a no-op: no CORS headers are emitted and OPTIONS requests fall through to the mux.
type CallEndDTO ¶
type CallRow ¶
type CallRow struct {
ID int64 `json:"id"`
System string `json:"system"`
Protocol string `json:"protocol"`
GroupID uint32 `json:"group_id"`
SourceID uint32 `json:"source_id"`
FrequencyHz uint32 `json:"frequency_hz"`
Encrypted bool `json:"encrypted"`
Emergency bool `json:"emergency"`
DataCall bool `json:"data_call"`
DeviceSerial string `json:"device_serial"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at,omitempty"`
DurationMs int64 `json:"duration_ms,omitempty"`
EndReason string `json:"end_reason,omitempty"`
TalkgroupAlpha string `json:"talkgroup_alpha,omitempty"`
}
CallRow mirrors storage.CallRow as a JSON-friendly row. Lives in the api package so the storage package can stay free of API concerns.
type CallStartDTO ¶
type CallStartDTO struct {
Grant GrantDTO `json:"grant"`
Talkgroup *TalkgroupDTO `json:"talkgroup,omitempty"`
DeviceSerial string `json:"device_serial"`
StartedAt time.Time `json:"started_at"`
}
CallStartDTO / CallEndDTO mirror the trunking event payloads.
type ConvChannelStatusDTO ¶
type ConvChannelStatusDTO struct {
Index int `json:"index"`
Label string `json:"label"`
FrequencyHz uint32 `json:"frequency_hz"`
Mode string `json:"mode"`
Active bool `json:"active"`
LockedOut bool `json:"locked_out,omitempty"`
LastBreakAt time.Time `json:"last_break_at,omitempty"`
}
ConvChannelStatusDTO mirrors conventional.ChannelStatus.
type ConvScannerStatusDTO ¶
type ConvScannerStatusDTO struct {
Enabled bool `json:"enabled"`
State string `json:"state,omitempty"`
DeviceSerial string `json:"device_serial,omitempty"`
CursorIndex int `json:"cursor_index,omitempty"`
Channels []ConvChannelStatusDTO `json:"channels"`
}
ConvScannerStatusDTO is the conventional FM scanner's read shape.
type DevicesProvider ¶
DevicesProvider returns a snapshot of the SDR pool. The api package stays free of a hard dependency on internal/sdr's implementation details; the daemon supplies *sdr.Pool, tests supply a fake.
type EngineMutator ¶
EngineMutator is the optional write side of the engine. Daemons that have AllowMutations enabled supply a real engine; tests can inject a fake. When nil the end-call route returns 503.
type EngineSnapshot ¶
type EngineSnapshot interface {
ActiveCalls() []*trunking.ActiveCall
}
EngineSnapshot is the subset of trunking.Engine the API needs. Decoupling from the concrete type keeps the API testable with a fake engine.
type EventDTO ¶
type EventDTO struct {
Kind string `json:"kind"`
Timestamp time.Time `json:"timestamp"`
Payload any `json:"payload"`
}
EventDTO is the JSON envelope for every event streamed to clients. Kind matches the events.Kind constant; Payload is the kind-specific body (one of the *DTO types below). A separate envelope keeps the wire format easy to consume from JS / browser frontends.
type GRPCServer ¶
type GRPCServer struct {
apiv1.UnimplementedSystemServiceServer
apiv1.UnimplementedTalkgroupServiceServer
apiv1.UnimplementedAudioServiceServer
// contains filtered or unexported fields
}
GRPCServer hosts the gRPC SystemService + TalkgroupService against the same in-process state as the HTTP/SSE/WebSocket server.
AudioService.StreamAudio is registered but is a no-op until the demod pipeline composer (deferred) starts pushing PCM into a per-call channel. The streaming surface is in place so clients can call it without churning at the wire-protocol layer when audio lands.
func NewGRPCServer ¶
func NewGRPCServer(opts GRPCServerOptions) (*GRPCServer, error)
NewGRPCServer constructs the server but does not bind a listener.
func (*GRPCServer) GetSystem ¶
func (g *GRPCServer) GetSystem(_ context.Context, req *apiv1.GetSystemRequest) (*apiv1.GetSystemResponse, error)
func (*GRPCServer) GetTalkgroup ¶
func (g *GRPCServer) GetTalkgroup(_ context.Context, req *apiv1.GetTalkgroupRequest) (*apiv1.GetTalkgroupResponse, error)
func (*GRPCServer) ListActiveCalls ¶
func (g *GRPCServer) ListActiveCalls(_ context.Context, _ *apiv1.ListActiveCallsRequest) (*apiv1.ListActiveCallsResponse, error)
func (*GRPCServer) ListSystems ¶
func (g *GRPCServer) ListSystems(_ context.Context, _ *apiv1.ListSystemsRequest) (*apiv1.ListSystemsResponse, error)
func (*GRPCServer) ListTalkgroups ¶
func (g *GRPCServer) ListTalkgroups(_ context.Context, _ *apiv1.ListTalkgroupsRequest) (*apiv1.ListTalkgroupsResponse, error)
func (*GRPCServer) Run ¶
func (g *GRPCServer) Run(ctx context.Context) error
Run binds the listener and serves until ctx cancels.
func (*GRPCServer) StreamAudio ¶
func (g *GRPCServer) StreamAudio(req *apiv1.StreamAudioRequest, srv apiv1.AudioService_StreamAudioServer) error
--- AudioService --- StreamAudio fans decoded PCM from the per-call composer to the gRPC client. The request's device_serials / talkgroup_ids filters act as allow-lists; empty matches everything. PCM samples are 16-bit little-endian mono at the recorder's configured rate (typically 8 kHz).
Returns:
codes.Unavailable when the daemon was started without an audio publisher (no composer wired, audio off, or older configuration). nil on graceful client cancel. any send-side error from the gRPC stream — typically the caller hung up.
type GRPCServerOptions ¶
type GRPCServerOptions struct {
Addr string
Systems []trunking.System
Talkgroups *trunking.TalkgroupDB
Engine EngineSnapshot
// Audio is the optional AudioPublisher backing StreamAudio.
// When nil the RPC still registers (so clients don't churn
// at the wire-protocol layer if audio is configured off) but
// returns Unavailable rather than streaming frames.
Audio *AudioPublisher
Log *slog.Logger
// TLSCert and TLSKey, when both non-empty, switch the gRPC
// server to TLS using credentials.NewServerTLSFromFile. Same
// disk-loaded-once semantics as the HTTP server's TLS support.
// Leave both empty for plain TCP (default; appropriate for
// loopback / private-network deployments).
TLSCert string
TLSKey string
}
GRPCServerOptions configure a new GRPCServer.
type GrantDTO ¶
type GrantDTO struct {
System string `json:"system"`
Protocol string `json:"protocol"`
GroupID uint32 `json:"group_id"`
SourceID uint32 `json:"source_id"`
FrequencyHz uint32 `json:"frequency_hz"`
ChannelID uint8 `json:"channel_id,omitempty"`
ChannelNumber uint16 `json:"channel_number,omitempty"`
Encrypted bool `json:"encrypted,omitempty"`
Emergency bool `json:"emergency,omitempty"`
DataCall bool `json:"data_call,omitempty"`
}
GrantDTO mirrors trunking.Grant.
type HealthDTO ¶
type HealthDTO struct {
// Status is always "ok" for a serving daemon — present so old
// callers that only check `.status == "ok"` keep working.
Status string `json:"status"`
// Now is the daemon-side timestamp in UTC. Useful for detecting
// clock skew between probe and daemon.
Now time.Time `json:"now"`
// Version is the daemon build version, redundant with the
// dedicated /api/v1/version endpoint but useful so probes can
// confirm process identity in one round-trip.
Version string `json:"version,omitempty"`
// PoolAttachedCount is the number of currently-attached SDR
// devices. Zero means no Devices provider is wired OR every
// device has detached — both are operator-actionable signals.
PoolAttachedCount int `json:"pool_attached_count"`
// ActiveCalls is the count of in-flight voice calls.
ActiveCalls int `json:"active_calls"`
// DBConnected reports whether the call-history database is
// wired. A daemon configured without `db_path` legitimately
// runs with DBConnected = false.
DBConnected bool `json:"db_connected"`
// MetricsEnabled reports whether /metrics is mounted.
MetricsEnabled bool `json:"metrics_enabled"`
// AuthMode echoes the bearer-token auth policy
// ("auto" / "required" / "disabled") so probes can flag a
// misconfigured production deployment.
AuthMode string `json:"auth_mode,omitempty"`
}
HealthDTO is the body shape returned by GET /api/v1/health. The extended fields (every key beyond status + now) let k8s / Nomad readiness probes and operator dashboards distinguish "the daemon process is up" from "the daemon process is actually doing work". All fields are best-effort — missing collaborators (no SDR pool, no engine, no history DB) just leave the corresponding field at its zero value rather than failing the request.
type HistoryFilter ¶
type HistoryFilter struct {
System string
GroupID uint32
Since time.Time
Until time.Time
Limit int
OnlyEnded bool
}
HistoryFilter mirrors storage.HistoryFilter for the api layer's purposes (passed through to whatever HistoryQuery implementation the daemon wires up).
type HistoryQuery ¶
type HistoryQuery interface {
History(ctx context.Context, f HistoryFilter) ([]CallRow, error)
}
HistoryQuery is the subset of storage.DB the history endpoint needs. Decoupling keeps the api package free of a hard dependency on the storage package and lets tests inject fakes.
func HistoryFromStorage ¶
func HistoryFromStorage(db *storage.DB) HistoryQuery
HistoryFromStorage wraps a *storage.DB as an api.HistoryQuery so the daemon can pass it to NewServer without the api package's CallRow / HistoryFilter types leaking into the storage package.
type ManualTuneRequest ¶
type ManualTuneRequest struct {
FrequencyHz uint32 `json:"frequency_hz"`
Label string `json:"label"`
Mode string `json:"mode"`
SquelchDbFS float64 `json:"squelch_dbfs"`
HangtimeMs int `json:"hangtime_ms"`
}
ManualTuneRequest is the shape of POST /api/v1/scanner/manual_tune. FrequencyHz is required; everything else falls back to scanner defaults (Mode=fm, SquelchDbFS=-50, Hangtime=1500ms).
type RetentionSweeper ¶
RetentionSweeper is the optional write side of the retention system: kick off one ad-hoc sweep. The daemon supplies the real sweeper from internal/storage; tests can fake it.
type RuntimeDTO ¶
type RuntimeDTO struct {
// API listener addresses (empty when disabled).
HTTPAddr string `json:"http_addr,omitempty"`
GRPCAddr string `json:"grpc_addr,omitempty"`
WSPath string `json:"ws_path,omitempty"`
SSEPath string `json:"sse_path,omitempty"`
MetricsPath string `json:"metrics_path,omitempty"`
AllowMutations bool `json:"allow_mutations"`
// Daemon log + version.
LogLevel string `json:"log_level"`
LogFormat string `json:"log_format"`
Version string `json:"version,omitempty"`
// Storage paths (sanitised — paths only, never contents).
StorageDBPath string `json:"storage_db_path,omitempty"`
StorageCCCache string `json:"storage_cc_cache,omitempty"`
// Retention windows.
RetentionCallLogDays int `json:"retention_call_log_days"`
RetentionFilesDays int `json:"retention_files_days"`
RetentionInterval time.Duration `json:"retention_interval_ns"`
// Recording config.
RecordingDir string `json:"recording_dir,omitempty"`
RecordingSampleRate int `json:"recording_sample_rate"`
RecordingWriteRaw bool `json:"recording_write_raw"`
RecordingEQEnabled bool `json:"recording_eq_enabled"`
RecordingEQTaps int `json:"recording_eq_taps,omitempty"`
RecordingEQStepSize string `json:"recording_eq_step_size,omitempty"`
// Audio runtime (mirrors AudioStatus but adds device list +
// backend identity so operators can confirm whether the Linux
// fallback path took effect).
AudioEnabled bool `json:"audio_enabled"`
AudioDevice string `json:"audio_device,omitempty"`
AudioSampleRate int `json:"audio_sample_rate"`
AudioBufferMs int `json:"audio_buffer_ms"`
AudioBackends []string `json:"audio_backends"`
AudioDisableFallbk bool `json:"audio_disable_fallback"`
// SDR pool config (the live status is on /api/v1/devices).
SDRSampleRate int `json:"sdr_sample_rate"`
SDRBackends []string `json:"sdr_backends"`
// Scanner config (the live state is on /api/v1/scanner).
ScannerScanMode string `json:"scanner_scan_mode"`
ScannerCCHuntEnabled bool `json:"scanner_cc_hunt_enabled"`
ScannerCCHuntDwellMs int `json:"scanner_cc_hunt_dwell_ms"`
ScannerCCHuntBackoffMs int `json:"scanner_cc_hunt_backoff_ms"`
ScannerCCMaxBackoffMs int `json:"scanner_cc_max_backoff_ms"`
ScannerManualTuneEnabled bool `json:"scanner_manual_tune_enabled"`
// Tone-out profiles (names only, plus tone counts + cooldown).
ToneProfiles []ToneProfileDTO `json:"tone_profiles,omitempty"`
// Vocoder map by protocol — operator-facing names like
// "p25-phase2" → "ambe2".
VocoderMap map[string]string `json:"vocoder_map"`
// MetricsEnabled mirrors metrics.enabled config.
MetricsEnabled bool `json:"metrics_enabled"`
}
RuntimeDTO is the sanitised, JSON-friendly snapshot of every config knob + runtime fact the TUI's tabbed Settings inspector renders. Keep this strictly read-only — no secrets, no credentials, no auth tokens. Operators expect /api/v1/runtime to be safe to scrape.
type RuntimeProvider ¶
type RuntimeProvider interface {
Runtime() RuntimeDTO
}
RuntimeProvider returns the runtime snapshot. The daemon supplies the production impl; tests supply a fake. Optional on ServerOptions — when nil, GET /api/v1/runtime returns 503.
type ScannerCockpit ¶
type ScannerCockpit interface {
// Status returns the unified read snapshot the TUI panel renders.
Status() ScannerStatus
// SetScanMode flips the global TG-scan-list mode at runtime.
// Returns the previous mode for audit / UX feedback.
SetScanMode(mode string) (prev string, err error)
// HoldHunt / ResumeHunt / ForceRetuneHunt apply to a single
// trunked system. Returns false when the system isn't configured.
HoldHunt(system string) bool
ResumeHunt(system string) bool
ForceRetuneHunt(system string) bool
// HoldConventional / ResumeConventional / DwellConventional
// drive the conventional FM scanner. DwellConventional indexes
// into the configured Channels list. The Hold/Resume operations
// return false when the conventional scanner isn't configured.
HoldConventional() bool
ResumeConventional() bool
DwellConventional(index int) bool
// LockoutConventional / UnlockoutConventional toggle the per-
// channel lockout flag the scan loop respects. Locked-out
// channels are skipped by pickNextChannel. Returns false when
// the conventional scanner isn't configured or the index is
// out of range.
LockoutConventional(index int) bool
UnlockoutConventional(index int) bool
// ManualTune appends a VFO-style temporary channel to the
// conventional scanner and forces dwell on it. Returns the new
// index + ok=true on success; ok=false when the conventional
// scanner isn't configured (no Voice SDR carved out for it).
ManualTune(req ManualTuneRequest) (index int, ok bool)
// ClearManualTune removes a previously-added temp channel by
// index. Returns false if the index isn't a temp channel or
// the scanner isn't configured.
ClearManualTune(index int) bool
}
ScannerCockpit is the API surface for the police-scanner subsystem: reads the current state (per-system CC hunt, conventional channel list, talkgroup-scan stats) and applies operator mutations from the TUI (hold/resume/retune the hunter, hold/resume/dwell-on the conventional scanner, flip the global scan mode).
The daemon supplies a single ScannerCockpit implementation that aggregates the cchunt.Supervisor + conventional.Scanner + engine; tests can stub a single struct that satisfies the whole interface.
type ScannerStatus ¶
type ScannerStatus struct {
ScanMode string `json:"scan_mode"`
Systems []SystemHuntStatusDTO `json:"systems"`
Conventional ConvScannerStatusDTO `json:"conventional"`
TalkgroupScanCount int `json:"tg_scan_count"`
TalkgroupTotalCount int `json:"tg_total"`
}
ScannerStatus is the JSON shape returned by GET /api/v1/scanner — a unified view over all three scanner-subsystem read surfaces.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server hosts the GopherTrunk HTTP/SSE/WebSocket API. A separate gRPC server (internal/api/grpc.go) shares the same in-process state.
func NewServer ¶
func NewServer(opts ServerOptions) (*Server, error)
NewServer constructs a server but does not yet bind a listener; call Run.
type ServerOptions ¶
type ServerOptions struct {
// Addr is the listen address (e.g. ":8080" or "127.0.0.1:9000").
Addr string
Bus *events.Bus
Engine EngineSnapshot
Talkgroups *trunking.TalkgroupDB
Systems []trunking.System
// History is optional. When non-nil the server exposes
// GET /api/v1/calls/history.
History HistoryQuery
// MetricsHandler is optional. When non-nil it is mounted at
// GET /metrics; the daemon passes internal/metrics.Metrics.Handler()
// here. Decoupling via http.Handler keeps the api package free of a
// hard dependency on the metrics package.
MetricsHandler http.Handler
Log *slog.Logger
// Version is reported by GET /api/v1/version.
Version string
// AllowMutations is the legacy mutation gate. Deprecated in
// favour of Auth — set Auth.Mode = AuthModeDisabled to get the
// same wide-open semantics, or AuthModeAuto / AuthModeRequired
// for the bearer-token middleware. When Auth.Mode is the zero
// value (AuthModeAuto) and AllowMutations is true, the daemon
// emits a deprecation warning and treats the daemon as
// AuthModeDisabled to preserve the existing behaviour.
AllowMutations bool
// Auth configures the mutation auth middleware. See AuthMode
// for the policy semantics. Zero-value is AuthModeAuto, which
// requires a token on non-loopback binds and bypasses the
// check on loopback (peer-cred trust on a single-host
// deployment).
Auth AuthConfig
// Mutator is the engine's write side (end call). Optional;
// when nil the corresponding routes return 503.
Mutator EngineMutator
// Retention is the storage sweeper's write side (run a sweep
// now). Optional.
Retention RetentionSweeper
// Tones is the tone-out detector's write side (reset per-device
// match state). Optional.
Tones ToneDetectorReset
// Devices exposes the SDR pool snapshot for GET /api/v1/devices.
// Optional; the route returns 503 when nil.
Devices DevicesProvider
// Scanner exposes the police-scanner cockpit (CC hunter,
// conventional FM scanner, TG scan list) for GET + PATCH
// /api/v1/scanner and the related mutation routes. Optional;
// when nil, the routes return 503.
Scanner ScannerCockpit
// Audio exposes the live-audio player + recorder gate for
// GET + PATCH /api/v1/audio. Optional; when nil, the routes
// return 503.
Audio AudioController
// Runtime exposes the read-only daemon config snapshot served at
// GET /api/v1/runtime. The TUI's tabbed Settings inspector uses
// it to surface every config knob. Optional; when nil, the
// route returns 503.
Runtime RuntimeProvider
// AudioPublisher, when non-nil, enables the
// GET /api/v1/audio/stream HTTP endpoint that streams live
// composed PCM as a continuous WAV body. Reuses the same
// publisher that backs gRPC StreamAudio so the HTTP stream is
// a parallel subscriber rather than a second fan-out.
AudioPublisher *AudioPublisher
// CORS configures the cross-origin middleware. Off when
// AllowedOrigins is empty (the daemon emits no CORS headers).
// Set this when the browser-served SPA is loaded from an
// origin different to the daemon's (most commonly file://,
// whose Origin header is the literal string "null").
CORS CORSConfig
// TLSCert and TLSKey, when both non-empty, switch the HTTP
// server to TLS. Paths point at PEM-encoded files on disk that
// the daemon reads at start-up. Leaving either empty serves
// plain HTTP (the default — appropriate for loopback / private-
// network deployments where the bearer-token auth gate is the
// only protection on mutations).
TLSCert string
TLSKey string
}
ServerOptions configure a new Server.
type SystemDTO ¶
type SystemDTO struct {
Name string `json:"name"`
Protocol string `json:"protocol"`
ControlChannels []uint32 `json:"control_channels"`
WACN uint32 `json:"wacn,omitempty"`
SystemID uint16 `json:"system_id,omitempty"`
RFSS uint8 `json:"rfss,omitempty"`
Site uint8 `json:"site,omitempty"`
// Per-protocol FEC opt-out surface. Empty strings indicate the
// new spec-correct default is active (channel coding / FEC on
// for every protocol). Non-empty values that parse to "off" /
// "false" / "0" opt the operator into the legacy raw-bit path
// per-protocol. The TUI Settings panel renders these so operators
// can verify their config landed; runtime mutation is a follow-up
// (currently requires editing config.yaml + restarting the
// daemon).
TETRAColourCode uint32 `json:"tetra_colour_code,omitempty"`
TETRAChannel string `json:"tetra_channel,omitempty"`
TETRAChannelCoding string `json:"tetra_channel_coding,omitempty"`
LTRFCSMode string `json:"ltr_fcs_mode,omitempty"`
LTRManchesterMode string `json:"ltr_manchester_mode,omitempty"`
P25Phase2TrellisMode string `json:"p25_phase2_trellis_mode,omitempty"`
P25Phase2RSMode string `json:"p25_phase2_rs_mode,omitempty"`
P25Phase2ScramblerMode string `json:"p25_phase2_scrambler_mode,omitempty"`
NXDNViterbiMode string `json:"nxdn_viterbi_mode,omitempty"`
EDACSBCHMode string `json:"edacs_bch_mode,omitempty"`
MPT1327BCHMode string `json:"mpt1327_bch_mode,omitempty"`
MPT1327CWSCTolerance string `json:"mpt1327_cwsc_tolerance,omitempty"`
MotorolaBCHMode string `json:"motorola_bch_mode,omitempty"`
}
SystemDTO mirrors trunking.System for JSON.
type SystemHuntStatusDTO ¶
type SystemHuntStatusDTO struct {
Name string `json:"name"`
Protocol string `json:"protocol"`
State string `json:"state"`
AttemptedFreqHz uint32 `json:"attempted_freq_hz,omitempty"`
AttemptIndex int `json:"attempt_index,omitempty"`
TotalCandidates int `json:"total_candidates,omitempty"`
LockedFreqHz uint32 `json:"locked_freq_hz,omitempty"`
LockedAt time.Time `json:"locked_at,omitempty"`
NAC uint16 `json:"nac,omitempty"`
LastFailedAt time.Time `json:"last_failed_at,omitempty"`
BackoffMs int `json:"backoff_ms,omitempty"`
LastGrantAt time.Time `json:"last_grant_at,omitempty"`
}
SystemHuntStatusDTO mirrors cchunt.SystemStatus for the wire layer so the api package doesn't import internal/scanner.
type TalkgroupDTO ¶
type TalkgroupDTO struct {
ID uint32 `json:"id"`
AlphaTag string `json:"alpha_tag"`
Description string `json:"description,omitempty"`
Tag string `json:"tag,omitempty"`
Group string `json:"group,omitempty"`
Mode string `json:"mode,omitempty"`
Priority int `json:"priority,omitempty"`
Lockout bool `json:"lockout,omitempty"`
Scan bool `json:"scan"`
}
TalkgroupDTO mirrors trunking.TalkGroup for JSON.
type ToneDetectorReset ¶
type ToneDetectorReset interface {
ResetDevice(serial string)
}
ToneDetectorReset is the optional write side of the tone-out detector: clear per-device match progress without throwing away the cooldown clock. Daemons that wire the detector supply the real impl; tests can fake it.
type ToneProfileDTO ¶
type ToneProfileDTO struct {
Name string `json:"name"`
AlphaTag string `json:"alpha_tag,omitempty"`
Cooldown time.Duration `json:"cooldown_ns"`
ToneCount int `json:"tone_count"`
}
ToneProfileDTO is the minimal projection of a tone-out profile — no internal detector state, just the operator-relevant fields.