api

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jun 2, 2026 License: Apache-2.0 Imports: 34 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 ADSBProvider added in v0.2.6

type ADSBProvider interface {
	RecentAircraftReports(limit int) ([]storage.AircraftReport, error)
	// CurrentAircraft returns the coalesced latest state per ICAO seen
	// within maxAge (≤ 0 → provider default).
	CurrentAircraft(maxAge time.Duration) ([]storage.AircraftReport, error)
}

ADSBProvider is the read surface the adsb endpoint consumes. The daemon implements it on top of storage.AircraftLog; tests substitute a fake.

type AISMessageDTO added in v0.2.6

type AISMessageDTO struct {
	ID         int64     `json:"id"`
	ReceivedAt time.Time `json:"received_at"`
	MMSI       uint32    `json:"mmsi"`
	Type       string    `json:"type"`
	Body       string    `json:"body,omitempty"`

	Latitude         float64 `json:"latitude,omitempty"`
	Longitude        float64 `json:"longitude,omitempty"`
	SpeedOverGround  float64 `json:"sog,omitempty"`
	CourseOverGround float64 `json:"cog,omitempty"`
	Heading          int     `json:"heading,omitempty"`
	HasPosition      bool    `json:"has_position"`

	VesselName  string `json:"vessel_name,omitempty"`
	Callsign    string `json:"callsign,omitempty"`
	Destination string `json:"destination,omitempty"`
	ShipType    int    `json:"ship_type,omitempty"`
	IMO         uint32 `json:"imo,omitempty"`

	RawHex string `json:"raw_hex,omitempty"`
	FCSOK  bool   `json:"fcs_ok"`
}

AISMessageDTO is the JSON wire shape for the ais-log endpoint. The Position fields and the static-data fields are omitted from the JSON when zero/empty so the wire stays compact for the position-only common case.

type AISProvider added in v0.2.6

type AISProvider interface {
	RecentAISMessages(limit int) ([]storage.AISMessage, error)
}

AISProvider is the read surface the ais-log endpoint consumes. The daemon implements it on top of storage.VesselLog; tests substitute a fake.

type APRSPacketDTO added in v0.2.4

type APRSPacketDTO struct {
	ID         int64     `json:"id"`
	ReceivedAt time.Time `json:"received_at"`
	Src        string    `json:"src"`
	Dst        string    `json:"dst"`
	Path       string    `json:"path,omitempty"`
	Type       string    `json:"type"`
	Body       string    `json:"body,omitempty"`
	Latitude   float64   `json:"latitude,omitempty"`
	Longitude  float64   `json:"longitude,omitempty"`
	RawInfo    string    `json:"raw_info,omitempty"`
	FCSOK      bool      `json:"fcs_ok"`
}

APRSPacketDTO is the JSON wire shape for the aprs-log endpoint.

type APRSProvider added in v0.2.4

type APRSProvider interface {
	RecentAPRSPackets(limit int) ([]storage.APRSPacket, error)
}

APRSProvider is the read surface the aprs-log endpoint consumes. The daemon implements it on top of storage.APRSLog; tests substitute a fake.

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 AffiliationDTO added in v0.1.7

type AffiliationDTO struct {
	System            string `json:"system"`
	Protocol          string `json:"protocol"`
	SourceID          uint32 `json:"source_id"`
	GroupID           uint32 `json:"group_id"`
	AnnouncementGroup uint32 `json:"announcement_group,omitempty"`
	Response          string `json:"response"`
}

AffiliationDTO mirrors trunking.Affiliation.

type AffiliationProvider added in v0.1.9

type AffiliationProvider interface {
	Affiliations() []trunking.UnitActivity
}

AffiliationProvider is the read side of the affiliation tracker, supplying the unit-activity table for GET /api/v1/affiliations.

type AircraftReportDTO added in v0.2.6

type AircraftReportDTO struct {
	ID         int64     `json:"id"`
	ReceivedAt time.Time `json:"received_at"`
	ICAO       uint32    `json:"icao"`
	ICAOHex    string    `json:"icao_hex"`
	Kind       string    `json:"kind"`
	Body       string    `json:"body,omitempty"`
	CRCValid   bool      `json:"crc_valid"`

	Callsign string `json:"callsign,omitempty"`
	Category int    `json:"category,omitempty"`

	Latitude    float64 `json:"latitude,omitempty"`
	Longitude   float64 `json:"longitude,omitempty"`
	Altitude    int     `json:"altitude_ft,omitempty"`
	HasPosition bool    `json:"has_position"`
	HasAltitude bool    `json:"has_altitude"`

	GroundSpeedKn   int     `json:"ground_speed_kn,omitempty"`
	TrackDeg        float64 `json:"track_deg,omitempty"`
	VerticalRateFPM int     `json:"vertical_rate_fpm,omitempty"`

	RawHex string `json:"raw_hex,omitempty"`
}

AircraftReportDTO is the JSON wire shape for the adsb endpoint. Position fields, identification fields, and velocity fields stay omitted from the JSON when zero / empty so the wire stays compact for the kind-specific common case.

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

""             → AuthModeDisabled (the new default — gophertrunk
                 is overwhelmingly deployed on closed LANs where
                 the bearer-token middleware is friction without
                 a corresponding threat model; opt back in by
                 setting "auto" or "required" explicitly)
"auto"         → AuthModeAuto
"required" / "on" / "true"    → AuthModeRequired
"disabled" / "off" / "false"  → AuthModeDisabled

Unknown strings return AuthModeDisabled with ok=false so callers can warn without leaving the daemon in an ambiguous state.

func (AuthMode) String

func (m AuthMode) String() string

type BookmarkDTO added in v0.2.3

type BookmarkDTO struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	FreqHz    uint32    `json:"freq_hz"`
	Mode      string    `json:"mode"`
	CTCSSHz   float64   `json:"ctcss_hz,omitempty"`
	DCSCode   uint16    `json:"dcs_code,omitempty"`
	Notes     string    `json:"notes,omitempty"`
	Group     string    `json:"group,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

BookmarkDTO is the JSON wire shape served by the bookmark endpoints. Mirrors storage.Bookmark — kept distinct only so the api package can stay free of storage-package imports at the type-name level (the interface above does the actual decoupling).

type BookmarkProvider added in v0.2.3

type BookmarkProvider interface {
	ListBookmarks() ([]storage.Bookmark, error)
	GetBookmark(id int64) (storage.Bookmark, error)
	CreateBookmark(b storage.Bookmark) (storage.Bookmark, error)
	UpdateBookmark(b storage.Bookmark) (storage.Bookmark, error)
	DeleteBookmark(id int64) error
}

BookmarkProvider is the read+write surface the bookmarks endpoints consume. The daemon implements it on top of storage.BookmarkStore; tests substitute a fake. Decoupling keeps the api package free of a hard dependency on internal/storage.

type BroadcastStatusProvider added in v0.1.9

type BroadcastStatusProvider interface {
	BroadcastStats() any
}

BroadcastStatusProvider is the read side of the outbound call-streaming subsystem (internal/broadcast). BroadcastStats returns a JSON-serialisable counter snapshot; the daemon adapts broadcast.Manager.Stats() to this interface so the api package keeps no compile-time dependency on internal/broadcast.

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 daemon's permissive default (CORS open to any origin) applies — closed-LAN setups don't have to opt into CORS to load the web SPA from file:// or a sibling static server. Operators who run on a hostile network override this list to clamp it back down.

func (CORSConfig) IsDefaultPermissive added in v0.1.5

func (c CORSConfig) IsDefaultPermissive() bool

IsDefaultPermissive reports whether the runtime is operating on the empty-config-means-allow-all default. Surfaced so the daemon can warn at startup when a non-loopback bind is combined with the permissive default.

type CallEncryptionDTO added in v0.2.2

type CallEncryptionDTO struct {
	DeviceSerial string    `json:"device_serial"`
	System       string    `json:"system,omitempty"`
	Protocol     string    `json:"protocol,omitempty"`
	GroupID      uint32    `json:"group_id,omitempty"`
	AlgorithmID  uint8     `json:"algorithm_id"`
	KeyID        uint16    `json:"key_id"`
	At           time.Time `json:"at"`
}

CallEncryptionDTO mirrors trunking.CallEncryption for SSE / REST consumers. Subscribers patch the matching active-call row with the new ALGID/KID so the UI flips from "enc" to "enc 0x84 (AES-256)" the moment the LDU2 lands.

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 ConfigWriter added in v0.1.5

type ConfigWriter interface {
	// WritePatch applies the patch to the backing config.yaml and
	// returns the merged config so callers can route hot-reloadable
	// fields to the in-memory applier.
	WritePatch(p config.Patch) (config.Config, error)
	// Path is the path to the config.yaml the writer mutates.
	// Empty means "no config file backs this daemon" (the SPA / TUI
	// should render the Settings panel read-only).
	Path() string
}

ConfigWriter wraps the daemon's config.yaml writer. Decoupled via an interface so tests can fake it and the api package doesn't pull in the OS file machinery.

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 DSCMessageDTO added in v0.2.6

type DSCMessageDTO struct {
	ID         int64     `json:"id"`
	ReceivedAt time.Time `json:"received_at"`
	Format     string    `json:"format"`
	Category   string    `json:"category"`
	SelfMMSI   uint64    `json:"self_mmsi"`
	TargetMMSI uint64    `json:"target_mmsi,omitempty"`
	Nature     string    `json:"nature,omitempty"`
	TimeUTC    string    `json:"time_utc,omitempty"`

	Latitude    float64 `json:"latitude,omitempty"`
	Longitude   float64 `json:"longitude,omitempty"`
	HasPosition bool    `json:"has_position"`

	Body   string `json:"body,omitempty"`
	RawHex string `json:"raw_hex,omitempty"`
}

DSCMessageDTO is the JSON wire shape for the dsc-log endpoint. Position fields and distress-only fields stay omitted from the JSON when zero / empty so the routine-call wire stays compact.

type DSCProvider added in v0.2.6

type DSCProvider interface {
	RecentDSCMessages(limit int) ([]storage.DSCMessage, error)
}

DSCProvider is the read surface the dsc-log endpoint consumes. The daemon implements it on top of storage.DSCLog; tests substitute a fake.

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 DiagProvider added in v0.2.3

type DiagProvider interface {
	// OpenIQStream starts a per-request decimator on the named
	// device. TargetRateSPS clamps the output rate (≤ device
	// sample_rate). Returns the wire-frame channel and a cleanup
	// func the caller MUST invoke on disconnect.
	OpenIQStream(ctx context.Context, serial string, targetRateSPS uint32) (<-chan IQFrame, func(), error)
}

DiagProvider is the daemon-side abstraction the diag endpoints consume. The daemon implements it on top of the iqtap broker map; tests substitute 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

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

func (*GRPCServer) GetSystem

func (*GRPCServer) GetTalkgroup

func (*GRPCServer) ListRIDHistory added in v0.2.4

func (*GRPCServer) ListRIDs added in v0.2.4

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
	// RIDs is the operator-configured radio-ID alias table. When nil
	// the server allocates an empty one so RIDService still serves a
	// stable shape.
	RIDs *trunking.RIDDB
	// Affiliations is the read side of the affiliation tracker —
	// supplies the live UnitActivity overlay for RIDService. Optional.
	Affiliations AffiliationProvider
	// History supplies per-RID call history for ListRIDHistory.
	// Optional; without it ListRIDHistory returns Unavailable.
	History HistoryQuery
	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"`
	// AlgorithmID / KeyID surface the P25 encryption parameters
	// recovered from the in-call signalling. Zero when Encrypted is
	// false; also zero on a Phase 1 grant until the LDU2 Encryption
	// Sync has been parsed and the engine has backfilled the active
	// call (see KindCallEncryption).
	AlgorithmID uint8  `json:"algorithm_id,omitempty"`
	KeyID       uint16 `json:"key_id,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
	SourceID  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 IQFrame added in v0.2.3

type IQFrame struct {
	TimestampNs  int64     `json:"ts_ns"`
	SampleRateHz uint32    `json:"sample_rate"`
	CenterHz     uint32    `json:"center_hz"`
	Points       []IQPoint `json:"points"`
	EnergyDBFS   float32   `json:"energy_dbfs"`
}

IQFrame is the wire shape of one decimated-IQ batch.

type IQPoint added in v0.2.3

type IQPoint struct {
	I float32 `json:"i"`
	Q float32 `json:"q"`
}

IQPoint and IQFrame mirror the shapes the daemon's diag.Decimator produces. Defined here so the api package stays free of an import dependency on internal/dsp/diag; the DiagProvider interface bridges the two.

type ImportCommitResult added in v0.1.5

type ImportCommitResult struct {
	SystemsAdded    []string `json:"systems_added"`
	SystemsReplaced []string `json:"systems_replaced"`
	CSVPaths        []string `json:"csv_paths,omitempty"`
	ConfigPath      string   `json:"config_path,omitempty"`
}

ImportCommitResult is the response shape for a successful commit.

type ImportPreviewResponse added in v0.1.5

type ImportPreviewResponse struct {
	ID      string            `json:"id"`
	Systems []ParsedSystemDTO `json:"systems"`
}

ImportPreviewResponse is the response shape for POST /api/v1/import.

type ImportSource added in v0.1.5

type ImportSource struct {
	Filename string
	Kind     ImportSourceKind
	Data     []byte
}

ImportSource is one uploaded file in a multipart import request. Filename + content kept in memory so the handler can pipe them to the daemon's parsers without retaining a file descriptor.

type ImportSourceKind added in v0.1.5

type ImportSourceKind string

ImportSourceKind discriminates the upload's payload format.

const (
	ImportSourcePDF ImportSourceKind = "pdf"
	ImportSourceCSV ImportSourceKind = "csv"
)

type Importer added in v0.1.5

type Importer interface {
	// Parse runs the relevant parser (PDF or CSV) against the
	// supplied source and returns a preview DTO.
	Parse(s ImportSource) (ParsedSystemDTO, error)
	// Commit finalises a previously-parsed batch by merging it
	// into config.yaml and refreshing the in-memory talkgroup DB.
	// The implementer is responsible for serialising commits with
	// any other config writer (settings PATCH) so two callers
	// can't race the on-disk file.
	Commit(sources []ImportSource, force bool) (ImportCommitResult, error)
}

Importer is the daemon-side import surface. Decoupled via interface so the api package doesn't have to reach into cmd/gophertrunk's parser internals. The daemon supplies an adapter that delegates to parsePDFFile, parseCSVFile, mergeIntoConfig.

type LocationFix added in v0.1.9

type LocationFix struct {
	System     string  `json:"system"`
	Protocol   string  `json:"protocol"`
	RadioID    uint32  `json:"radio_id"`
	Talkgroup  uint32  `json:"talkgroup"`
	Latitude   float64 `json:"latitude"`
	Longitude  float64 `json:"longitude"`
	SpeedKnots float64 `json:"speed_knots"`
	HeadingDeg float64 `json:"heading_deg"`
	ReportedAt string  `json:"reported_at"` // RFC3339
}

LocationFix is one geographic fix returned by GET /api/v1/locations.

type LocationQuery added in v0.1.9

type LocationQuery interface {
	RecentLocations(limit int) ([]LocationFix, error)
}

LocationQuery is the read side of the GPS/location subsystem, supplying recent fixes for GET /api/v1/locations and the web map.

func LocationsFromStorage added in v0.1.9

func LocationsFromStorage(ll *storage.LocationLog) LocationQuery

LocationsFromStorage wraps a *storage.LocationLog as an api.LocationQuery so the daemon can pass it to NewServer without the storage package's row types leaking into the api package.

type M17LinkSetupDTO added in v0.2.9

type M17LinkSetupDTO struct {
	ID         int64     `json:"id"`
	ReceivedAt time.Time `json:"received_at"`
	Src        string    `json:"src"`
	Dst        string    `json:"dst"`
	Mode       string    `json:"mode"`
	CAN        uint8     `json:"can"`
	Meta       string    `json:"meta,omitempty"`
	CRCOK      bool      `json:"crc_ok"`
	Body       string    `json:"body,omitempty"`
}

M17LinkSetupDTO is the JSON wire shape for the m17-log endpoint.

type M17Provider added in v0.2.9

type M17Provider interface {
	RecentM17LinkSetups(limit int) ([]storage.M17LinkSetup, error)
}

M17Provider is the read surface the m17-log endpoint consumes. The daemon implements it on top of storage.M17Log; tests substitute a fake.

type MDC1200MessageDTO added in v0.2.7

type MDC1200MessageDTO struct {
	ID         int64     `json:"id"`
	ReceivedAt time.Time `json:"received_at"`
	Op         uint8     `json:"op"`
	Arg        uint8     `json:"arg"`
	UnitID     uint16    `json:"unit_id"`
	Operation  string    `json:"operation,omitempty"`
	Body       string    `json:"body,omitempty"`
	RawHex     string    `json:"raw_hex,omitempty"`
	CRCOK      bool      `json:"crc_ok"`
}

MDC1200MessageDTO is the JSON wire shape for the mdc1200-log endpoint. The operation label and raw-hex stay omitted when empty so the wire stays compact for the common PTT-ID burst.

type MDC1200Provider added in v0.2.7

type MDC1200Provider interface {
	RecentMDC1200Messages(limit int) ([]storage.MDC1200Message, error)
}

MDC1200Provider is the read surface the mdc1200-log endpoint consumes. The daemon implements it on top of storage.MDC1200Log; tests substitute a fake.

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 PagerMessageDTO added in v0.2.3

type PagerMessageDTO struct {
	ID         int64     `json:"id"`
	ReceivedAt time.Time `json:"received_at"`
	Protocol   string    `json:"protocol"`
	RIC        uint32    `json:"ric"`
	Func       uint8     `json:"func"`
	Encoding   string    `json:"encoding"`
	Body       string    `json:"body"`
	Corrected  int       `json:"corrected"`
}

PagerMessageDTO is the JSON wire shape for the pager-log endpoint.

type PagerProvider added in v0.2.3

type PagerProvider interface {
	RecentPagerMessages(limit int) ([]storage.PagerMessage, error)
}

PagerProvider is the read surface the pager-log endpoint consumes. The daemon implements it on top of storage.PagerLog; tests substitute a fake.

type ParsedSystemDTO added in v0.1.5

type ParsedSystemDTO struct {
	Name        string                 `json:"name"`
	Location    string                 `json:"location,omitempty"`
	County      string                 `json:"county,omitempty"`
	SysID       string                 `json:"sysid,omitempty"`
	WACN        string                 `json:"wacn,omitempty"`
	SystemType  string                 `json:"system_type,omitempty"`
	Protocol    string                 `json:"protocol"`
	SiteCount   int                    `json:"site_count"`
	TalkgroupCt int                    `json:"talkgroup_count"`
	SourcePath  string                 `json:"source_path,omitempty"`
	Extra       map[string]interface{} `json:"extra,omitempty"`
}

ParsedSystemDTO is the JSON projection of one parsed system / site / talkgroup tree. It deliberately mirrors the cmd/gophertrunk parsedSystem shape so the SPA / TUI can render the preview verbatim without learning a third schema.

type PatchDTO added in v0.2.4

type PatchDTO struct {
	System     string    `json:"system"`
	Protocol   string    `json:"protocol"`
	SuperGroup uint32    `json:"super_group"`
	Members    []uint32  `json:"members"`
	Vendor     string    `json:"vendor,omitempty"`
	Add        bool      `json:"add"`
	At         time.Time `json:"at"`
}

PatchDTO mirrors trunking.Patch for SSE / REST consumers. Add=true is a patch becoming active; Add=false is a cancel.

type RIDDTO added in v0.2.4

type RIDDTO struct {
	ID          uint32 `json:"id"`
	Alias       string `json:"alias,omitempty"`
	Description string `json:"description,omitempty"`
	Tag         string `json:"tag,omitempty"`
	Group       string `json:"group,omitempty"`
	Owner       string `json:"owner,omitempty"`
	Priority    int    `json:"priority,omitempty"`
	Lockout     bool   `json:"lockout,omitempty"`
	Watch       bool   `json:"watch"`
	Icon        string `json:"icon,omitempty"`

	// Configured is true when this row is backed by an entry in the
	// static RIDDB (rid_alias_file). Used by the UI to distinguish
	// known radios from RIDs only ever seen over the air.
	Configured bool `json:"configured"`

	// Live observation fields — empty/zero when the RID has not been
	// seen since the daemon started (or since the affiliation tracker
	// swept it).
	System        string    `json:"system,omitempty"`
	Protocol      string    `json:"protocol,omitempty"`
	LastTalkgroup uint32    `json:"last_talkgroup,omitempty"`
	TalkerAlias   string    `json:"talker_alias,omitempty"`
	TalkerAliasAt time.Time `json:"talker_alias_at,omitempty"`
	CallCount     uint64    `json:"call_count,omitempty"`
	FirstSeen     time.Time `json:"first_seen,omitempty"`
	LastSeen      time.Time `json:"last_seen,omitempty"`
}

RIDDTO mirrors trunking.RID plus the live affiliation-tracker fields (last_seen, last_talkgroup, talker_alias, call_count). When a row is purely live (no configured static RID), the configured fields are zero / empty and Live is true.

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

	// ConfigPath is the absolute path to the config.yaml backing this
	// daemon, or empty when the daemon was started without a -config
	// file. The SPA / TUI use it to gate the editable Settings panel:
	// empty = render read-only ("daemon running on built-in defaults").
	ConfigPath string `json:"config_path,omitempty"`
	// StartupWarnings carries the non-fatal observations the daemon
	// collected during NewDaemon (missing talkgroup CSV, SDR pool
	// failed to open, etc.). Surfaced so the SPA Dashboard can pin
	// them until the operator dismisses them.
	StartupWarnings []string `json:"startup_warnings,omitempty"`

	// HiddenTabs lists the navigation tab keys the operator switched
	// off via web.tabs in config. Both the web SPA and the TUI filter
	// these out of their nav. Empty/omitted means every tab is shown.
	HiddenTabs []string `json:"hidden_tabs,omitempty"`
}

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) BoundAddr added in v0.1.5

func (s *Server) BoundAddr() string

BoundAddr returns the actual TCP address the listener bound to, useful when callers configured ":0" / "127.0.0.1:0" and need the kernel-assigned port. Returns "" before Run() has bound.

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
	// RIDs is the operator-configured radio-ID alias table. When nil
	// the server allocates an empty one so the routes serve a stable
	// shape; the daemon passes a populated DB loaded from each
	// system's rid_alias_file.
	RIDs    *trunking.RIDDB
	Systems []trunking.System
	// History is optional. When non-nil the server exposes
	// GET /api/v1/calls/history.
	History HistoryQuery
	// Locations is optional. When non-nil the server exposes
	// GET /api/v1/locations for the web map.
	Locations LocationQuery
	// Affiliations is optional. When non-nil the server exposes
	// GET /api/v1/affiliations (the unit-activity table).
	Affiliations AffiliationProvider
	// 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
	// Broadcast exposes the outbound call-streaming subsystem's
	// counters for GET /api/v1/broadcast. Optional; when nil, the
	// route reports the subsystem as disabled.
	Broadcast BroadcastStatusProvider
	// 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
	// ConfigWriter, when supplied, enables PATCH /api/v1/settings:
	// the daemon writes the operator's edits to config.yaml
	// (preserving comments) and feeds the result back to
	// SettingsApplier for hot-reload. nil disables the endpoint
	// (returns 503) so daemons started without a -config file don't
	// pretend the SPA's edits will persist.
	ConfigWriter ConfigWriter
	// SettingsApplier is the in-process hot-reload surface invoked
	// by handleSettingsPatch after the on-disk write succeeds.
	// Optional: when nil, every field is reported as
	// "restart_required" in the response.
	SettingsApplier SettingsApplier
	// Importer enables the live system-import endpoints
	// (POST /api/v1/import, POST /api/v1/import/{id}/commit,
	// DELETE /api/v1/import/{id}). nil disables the endpoints —
	// the daemon emits 503 so the SPA can present a clear "import
	// disabled" message.
	Importer Importer
	// WebAssets, when non-nil and containing an `index.html`, is
	// served from `/` (and as the SPA fallback for any non-/api
	// path). Set this to the embedded web/dist filesystem so the
	// daemon hosts the operator console without a sibling
	// gophertrunk-web/ directory. Leave nil to keep the SPA
	// out-of-process.
	WebAssets fs.FS
	// 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
	// Spectrum, when non-nil, enables the
	// GET /api/v1/spectrum/devices read endpoint and the
	// WS /api/v1/spectrum/stream live FFT frame stream. The daemon
	// implements this on top of its iqtap.Broker map; nil keeps the
	// routes returning 503 so a build without SDRs doesn't pretend
	// to have a waterfall.
	Spectrum SpectrumProvider
	// Bookmarks, when non-nil, enables the
	// GET/POST/PATCH/DELETE /api/v1/bookmarks routes for operator-
	// managed conventional channel bookmarks. nil keeps the routes
	// returning 503. Wired by the daemon over the SQLite-backed
	// storage.BookmarkStore.
	Bookmarks BookmarkProvider
	// Diag, when non-nil, enables the
	// WS /api/v1/diag/iq decimated-IQ live stream that backs the
	// web Constellation panel. The daemon implements this over
	// the iqtap broker + internal/dsp/diag; nil keeps the route
	// returning 503.
	Diag DiagProvider
	// Pager, when non-nil, enables the
	// GET /api/v1/pager/messages route serving recent decoded
	// POCSAG (and eventually FLEX) pager messages. Wired by the
	// daemon over the SQLite-backed storage.PagerLog.
	Pager PagerProvider
	// APRS, when non-nil, enables the
	// GET /api/v1/aprs/packets route serving recent decoded
	// APRS / AX.25 packets. Wired by the daemon over the SQLite-
	// backed storage.APRSLog.
	APRS APRSProvider
	// AIS, when non-nil, enables the
	// GET /api/v1/ais/vessels route serving recent decoded
	// AIS messages. Wired by the daemon over the SQLite-backed
	// storage.VesselLog.
	AIS AISProvider
	// DSC, when non-nil, enables the
	// GET /api/v1/dsc/messages route serving recent decoded
	// marine DSC sequences. Wired by the daemon over the
	// SQLite-backed storage.DSCLog.
	DSC DSCProvider
	// M17, when non-nil, enables the
	// GET /api/v1/m17/linksetups route serving recent decoded M17
	// link-setup frames. Wired by the daemon over the SQLite-backed
	// storage.M17Log.
	M17 M17Provider
	// ADSB, when non-nil, enables the
	// GET /api/v1/adsb/aircraft route serving recent decoded
	// Mode-S frames. Wired by the daemon over the SQLite-backed
	// storage.AircraftLog.
	ADSB ADSBProvider
	// MDC1200, when non-nil, enables the
	// GET /api/v1/mdc1200/messages route serving recent decoded
	// MDC1200 signaling bursts. Wired by the daemon over the
	// SQLite-backed storage.MDC1200Log.
	MDC1200 MDC1200Provider
	// 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 SettingsApplier added in v0.1.5

type SettingsApplier interface {
	SetLogLevel(level string) error
	SetAudioVolume(volume float32)
	SetAudioMuted(muted bool)
	SetAudioEnabled(enabled bool)
	SetRecordingEnabled(enabled bool)
	SetScannerScanMode(mode string) error
}

SettingsApplier is the optional in-process hot-reload surface for fields the daemon can change without a restart. Routes that don't have a matching Applier method are still written to disk; the response's RestartRequired list flags them so the UI can render "restart required" badges.

type SettingsPatchRequest added in v0.1.5

type SettingsPatchRequest struct {
	LogLevel  *string `json:"log_level,omitempty"`
	LogFormat *string `json:"log_format,omitempty"`

	APIHTTPAddr *string `json:"api_http_addr,omitempty"`
	APIGRPCAddr *string `json:"api_grpc_addr,omitempty"`
	APIAuthMode *string `json:"api_auth_mode,omitempty"`

	AudioEnabled  *bool    `json:"audio_enabled,omitempty"`
	AudioDevice   *string  `json:"audio_device,omitempty"`
	AudioVolume   *float32 `json:"audio_volume,omitempty"`
	AudioMuted    *bool    `json:"audio_muted,omitempty"`
	AudioBufferMs *int     `json:"audio_buffer_ms,omitempty"`

	RecordingsDir        *string `json:"recordings_dir,omitempty"`
	RecordingsSampleRate *uint32 `json:"recordings_sample_rate,omitempty"`
	RecordingsWriteRaw   *bool   `json:"recordings_write_raw,omitempty"`

	RetentionCallLogDays *int    `json:"retention_call_log_days,omitempty"`
	RetentionFilesDays   *int    `json:"retention_files_days,omitempty"`
	RetentionInterval    *string `json:"retention_interval,omitempty"`

	SDRSampleRate *uint32 `json:"sdr_sample_rate,omitempty"`

	ScannerScanMode          *string `json:"scanner_scan_mode,omitempty"`
	ScannerManualTuneEnabled *bool   `json:"scanner_manual_tune_enabled,omitempty"`
	ScannerCCHuntEnabled     *bool   `json:"scanner_cc_hunt_enabled,omitempty"`
	ScannerCCHuntDwellMs     *int    `json:"scanner_cc_hunt_dwell_ms,omitempty"`
	ScannerCCHuntBackoffMs   *int    `json:"scanner_cc_hunt_backoff_ms,omitempty"`
	ScannerCCHuntMaxBackoff  *int    `json:"scanner_cc_hunt_max_backoff_ms,omitempty"`

	StoragePath        *string `json:"storage_path,omitempty"`
	StorageCCCacheFile *string `json:"storage_cc_cache_file,omitempty"`

	MetricsEnabled *bool `json:"metrics_enabled,omitempty"`
}

SettingsPatchRequest is the JSON shape of PATCH /api/v1/settings. Pointer fields preserve "leave alone" semantics — JSON-omitted fields aren't zeroed in the daemon's running config.

The shape mirrors config.Patch one-for-one; the names use snake- case-with-section-prefix so the wire format reads close to the YAML config keys an operator already knows from config.yaml.

type SettingsResponse added in v0.1.5

type SettingsResponse struct {
	Applied         []string   `json:"applied"`
	RestartRequired []string   `json:"restart_required"`
	ConfigPath      string     `json:"config_path,omitempty"`
	Runtime         RuntimeDTO `json:"runtime"`
}

SettingsResponse is the JSON shape returned by PATCH /api/v1/settings. Applied lists the keys that took effect immediately; RestartRequired lists keys that were written to config.yaml but need a daemon restart to take effect.

type SpectrumDevice added in v0.2.3

type SpectrumDevice struct {
	Serial       string `json:"serial"`
	Driver       string `json:"driver"`
	Product      string `json:"product,omitempty"`
	Role         string `json:"role"`
	CenterHz     uint32 `json:"center_hz"`
	SampleRateHz uint32 `json:"sample_rate_hz"`
}

SpectrumDevice is the per-SDR descriptor returned by GET /api/v1/spectrum/devices. Mirrors the proto shape but stays JSON-self-contained for the same reason SDRStatus does.

type SpectrumFrame added in v0.2.3

type SpectrumFrame struct {
	TimestampNs  int64     `json:"ts_ns"`
	CenterHz     uint32    `json:"center_hz"`
	SampleRateHz uint32    `json:"sample_rate_hz"`
	Bins         []float32 `json:"bins"`
}

SpectrumFrame is the wire shape of one frame on the WS stream.

type SpectrumProvider added in v0.2.3

type SpectrumProvider interface {
	// Devices returns the list of SDRs that can be streamed.
	Devices() []SpectrumDevice
	// OpenStream starts a per-request producer for the given device.
	// FFTSize must be a positive power of two; fps caps the frame
	// rate (zero picks a default). Returns an output channel that
	// closes when ctx cancels or the device disappears, and a
	// cleanup func the caller MUST invoke on disconnect.
	OpenStream(ctx context.Context, serial string, fftSize int, fps float64) (<-chan SpectrumFrame, func(), error)
	// Tune programs the named SDR's centre frequency in Hz. Used by
	// the web panel's click-to-tune handler. Returns an error if the
	// serial isn't known or the underlying device rejects the value.
	Tune(serial string, centerHz uint32) error
}

SpectrumProvider is the daemon-side abstraction the api package consumes. The daemon (cmd/gophertrunk) implements it on top of the iqtap broker map; tests can substitute a fake. Kept narrow so the api package stays free of dependencies on internal/sdr.

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"`
	P25Phase1DemodMode     string  `json:"p25_phase1_demod_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"`
	NXDNDeviationHz        float64 `json:"nxdn_deviation_hz,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"`
	Stream      bool   `json:"stream"`
	Record      bool   `json:"record"`
	Mute        bool   `json:"mute"`
	Icon        string `json:"icon,omitempty"`
}

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.

type TuneRequest added in v0.2.3

type TuneRequest struct {
	CenterHz uint32 `json:"center_hz"`
}

TuneRequest is the body shape POST'd to /api/v1/spectrum/devices/{serial}/tune.

type UnitRegistrationDTO added in v0.1.7

type UnitRegistrationDTO struct {
	System   string `json:"system"`
	Protocol string `json:"protocol"`
	SourceID uint32 `json:"source_id"`
	WACN     uint32 `json:"wacn"`
	SystemID uint16 `json:"system_id"`
	Response string `json:"response"`
}

UnitRegistrationDTO mirrors trunking.UnitRegistration.

Directories

Path Synopsis
pb
v1
Package rigctld implements a subset of Hamlib's rigctld TCP wire protocol so external amateur-radio tooling (Cloudlog, N1MM, GridTracker, PSTRotator, satellite trackers, logging programs) can read and set the frequency of one of GopherTrunk's SDRs over the network.
Package rigctld implements a subset of Hamlib's rigctld TCP wire protocol so external amateur-radio tooling (Cloudlog, N1MM, GridTracker, PSTRotator, satellite trackers, logging programs) can read and set the frequency of one of GopherTrunk's SDRs over the network.

Jump to

Keyboard shortcuts

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