Documentation
¶
Overview ¶
Package api exposes GopherTrunk's read + write control surface, the streaming events feed, and the gRPC mirror of the same state.
The default daemon links both the HTTP+SSE+WebSocket server defined here and the gRPC server in grpc.go. Mutation endpoints (end call, set talkgroup priority/lockout/scan, retention sweep, tone-out reset, scanner cockpit) are gated behind api.allow_mutations so a daemon bound to a trusted interface can expose them while a default build stays read-only.
gRPC bindings (proto/*.proto under the repo root) generate Go code at internal/api/pb/v1 when `make proto` is invoked with protoc and the standard plugins installed.
Layout:
server.go HTTP server lifecycle (Run, Close), routing, mux handlers.go REST read handlers (health/version/systems/talkgroups/calls/devices) handlers_mutations.go REST mutation handlers (end-call, retention, talkgroup, tone-reset) handlers_scanner.go Scanner cockpit REST handlers (status + 6 mutation routes) sse.go Server-Sent Events stream of internal/events bus events ws.go WebSocket bridge that streams the same events as JSON grpc.go gRPC server: SystemService + TalkgroupService + AudioService types.go JSON-friendly DTOs (mirroring the proto definitions)
Index ¶
- type ADSBProvider
- type AISMessageDTO
- type AISProvider
- type APRSPacketDTO
- type APRSProvider
- type ActiveCallDTO
- type AffiliationDTO
- type AffiliationProvider
- type AircraftReportDTO
- type AudioController
- type AudioPublisher
- func (p *AudioPublisher) Close() error
- func (p *AudioPublisher) Run(ctx context.Context) error
- func (p *AudioPublisher) Stats() AudioPublisherStats
- func (p *AudioPublisher) Subscribe(filter AudioSubFilter) *audioSubscriber
- func (p *AudioPublisher) Unsubscribe(sub *audioSubscriber)
- func (p *AudioPublisher) WritePCM(deviceSerial string, samples []int16) error
- type AudioPublisherStats
- type AudioStatusDTO
- type AudioSubFilter
- type AuthConfig
- type AuthMode
- type BookmarkDTO
- type BookmarkProvider
- type BroadcastStatusProvider
- type CORSConfig
- type CallEncryptionDTO
- type CallEndDTO
- type CallRow
- type CallStartDTO
- type ConfigWriter
- type ConvChannelStatusDTO
- type ConvScannerStatusDTO
- type DSCMessageDTO
- type DSCProvider
- type DevicesProvider
- type DiagProvider
- type EngineMutator
- type EngineSnapshot
- type EventDTO
- type GRPCServer
- func (g *GRPCServer) GetRID(_ context.Context, req *apiv1.GetRIDRequest) (*apiv1.GetRIDResponse, error)
- func (g *GRPCServer) GetSystem(_ context.Context, req *apiv1.GetSystemRequest) (*apiv1.GetSystemResponse, error)
- func (g *GRPCServer) GetTalkgroup(_ context.Context, req *apiv1.GetTalkgroupRequest) (*apiv1.GetTalkgroupResponse, error)
- func (g *GRPCServer) ListActiveCalls(_ context.Context, _ *apiv1.ListActiveCallsRequest) (*apiv1.ListActiveCallsResponse, error)
- func (g *GRPCServer) ListRIDHistory(ctx context.Context, req *apiv1.ListRIDHistoryRequest) (*apiv1.ListRIDHistoryResponse, error)
- func (g *GRPCServer) ListRIDs(_ context.Context, _ *apiv1.ListRIDsRequest) (*apiv1.ListRIDsResponse, error)
- func (g *GRPCServer) ListSystems(_ context.Context, _ *apiv1.ListSystemsRequest) (*apiv1.ListSystemsResponse, error)
- func (g *GRPCServer) ListTalkgroups(_ context.Context, _ *apiv1.ListTalkgroupsRequest) (*apiv1.ListTalkgroupsResponse, error)
- func (g *GRPCServer) Run(ctx context.Context) error
- func (g *GRPCServer) Stop()
- func (g *GRPCServer) StreamAudio(req *apiv1.StreamAudioRequest, srv apiv1.AudioService_StreamAudioServer) error
- type GRPCServerOptions
- type GrantDTO
- type HealthDTO
- type HistoryFilter
- type HistoryQuery
- type IQFrame
- type IQPoint
- type ImportCommitResult
- type ImportPreviewResponse
- type ImportSource
- type ImportSourceKind
- type Importer
- type LocationFix
- type LocationQuery
- type M17LinkSetupDTO
- type M17Provider
- type MDC1200MessageDTO
- type MDC1200Provider
- type ManualTuneRequest
- type PagerMessageDTO
- type PagerProvider
- type ParsedSystemDTO
- type PatchDTO
- type RIDDTO
- type RetentionSweeper
- type RuntimeDTO
- type RuntimeProvider
- type ScannerCockpit
- type ScannerStatus
- type Server
- type ServerOptions
- type SettingsApplier
- type SettingsPatchRequest
- type SettingsResponse
- type SpectrumDevice
- type SpectrumFrame
- type SpectrumProvider
- type SystemDTO
- type SystemHuntStatusDTO
- type TalkgroupDTO
- type ToneDetectorReset
- type ToneProfileDTO
- type TuneRequest
- type UnitRegistrationDTO
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 ¶
NewAudioPublisher constructs a publisher backed by the supplied bus. The bus subscription happens at construction time so callers can publish CallStart events before Run begins without losing them.
func (*AudioPublisher) Close ¶
func (p *AudioPublisher) Close() error
Close releases the bus subscription. Safe to call multiple times.
func (*AudioPublisher) Run ¶
func (p *AudioPublisher) Run(ctx context.Context) error
Run drains bus events until ctx cancels, maintaining the per- device-serial Grant map that WritePCM consults. Returns ctx.Err() on shutdown.
func (*AudioPublisher) Stats ¶
func (p *AudioPublisher) Stats() AudioPublisherStats
func (*AudioPublisher) Subscribe ¶
func (p *AudioPublisher) Subscribe(filter AudioSubFilter) *audioSubscriber
Subscribe registers a new subscriber and returns its frame channel. Caller MUST call Unsubscribe(ret) before letting the channel go out of scope — leaked subscribers keep the publisher fanning frames into them forever. Channel capacity defaults to 64 frames (≈ 1 second of audio at typical chunk sizes).
func (*AudioPublisher) Unsubscribe ¶
func (p *AudioPublisher) Unsubscribe(sub *audioSubscriber)
Unsubscribe removes the subscriber. Idempotent. After unsubscribing the channel is closed so any reader sees io.EOF / channel-closed.
func (*AudioPublisher) WritePCM ¶
func (p *AudioPublisher) WritePCM(deviceSerial string, samples []int16) error
WritePCM satisfies composer.PCMSink. Builds one AudioFrame per call and fans it to every subscriber whose filter matches. A missing Grant (composer wrote PCM before CallStart landed) drops the frame silently — the publisher only emits frames that carry full talkgroup context.
type AudioPublisherStats ¶
Stats reports cumulative publisher counters. Useful for the /metrics surface and for diagnosing slow consumers.
type AudioStatusDTO ¶
type AudioStatusDTO struct {
// BackendEnabled is true when a real audio sink is attached.
// False = audio.enabled was off in config or the backend
// failed to init; PATCH still works but takes effect only on
// the recorder gate.
BackendEnabled bool `json:"backend_enabled"`
// SampleRate is the host playback rate in Hz.
SampleRate uint32 `json:"sample_rate"`
// Volume is the software gain (0..1).
Volume float32 `json:"volume"`
// Muted reports the mute state.
Muted bool `json:"muted"`
// RecordingEnabled is the recorder's "create new sessions"
// gate. In-flight sessions are unaffected.
RecordingEnabled bool `json:"recording_enabled"`
// DropsTotal is a monotonically increasing counter of PCM
// samples lost because the playback queue was full.
DropsTotal uint64 `json:"drops_total"`
}
AudioStatusDTO is the JSON shape returned by GET /api/v1/audio. Mirrors the AudioController interface so the TUI doesn't need to know how the daemon plumbed the player + recorder together.
type AudioSubFilter ¶
type AudioSubFilter struct {
DeviceSerials []string
TalkgroupIDs []uint32
// IncludeRaw mirrors the proto flag. Until WriteRawFrame is
// wired into the publisher (digital-voice raw frames are a
// follow-up), this just selects whether to surface PCM frames
// at all — false is the safe default that's never going to
// break a caller that didn't ask for audio.
IncludeRaw bool
}
AudioSubFilter is what callers pass to Subscribe to scope the frames they receive. Empty fields match everything.
type AuthConfig ¶
type AuthConfig struct {
// Mode picks the policy. See AuthMode for the trade-offs.
Mode AuthMode
// Token is the inline bearer token. Compared with
// crypto/subtle.ConstantTimeCompare. Prefer TokenFile so the
// token doesn't live in config.yaml — but inline is supported
// for ephemeral / test setups.
Token string
// TokenFile is a path to a file containing the bearer token
// (whitespace stripped). Read at startup; the daemon reloads it
// on every request so operators can rotate tokens without a
// restart. Empty disables file-based tokens.
TokenFile string
// TrustedNetworks is a list of CIDRs whose source addresses
// bypass the bearer-token check under AuthModeAuto. Loopback
// (127.0.0.1/32 and ::1/128) is implicitly trusted under
// AuthModeAuto and does not need to be listed here.
TrustedNetworks []string
}
AuthConfig configures the bearer-token auth middleware.
type AuthMode ¶
type AuthMode uint8
AuthMode selects the auth policy applied to mutation endpoints.
AuthModeAuto (default): the policy depends on the listener binding. Loopback (127.0.0.1 / ::1) and any address in AuthConfig.TrustedNetworks bypass the bearer-token check — peer-cred via kernel-enforced reachability is treated as a reasonable trust proxy on a single-host operator's box. Anything else (0.0.0.0 / a public interface) requires a valid Bearer token on every mutation request, and the daemon refuses to start without a configured token.
AuthModeRequired: every mutation request must carry a valid Bearer token regardless of source, even loopback. Useful when the daemon shares a host with untrusted users.
AuthModeDisabled: bypass the bearer check entirely (the legacy `allow_mutations: true` behaviour). Mutations are wide open — for backwards-compatible single-host workflows where the operator is the only one with shell access. The daemon logs a warning at startup so this isn't accidentally enabled in a hostile environment.
func ParseAuthMode ¶
ParseAuthMode maps a config string into an AuthMode. Recognised values (case-insensitive):
"" → 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.
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 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 ¶
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 ¶
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.UnimplementedRIDServiceServer
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) GetRID ¶ added in v0.2.4
func (g *GRPCServer) GetRID(_ context.Context, req *apiv1.GetRIDRequest) (*apiv1.GetRIDResponse, error)
func (*GRPCServer) GetSystem ¶
func (g *GRPCServer) GetSystem(_ context.Context, req *apiv1.GetSystemRequest) (*apiv1.GetSystemResponse, error)
func (*GRPCServer) GetTalkgroup ¶
func (g *GRPCServer) GetTalkgroup(_ context.Context, req *apiv1.GetTalkgroupRequest) (*apiv1.GetTalkgroupResponse, error)
func (*GRPCServer) ListActiveCalls ¶
func (g *GRPCServer) ListActiveCalls(_ context.Context, _ *apiv1.ListActiveCallsRequest) (*apiv1.ListActiveCallsResponse, error)
func (*GRPCServer) ListRIDHistory ¶ added in v0.2.4
func (g *GRPCServer) ListRIDHistory(ctx context.Context, req *apiv1.ListRIDHistoryRequest) (*apiv1.ListRIDHistoryResponse, error)
func (*GRPCServer) ListRIDs ¶ added in v0.2.4
func (g *GRPCServer) ListRIDs(_ context.Context, _ *apiv1.ListRIDsRequest) (*apiv1.ListRIDsResponse, error)
func (*GRPCServer) ListSystems ¶
func (g *GRPCServer) ListSystems(_ context.Context, _ *apiv1.ListSystemsRequest) (*apiv1.ListSystemsResponse, error)
func (*GRPCServer) ListTalkgroups ¶
func (g *GRPCServer) ListTalkgroups(_ context.Context, _ *apiv1.ListTalkgroupsRequest) (*apiv1.ListTalkgroupsResponse, error)
func (*GRPCServer) Run ¶
func (g *GRPCServer) Run(ctx context.Context) error
Run binds the listener and serves until ctx cancels.
func (*GRPCServer) StreamAudio ¶
func (g *GRPCServer) StreamAudio(req *apiv1.StreamAudioRequest, srv apiv1.AudioService_StreamAudioServer) error
--- AudioService --- StreamAudio fans decoded PCM from the per-call composer to the gRPC client. The request's device_serials / talkgroup_ids filters act as allow-lists; empty matches everything. PCM samples are 16-bit little-endian mono at the recorder's configured rate (typically 8 kHz).
Returns:
codes.Unavailable when the daemon was started without an audio publisher (no composer wired, audio off, or older configuration). nil on graceful client cancel. any send-side error from the gRPC stream — typically the caller hung up.
type GRPCServerOptions ¶
type GRPCServerOptions struct {
Addr string
Systems []trunking.System
Talkgroups *trunking.TalkgroupDB
// 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
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 ¶
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
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.
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.
Source Files
¶
- api.go
- audio_publisher.go
- auth.go
- cors.go
- diag.go
- grpc.go
- handlers.go
- handlers_adsb.go
- handlers_ais.go
- handlers_aprs.go
- handlers_audio.go
- handlers_audio_stream.go
- handlers_bookmarks.go
- handlers_broadcast.go
- handlers_dsc.go
- handlers_import.go
- handlers_locations.go
- handlers_m17.go
- handlers_mdc1200.go
- handlers_mutations.go
- handlers_pager.go
- handlers_rids.go
- handlers_runtime.go
- handlers_scanner.go
- handlers_settings.go
- server.go
- spectrum.go
- sse.go
- storage_adapter.go
- types.go
- ws.go
Directories
¶
| Path | Synopsis |
|---|---|
|
pb
|
|
|
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. |