api

package
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: May 15, 2026 License: Apache-2.0 Imports: 27 Imported by: 0

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

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

func NewAudioPublisher(bus *events.Bus, log *slog.Logger) (*AudioPublisher, error)

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 (*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

type AudioPublisherStats struct {
	Subscribers   int
	DroppedTotal  uint64
	TrackedGrants int
}

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.

const (
	AuthModeAuto AuthMode = iota
	AuthModeRequired
	AuthModeDisabled
)

func ParseAuthMode

func ParseAuthMode(s string) (AuthMode, bool)

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.

func (AuthMode) String

func (m AuthMode) String() string

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 CallEndDTO struct {
	Grant        GrantDTO      `json:"grant"`
	Talkgroup    *TalkgroupDTO `json:"talkgroup,omitempty"`
	DeviceSerial string        `json:"device_serial"`
	StartedAt    time.Time     `json:"started_at"`
	EndedAt      time.Time     `json:"ended_at"`
	Reason       string        `json:"reason"`
}

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

type DevicesProvider interface {
	Snapshot() []sdr.SDRStatus
}

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

type EngineMutator interface {
	EndCall(deviceSerial string, reason trunking.EndReason) bool
}

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 (*GRPCServer) GetTalkgroup

func (*GRPCServer) ListSystems

func (*GRPCServer) ListTalkgroups

func (*GRPCServer) Run

func (g *GRPCServer) Run(ctx context.Context) error

Run binds the listener and serves until ctx cancels.

func (*GRPCServer) Stop

func (g *GRPCServer) Stop()

Stop gracefully halts the gRPC server.

func (*GRPCServer) StreamAudio

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

type RetentionSweeper interface {
	SweepOnce(ctx context.Context)
}

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.

func (*Server) Close

func (s *Server) Close() error

Close gracefully shuts down the server. Safe to call after Run returns.

func (*Server) Run

func (s *Server) Run(ctx context.Context) error

Run binds the listener and serves until ctx cancels.

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.

Directories

Path Synopsis
pb
v1

Jump to

Keyboard shortcuts

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