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
- func (p *AudioPublisher) WritePCMForCall(deviceSerial string, callID uint64, samples []int16) error
- func (p *AudioPublisher) WriteRawFrame(deviceSerial, vocoder string, frame []byte) error
- func (p *AudioPublisher) WriteRawFrameForCall(deviceSerial string, callID uint64, vocoder string, frame []byte) error
- type AudioPublisherStats
- type AudioStatusDTO
- type AudioSubFilter
- type AuthConfig
- type AuthMode
- type BandPlanSlotDTO
- type BookmarkDTO
- type BookmarkProvider
- type BroadcastStatusProvider
- type CORSConfig
- type CallEncryptionDTO
- type CallEndDTO
- type CallRow
- type CallStartDTO
- type CaptureProvider
- type ConfigActivateRequest
- type ConfigActivateResponse
- type ConfigActivator
- type ConfigBuilderOptions
- type ConfigFileInfo
- type ConfigListResponse
- type ConfigLoadResponse
- type ConfigSaveRequest
- type ConfigSaveResponse
- type ConfigValidateRequest
- type ConfigWriter
- type ConvChannelStatusDTO
- type ConvScannerStatusDTO
- type DMRBandPlanDTO
- type DMRBandPlanLCNDTO
- type DMRBandPlanLearnedDTO
- type DMRGrantObservedDTO
- type DMRLinearBandPlanDTO
- type DSCMessageDTO
- type DSCProvider
- type DevicesProvider
- type DiagProvider
- type DibitArray
- type DocLink
- 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 HuntCaptureRequest
- type HuntCaptureResult
- type HuntCockpit
- type HuntRRReport
- type HuntStartRequest
- type HuntStatus
- type IQFrame
- type IQPoint
- type ImportCommitResult
- type ImportPreviewResponse
- type ImportSource
- type ImportSourceKind
- type ImportTalkgroupDTO
- type Importer
- type LoRaFrameDTO
- type LoRaProvider
- type LocationFix
- type LocationQuery
- type M17LinkSetupDTO
- type M17Provider
- type MDC1200MessageDTO
- type MDC1200Provider
- type ManualTuneRequest
- type MixerFrame
- type MixerProvider
- type NeighborDTO
- type NetworkReporter
- type NetworkTopologyProvider
- type PagerMessageDTO
- type PagerProvider
- type ParsedSystemDTO
- type PatchDTO
- type RFScopeOptions
- type RIDDTO
- type RRSystemResponse
- type RRVerifyResponse
- type RetentionSweeper
- type RuntimeDTO
- type RuntimeProvider
- type ScannerCockpit
- type ScannerStatus
- type Server
- type ServerOptions
- type SettingsApplier
- type SettingsPatchRequest
- type SettingsResponse
- type SiglabOptions
- type SiteDTO
- type SitesProvider
- type SpectrumDevice
- type SpectrumFrame
- type SpectrumProvider
- type SymbolFrame
- type SymbolProvider
- type SystemDTO
- type SystemHuntStatusDTO
- type TalkgroupCSVRow
- type TalkgroupDTO
- type ToneDetectorReset
- type ToneProfileDTO
- type TuneRequest
- type UnitRegistrationDTO
- type UnitToUnitRequestDTO
- type ValidationError
- type ValidationResult
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"`
// Following is true when a voice tuner is actively decoding this call.
// False for calls the control channel has announced but no tuner is
// following (every voice device busy) — surfaced so the operator sees all
// talkgroups up on the system, not only the ones being decoded. An
// unfollowed call has an empty DeviceSerial.
Following bool `json:"following"`
}
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"`
// RFSSID / SiteID name the radio's serving site (a genuine
// RID→site fix, unlike grant-site) and NAC is the coarse control
// channel access code (issue #698). Zero/omitted on non-P25
// affiliations and until the site's status broadcast has decoded.
RFSSID uint8 `json:"rfss_id,omitempty"`
SiteID uint8 `json:"site_id,omitempty"`
NAC uint16 `json:"nac,omitempty"`
}
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
// BackendError returns the reason the backend isn't attached when
// audio was enabled but the sink failed to open (e.g. the ioctl
// device rejected the pinned format). Empty when audio is healthy
// or was intentionally disabled — lets the UI distinguish "off by
// config" from "tried to start and failed".
BackendError() string
}
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 is audioSubChanCap frames (a few seconds 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. It fans live PCM to every subscriber whose filter matches, INDEPENDENT of whether a CallStart grant has been observed for this device.
The grant cache is fed by the publisher's own events-bus subscription, which the bus can drop under load (full channel) — and on a busy control channel with multiple voice taps that happens often enough that gating audio on it left the WebUI live stream silent while disk recordings (a separate, grant-free PCM sink) kept working (issue #598). Neither stream consumer reads the frame's Grant for its payload: the HTTP handler writes raw PCM samples and gRPC StreamAudio forwards the frame verbatim. So when the grant is unknown we still emit the PCM with a zero Grant. Unfiltered and device-serial-only subscribers receive it; only talkgroup-filtered subscribers are skipped, since their predicate can't be evaluated without a known GroupID and we must not leak another talkgroup's audio to them.
The frame is built lazily on the first matching subscriber so the no-subscriber and no-match paths stay allocation-free.
func (*AudioPublisher) WritePCMForCall ¶ added in v0.4.9
func (p *AudioPublisher) WritePCMForCall(deviceSerial string, callID uint64, samples []int16) error
WritePCMForCall is WritePCM plus the CallID the PCM was decoded for. It satisfies voice.DecodedPCMCallSink so the recorder's decoded-audio tap can fence a stale frame from the call that previously held a reused voice-tap serial: when the live grant for the serial has already flipped to a newer call (its CallStart updated p.grants), PCM still draining from the older call would otherwise be fanned out labelled with — and leaked to subscribers filtered on — the new call's talkgroup. Dropping the mismatched frame closes that cross-call audio-bleed window. A zero CallID on either side matches (un-stamped / synthetic / analog calls), preserving WritePCM's behaviour.
func (*AudioPublisher) WriteRawFrame ¶ added in v0.4.9
func (p *AudioPublisher) WriteRawFrame(deviceSerial, vocoder string, frame []byte) error
WriteRawFrame fans a verbatim, un-decoded vocoder frame (IMBE / AMBE+2) to subscribers that asked for raw bytes (filter.IncludeRaw). It satisfies voice.RawFrameSink so the recorder's raw tap can stream the same bytes it writes to the .raw sidecar. vocoder is the codec name that produced the session (may be empty for protocols with no in-process decoder, e.g. ProVoice); it labels the wire frame but is not required.
func (*AudioPublisher) WriteRawFrameForCall ¶ added in v0.4.9
func (p *AudioPublisher) WriteRawFrameForCall(deviceSerial string, callID uint64, vocoder string, frame []byte) error
WriteRawFrameForCall is WriteRawFrame plus the CallID the frame was decoded for. It satisfies voice.RawFrameCallSink so a raw frame still draining from the call that previously held a reused voice-tap serial is fenced the same way WritePCMForCall fences decoded PCM — dropped rather than fanned out labelled with (and leaked to subscribers filtered on) the newer call's talkgroup. A zero CallID on either side matches, preserving WriteRawFrame's behaviour.
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"`
// BackendError carries the reason the sink failed to open when
// audio was enabled (e.g. the ioctl device rejected S16_LE mono).
// Omitted when audio is healthy or intentionally disabled, so a
// "Tap to enable audio" that produced no sound can show the cause.
BackendError string `json:"backend_error,omitempty"`
// 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"`
// StreamSubscribers is the number of live /audio/stream + gRPC
// StreamAudio consumers currently connected. A WebUI "Tap to
// enable audio" tap that produced no sound should still show >=1
// here while the tab is open — letting an operator tell a wiring
// problem apart from "no call is active". Zero when no publisher
// is wired.
StreamSubscribers int `json:"stream_subscribers"`
// StreamDropsTotal is the cumulative PCM samples dropped to slow
// stream subscribers (distinct from DropsTotal, which counts the
// host playback queue).
StreamDropsTotal uint64 `json:"stream_drops_total"`
// StreamTrackedGrants is the number of device serials the
// publisher currently has call context for. PCM still streams
// without a tracked grant (issue #598); this is a diagnostic.
StreamTrackedGrants int `json:"stream_tracked_grants"`
}
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 and is purely additive: false
// (the default, and what the WebUI sends) means PCM only; true means
// PCM *and* the verbatim un-decoded vocoder frames (IMBE / AMBE+2)
// the recorder's raw tap forwards via WriteRawFrame. Existing PCM-only
// callers are unaffected — raw frames are fanned solely to subscribers
// that opted in here.
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 BandPlanSlotDTO ¶ added in v0.5.6
type BandPlanSlotDTO struct {
ChannelID uint8 `json:"channel_id"`
BaseHz uint64 `json:"base_hz"`
SpacingHz uint32 `json:"spacing_hz"`
BandwidthHz uint32 `json:"bandwidth_hz,omitempty"`
TxOffsetHz int64 `json:"tx_offset_hz,omitempty"`
AccessTDMA bool `json:"access_tdma,omitempty"`
}
BandPlanSlotDTO is one P25 IDEN_UP frequency-band entry, mirroring trunking.TopoBandPlanSlot / trunking.ReportBand. It maps a channel ID to a downlink base frequency, channel spacing, and (signed) transmit offset — the table SDRtrunk surfaces in its "Details" tab and the data needed to map a neighbour's channel_id/channel_number back to an absolute frequency.
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"`
// Timeslot is the 1-based DMR TDMA slot (0 = n/a, 1 = TS1, 2 = TS2).
Timeslot uint8 `json:"timeslot,omitempty"`
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"`
// SignalDbFS is the call's mean received channel power in dBFS
// (channel power, not calibrated RSSI or SNR). nil when unmeasured.
SignalDbFS *float64 `json:"signal_dbfs,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 CaptureProvider ¶ added in v0.3.4
type CaptureProvider interface {
// Devices returns the SDRs that can be captured from.
Devices() []SpectrumDevice
// Capture records seconds worth of raw IQ from the named device and
// returns the complex samples plus the device's current sample rate and
// centre frequency. ctx cancels an in-flight capture.
Capture(ctx context.Context, serial string, seconds int) (iq []complex64, sampleRateHz, centerHz uint32, err error)
}
CaptureProvider taps a live SDR for a fixed-length raw-IQ capture. The daemon (cmd/gophertrunk) implements it over its iqtap.Broker map; nil keeps the POST /api/v1/siglab/capture route returning 503 so a build without SDRs (or the offline `siglab serve` console) doesn't pretend to have a tuner to record from. Kept narrow — and reusing the SpectrumDevice DTO — so the api package stays free of dependencies on internal/sdr, mirroring SpectrumProvider.
type ConfigActivateRequest ¶ added in v0.3.9
type ConfigActivateRequest struct {
Path string `json:"path"`
// Mode is "reload" (default — hot-apply what it can) or "restart"
// (re-exec the daemon so every field takes effect).
Mode string `json:"mode,omitempty"`
}
ConfigActivateRequest is the body of POST /api/v1/config/activate.
type ConfigActivateResponse ¶ added in v0.3.9
type ConfigActivateResponse struct {
Path string `json:"path"`
Mode string `json:"mode"`
Applied []string `json:"applied,omitempty"`
RestartRequired []string `json:"restart_required,omitempty"`
Restarting bool `json:"restarting,omitempty"`
}
ConfigActivateResponse reports the outcome. For reload it carries the applied / restart-required field keys; for restart it just echoes the target (the daemon is tearing down to re-exec).
type ConfigActivator ¶ added in v0.3.9
type ConfigActivator interface {
// ActivateReload re-points the daemon at path and reloads it in
// place, returning the field keys it hot-applied vs. those that need
// a restart to take effect.
ActivateReload(path string) (applied, restartRequired []string, err error)
// ActivateRestart re-execs the daemon into path so every field takes
// effect. It returns once the restart is scheduled (the process tears
// down and re-execs shortly after); an error means the restart could
// not be initiated (e.g. the new config does not load).
ActivateRestart(path string) error
}
ConfigActivator loads/hot-swaps the config file the daemon runs on. Supplied by the daemon; nil on the standalone `config serve` (which has no live daemon to reconfigure), so the activate endpoint returns 503.
type ConfigBuilderOptions ¶ added in v0.3.6
type ConfigBuilderOptions struct {
// Enabled mounts the /api/v1/config/* routes and the builder SPA.
Enabled bool
// ConfigDir, when set, is the single directory the builder lists and
// constrains saves to. When empty the builder uses
// config.CandidateDirs() (the same locations the daemon discovers).
ConfigDir string
// Assets is the builder SPA, served at /config/ when the daemon mounts
// the subsystem as a secondary tree. The standalone `config serve`
// command instead puts the SPA in ServerOptions.WebAssets (served at
// /), leaving this nil.
Assets fs.FS
// RadioReference holds the credentials for the RR browse/import
// routes. When AppKey is empty the RR routes return 503.
RadioReference radioreference.Auth
}
ConfigBuilderOptions configure the web Config Builder/Editor subsystem.
type ConfigFileInfo ¶ added in v0.3.6
type ConfigFileInfo struct {
Path string `json:"path"`
Dir string `json:"dir"`
Name string `json:"name"`
Size int64 `json:"size"`
Modified string `json:"modified"` // RFC3339
Valid bool `json:"valid"`
Error string `json:"error,omitempty"`
}
ConfigFileInfo describes one discovered config file.
type ConfigListResponse ¶ added in v0.3.6
type ConfigListResponse struct {
Dirs []string `json:"dirs"`
Files []ConfigFileInfo `json:"files"`
}
ConfigListResponse is the body of GET /api/v1/config/files.
type ConfigLoadResponse ¶ added in v0.3.6
type ConfigLoadResponse struct {
Path string `json:"path"`
Config config.Config `json:"config"`
Validation ValidationResult `json:"validation"`
Mtime int64 `json:"mtime"`
}
ConfigLoadResponse is the body of GET /api/v1/config/file.
type ConfigSaveRequest ¶ added in v0.3.6
type ConfigSaveRequest struct {
Path string `json:"path"`
Config config.Config `json:"config"`
Mtime int64 `json:"mtime,omitempty"`
Overwrite bool `json:"overwrite"`
// Talkgroups optionally writes Trunk Recorder–style CSV sidecars
// keyed by the relative TalkgroupFile path referenced in the config.
Talkgroups map[string][]TalkgroupCSVRow `json:"talkgroups,omitempty"`
}
ConfigSaveRequest is the body of POST /api/v1/config/file.
type ConfigSaveResponse ¶ added in v0.3.6
type ConfigSaveResponse struct {
Path string `json:"path"`
Mtime int64 `json:"mtime"`
TalkgroupCSVs []string `json:"talkgroup_csvs,omitempty"`
}
ConfigSaveResponse is the body returned after a successful save.
type ConfigValidateRequest ¶ added in v0.3.6
type ConfigValidateRequest struct {
Config config.Config `json:"config"`
Section string `json:"section,omitempty"`
}
ConfigValidateRequest is the body of POST /api/v1/config/validate. When Section is empty the whole config is validated (one error per section).
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 DMRBandPlanDTO ¶ added in v0.4.1
type DMRBandPlanDTO struct {
Linear *DMRLinearBandPlanDTO `json:"linear,omitempty"`
Table []DMRBandPlanLCNDTO `json:"table,omitempty"`
}
DMRBandPlanDTO mirrors trunking.DMRBandPlan: exactly one of Linear or Table is populated.
type DMRBandPlanLCNDTO ¶ added in v0.4.1
DMRBandPlanLCNDTO is one explicit LCN→downlink-frequency entry.
type DMRBandPlanLearnedDTO ¶ added in v0.4.1
type DMRBandPlanLearnedDTO struct {
System string `json:"system"`
BaseHz uint32 `json:"base_hz,omitempty"`
SpacingHz uint32 `json:"spacing_hz,omitempty"`
Offset int8 `json:"offset,omitempty"`
Table []DMRBandPlanLCNDTO `json:"table,omitempty"`
NumPairs int `json:"num_pairs"`
Confidence float64 `json:"confidence"`
ResidualHz uint32 `json:"residual_hz,omitempty"`
}
DMRBandPlanLearnedDTO mirrors events.DMRBandPlanLearned — the result of the DMR Tier III LCN autoconfig learner fitting a band plan from observed (LCN, frequency) pairs. A linear plan sets BaseHz/SpacingHz/Offset with an empty Table; an irregular plan populates Table. (#638)
type DMRGrantObservedDTO ¶ added in v0.4.1
type DMRGrantObservedDTO struct {
System string `json:"system"`
ColorCode uint8 `json:"color_code"`
LCN uint16 `json:"lcn"`
Timeslot uint8 `json:"timeslot"`
GroupID uint32 `json:"group_id"`
SourceID uint32 `json:"source_id"`
CCFreqHz uint32 `json:"cc_freq_hz"`
At time.Time `json:"at"`
}
DMRGrantObservedDTO mirrors events.DMRGrantObserved — a DMR Tier III voice-grant CSBK seen by the control channel before the LCN is resolved to a frequency. Surfaced so operators can watch the autoconfig learner's raw input in the CC Activity log. Timeslot uses the raw CSBK encoding (0 = TS1, 1 = TS2). (#638)
type DMRLinearBandPlanDTO ¶ added in v0.4.1
type DMRLinearBandPlanDTO struct {
BaseHz uint32 `json:"base_hz"`
SpacingHz uint32 `json:"spacing_hz"`
Offset int8 `json:"offset,omitempty"`
}
DMRLinearBandPlanDTO mirrors trunking.DMRLinearBandPlan — a regular base+spacing grid (freq = base + (lcn-offset)*spacing).
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). offsetHz mixes a frequency offset (relative to
// the device centre) down to baseband before decimation so an
// off-centre channel can be viewed clear of the centre DC spike;
// zero keeps the centre-tuned view. The provider clamps the
// magnitude to the device's Nyquist. Returns the wire-frame
// channel and a cleanup func the caller MUST invoke on disconnect.
OpenIQStream(ctx context.Context, serial string, targetRateSPS uint32, offsetHz int32) (<-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 DibitArray ¶ added in v0.3.7
type DibitArray []uint8
DibitArray marshals a dibit stream as a JSON number array. Go's encoding/json renders []byte/[]uint8 as a base64 string, which the web client (expecting number[]) silently drops — this forces the array form. Unmarshal uses the default []uint8 path (which accepts a JSON number array), so round-trips still work.
func (DibitArray) MarshalJSON ¶ added in v0.3.7
func (d DibitArray) MarshalJSON() ([]byte, error)
MarshalJSON emits the dibits as [n,n,...]; a nil/empty slice becomes [] so the field is always an array, never null, on the wire.
type DocLink ¶ added in v0.3.6
type DocLink struct {
Title string `json:"title"`
URL string `json:"url"`
Description string `json:"description"`
Instructions string `json:"instructions"`
}
DocLink carries a config section's shared help for the web Config Builder: the section Instructions plus a documentation link (Title/URL) opened in a new tab from the section's help affordance. Both come from the single Go source of truth in internal/configbuilder (Sections), so the web and the terminal builder present identical section help.
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
// ObservedCalls returns voice calls the control channel announced that no
// voice tuner is following (every device busy). The active-calls endpoint
// merges them with ActiveCalls so the operator sees every talkgroup up on
// the system, not just the ones a tuner is decoding.
ObservedCalls() []*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.
func EventToDTO ¶ added in v0.5.1
EventToDTO converts an internal events.Event to the JSON wire envelope used by the SSE/WS streams. It is exported so other sinks (e.g. the event JSONL file log) can serialise events in exactly the same shape clients already consume, rather than inventing a divergent format.
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
// Diagnostics is the shared error-diagnostics collector. When set
// with VerboseErrors (or a request carrying the gophertrunk-verbose
// metadata key), returned errors are decorated with the diagnostics
// banner + wrapped chain via a server interceptor.
Diagnostics *gtdiag.Collector
// VerboseErrors mirrors diagnostics.verbose_errors.
VerboseErrors bool
// DisplayLoc is the timezone string-formatted response timestamps render in.
// Nil means time.Local (the daemon passes display.timezone).
DisplayLoc *time.Location
// 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"`
// RFSSID / SiteID name the P25 site whose control channel granted
// the call, so consumers can label metrics by site instead of the
// opaque channel_id (issue #698). Zero (and omitted) on non-P25
// grants and until the site's RFSS Status TSBK has been decoded.
RFSSID uint8 `json:"rfss_id,omitempty"`
SiteID uint8 `json:"site_id,omitempty"`
// NAC is the control channel's Network Access Code (Phase 2: the
// NSB Color Code). Coarse — not unique per site — so consumers
// label by (rfss_id, site_id). Zero/omitted on non-P25 grants.
NAC uint16 `json:"nac,omitempty"`
// Timeslot is the 1-based TDMA slot (0 = n/a, 1 = TS1, 2 = TS2).
// Non-zero only for slotted protocols (DMR Tier III); identifies
// which of a carrier's two calls this is.
Timeslot uint8 `json:"timeslot,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, rendered in the configured display
// timezone (display.timezone; local by default). It carries an explicit
// offset, so a probe can still detect clock skew against its own clock.
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 HuntCaptureRequest ¶ added in v0.5.9
type HuntCaptureRequest struct {
FreqHz uint32 `json:"freq_hz"` // signal centre frequency to record
Seconds float64 `json:"seconds,omitempty"` // capture length (0 ⇒ a default)
Target string `json:"target,omitempty"` // "siglab" (default) | "cryptolab"
}
HuntCaptureRequest records one signal from the survey inventory and routes it to deeper analysis — the daemon counterpart of the CLI's -survey-capture.
type HuntCaptureResult ¶ added in v0.5.9
type HuntCaptureResult struct {
Path string `json:"path"` // the written .cfile
MetadataPath string `json:"metadata_path,omitempty"` // the sidecar
SampleRateHz float64 `json:"sample_rate_hz"`
CenterHz uint32 `json:"center_hz"`
Samples int `json:"samples"`
// Identify is a short SigLab identify summary (target=siglab), e.g.
// "p25 (0.82)" or "inconclusive".
Identify string `json:"identify,omitempty"`
// FramesPath is the cryptolab `ks` frames file (target=cryptolab), empty
// otherwise.
FramesPath string `json:"frames_path,omitempty"`
// Note carries an advisory (e.g. a wideband signal recorded as a slice).
Note string `json:"note,omitempty"`
}
HuntCaptureResult is what the daemon recorded and where it routed it.
type HuntCockpit ¶ added in v0.3.5
type HuntCockpit interface {
// Status returns the current run snapshot (+ discovered system when ready).
Status() HuntStatus
// Start launches a live hunt; returns the new run id, or an error (e.g. a
// run is already active, or no SDR is available).
Start(req HuntStartRequest) (runID int, err error)
// Stop cancels the active run; false when nothing is running.
Stop() bool
// Export serializes a run's discovered system in the named format
// (bundle|trunk-recorder|rr), returning the bytes + a suggested filename.
// id 0 = the latest run. Returns ErrHuntNoSuchRun for an unknown id.
Export(id int, format string) (data []byte, filename string, err error)
// ExportSurvey serializes a run's classified signal inventory in the named
// format (json|csv). id 0 = the latest run. Returns ErrHuntNoSuchRun for an
// unknown id, or an error when the run was a plain hunt (no survey).
ExportSurvey(id int, format string) (data []byte, filename string, err error)
// CaptureSignal records raw IQ of one signal from the inventory and routes
// it to SigLab/CryptoLab — the daemon side of the list-driven capture.
// Returns an "in progress" error while a run is active.
CaptureSignal(req HuntCaptureRequest) (HuntCaptureResult, error)
// Commit merges a run's discovered system into config.yaml (id 0 = latest),
// returning a human-readable list of changes.
Commit(id int, force, dryRun bool) (changes []string, err error)
// RadioReference cross-references a run's discovered system against
// RadioReference (id 0 = latest): duplicate-system hints plus a
// frequency/talkgroup diff. countyID and checkSIDs select the existing
// systems to compare against. Returns an error when RR credentials are not
// configured or no system has been discovered yet.
RadioReference(id, countyID int, checkSIDs []int) (HuntRRReport, error)
}
HuntCockpit is the API surface for the live system-discovery ("hunt") subsystem: read the current run state + discovered map, start/stop a blind spectrum-sweep run, and export or commit the result. The daemon supplies a single implementation wrapping the hunt.Manager; tests can stub it.
type HuntRRReport ¶ added in v0.4.2
type HuntRRReport struct {
Hints []hunt.DuplicateHint `json:"hints,omitempty"`
Diff *hunt.RRDiff `json:"diff,omitempty"`
Compared int `json:"compared"` // existing RR systems compared against
}
HuntRRReport is the GET /api/v1/hunt/radioreference response: the RadioReference cross-reference of a run's discovered system — ranked duplicate-system hints and a frequency/talkgroup diff vs the strongest match.
type HuntStartRequest ¶ added in v0.3.5
type HuntStartRequest struct {
Serial string `json:"serial,omitempty"`
Bands []string `json:"bands,omitempty"` // "low:high" MHz
Candidates []float64 `json:"candidates,omitempty"` // MHz
NoSweep bool `json:"no_sweep,omitempty"`
ConfirmBorrow bool `json:"confirm_borrow,omitempty"` // authorize borrowing an SDR with live wideband decode (suspends it for the run)
Survey bool `json:"survey,omitempty"` // classify+decode every carrier, not just trunking CCs
ClassifyOnly bool `json:"classify_only,omitempty"` // survey: classify, skip decoding
DetectWideband bool `json:"detect_wideband,omitempty"` // survey: detect+stitch signals wider than a channel (cellular/WiFi/OFDM)
DetectEncryption bool `json:"detect_encryption,omitempty"` // survey: entropy-triage unknown digital carriers for encryption
PersistSurvey bool `json:"persist_survey,omitempty"` // survey: stream carriers to NDJSON (crash-safe, resumable)
Resume bool `json:"resume,omitempty"` // survey: skip frequencies already in the NDJSON file
AutoGain bool `json:"auto_gain,omitempty"` // post-run: sweep gains on locked CCs, recommend the best
AutoGainSet string `json:"auto_gain_set,omitempty"` // optional comma-separated gain ladder in dB (e.g. "0,8.7,16.6")
MaxDwellSeconds float64 `json:"max_dwell_seconds,omitempty"`
MonitorSeconds float64 `json:"monitor_seconds,omitempty"` // >0: stream a locked CC in real time for up to this long (converge-and-stop), instead of buffering the dwell
Protocol string `json:"protocol,omitempty"`
DwellSeconds float64 `json:"dwell_seconds,omitempty"`
SweepDwellMs int `json:"sweep_dwell_ms,omitempty"`
PeakThresholdDb float64 `json:"peak_threshold_db,omitempty"`
MinSpacingHz uint32 `json:"min_spacing_hz,omitempty"`
FFTSize int `json:"fft_size,omitempty"`
MinConfidence float64 `json:"min_confidence,omitempty"`
Name string `json:"name,omitempty"`
State string `json:"state,omitempty"`
County string `json:"county,omitempty"`
Location string `json:"location,omitempty"`
}
HuntStartRequest is the POST /api/v1/hunt/start body. Frequencies are in MHz for operator convenience. With Bands set the hunt sweeps; with Candidates + NoSweep it probes the listed control channels directly.
type HuntStatus ¶ added in v0.3.5
type HuntStatus struct {
RunID int `json:"run_id"`
State string `json:"state"`
Running bool `json:"running"`
Mode string `json:"mode,omitempty"` // "hunt" | "survey"
Phase string `json:"phase,omitempty"`
Detail string `json:"detail,omitempty"`
Sites int `json:"sites"`
Talkgroups int `json:"talkgroups"`
SystemName string `json:"system_name,omitempty"`
// Signals is the classified-carrier inventory of a survey run (empty for a
// plain hunt) — the survey's primary result.
Signals []hunt.DetectedSignal `json:"signals,omitempty"`
Error string `json:"error,omitempty"`
System *hunt.DiscoveredSystem `json:"system,omitempty"`
Reports []hunt.CaptureReport `json:"reports,omitempty"`
// GainRecommendations / GainNote carry the result of an -auto-gain run.
GainRecommendations []hunt.GainRecommendation `json:"gain_recommendations,omitempty"`
GainNote string `json:"gain_note,omitempty"`
}
HuntStatus is the JSON shape returned by GET /api/v1/hunt — the live system-discovery run state plus the discovered system map once available.
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 ImportTalkgroupDTO ¶ added in v0.3.6
type ImportTalkgroupDTO struct {
Decimal uint32 `json:"decimal"`
AlphaTag string `json:"alpha_tag,omitempty"`
Description string `json:"description,omitempty"`
Tag string `json:"tag,omitempty"`
Group string `json:"group,omitempty"`
Mode string `json:"mode,omitempty"`
Encrypted bool `json:"encrypted,omitempty"`
}
ImportTalkgroupDTO is one parsed talkgroup, shaped to match the columns trunking.TalkGroup loads from a Trunk Recorder–style 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 LoRaFrameDTO ¶ added in v0.3.8
type LoRaFrameDTO struct {
ID int64 `json:"id"`
ReceivedAt time.Time `json:"received_at"`
FrequencyHz uint32 `json:"frequency_hz"`
SF int `json:"sf"`
CR int `json:"cr"`
BandwidthHz int `json:"bandwidth_hz"`
RSSI float64 `json:"rssi"`
SNR float64 `json:"snr"`
CFOHz float64 `json:"cfo_hz"`
CRCOK bool `json:"crc_ok"`
PayloadHex string `json:"payload_hex,omitempty"`
IsLoRaWAN bool `json:"is_lorawan"`
MType string `json:"mtype,omitempty"`
DevAddr string `json:"dev_addr,omitempty"`
FCnt uint32 `json:"fcnt"`
FPort int `json:"fport"`
MICOK bool `json:"mic_ok"`
Decrypted bool `json:"decrypted"`
Decoded string `json:"decoded,omitempty"`
}
LoRaFrameDTO is the JSON wire shape for the lora-log endpoint.
type LoRaProvider ¶ added in v0.3.8
LoRaProvider is the read surface the lora-log endpoint consumes. The daemon implements it on top of storage.LoRaLog; tests substitute a fake.
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, loc *time.Location) 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. loc is the display timezone the ReportedAt timestamp renders in (nil → time.Local).
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 MixerFrame ¶ added in v0.4.0
type MixerFrame struct {
TimestampNs int64 `json:"ts_ns"`
CenterHz uint32 `json:"center_hz"`
SampleRateHz uint32 `json:"sample_rate_hz"`
OffsetHz int32 `json:"offset_hz"`
CarrierOffsetHz float64 `json:"carrier_offset_hz"`
RawBins []float32 `json:"raw_bins"`
TunedBins []float32 `json:"tuned_bins"`
}
MixerFrame is the wire shape of one channel-baseband spectrum snapshot for the web Mixer plot (OP25's Raw Mixer / Tuned Mixer tabs). It mirrors symbolscope.MixerFrame so the api package stays free of a dependency on the DSP package; the MixerProvider interface bridges the two.
RawBins is the FFT of the channelized baseband as the receiver sees it (the carrier sits at the residual offset); TunedBins is the same window re-mixed by the receiver's carrier-offset estimate so a locked loop centres the carrier. Both are dBFS, FFT-shifted (Bins[0] is the lowest frequency, Bins[n/2] is DC), equal length.
type MixerProvider ¶ added in v0.4.0
type MixerProvider interface {
// OpenMixerStream starts a per-request symbol-recovery engine on the
// named device with the Mixer FFT taps enabled. proto selects the
// receiver ("p25-c4fm" / "p25-cqpsk"); offsetHz tunes an off-centre
// channel down to baseband before channelizing. Returns the wire-frame
// channel and a cleanup func the caller MUST invoke on disconnect.
OpenMixerStream(ctx context.Context, serial, proto string, offsetHz int32) (<-chan MixerFrame, func(), error)
}
MixerProvider is the daemon-side abstraction the mixer endpoint consumes. The daemon implements it on top of the iqtap broker map + internal/scanner/symbolscope; tests substitute a fake.
type NeighborDTO ¶ added in v0.5.3
type NeighborDTO struct {
RFSS uint8 `json:"rfss"`
Site uint8 `json:"site"`
ChannelID uint8 `json:"channel_id,omitempty"`
ChannelNumber uint16 `json:"channel_number,omitempty"`
DownlinkHz uint32 `json:"downlink_hz,omitempty"`
UplinkHz uint32 `json:"uplink_hz,omitempty"`
RFSSHex string `json:"rfss_hex,omitempty"`
SiteHex string `json:"site_hex,omitempty"`
}
NeighborDTO is one adjacent site advertised by a system's control channel, with band-plan-resolved downlink/uplink frequencies (zero when the band plan was not yet known). The hex renderings follow the P25 field convention, alongside the decimal fields.
type NetworkReporter ¶ added in v0.5.0
NetworkReporter is the optional capability a SitesProvider may implement to render the live, human-readable network-configuration report for a system (the SiteTracker does). GET /api/v1/systems/{name}/report uses it when the wired provider supports it; the report reflects the most recently camped site of the system. The bool is false when no topology has been observed yet.
type NetworkTopologyProvider ¶ added in v0.5.3
type NetworkTopologyProvider interface {
Topology(system string) (*trunking.TopologySnapshot, bool)
}
NetworkTopologyProvider is the optional capability a SitesProvider may implement to expose the live topology snapshot (identity + neighbours + band plan) for a system. The SiteTracker implements it; the systems endpoints use it to overlay the decoded adjacent-site ("neighbor") list onto a SystemDTO so the Systems panels can show neighbours without the drill-in report. The bool is false when no topology has been observed for the system yet.
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"`
// ControlChannels and Talkgroups carry the parsed detail the web
// Config Builder folds into its draft (control channels collapsed
// across sites, matching the flat config schema). The legacy
// /api/v1/import preview ignores them; they are populated for the
// builder's POST /api/v1/config/parse path.
ControlChannels []uint32 `json:"control_channels,omitempty"`
Talkgroups []ImportTalkgroupDTO `json:"talkgroups,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 RFScopeOptions ¶ added in v0.6.0
type RFScopeOptions struct {
Enabled bool
// Assets is the embedded RF Scope SPA file tree. nil mounts the API only.
Assets fs.FS
}
RFScopeOptions configures the optional RF Scope console. When Enabled, NewServer wires the RF Scope web subsystem and routes() mounts the /api/v1/rfscope/* analysis routes plus — when Assets carries an index.html — the RF Scope SPA at /rfscope/. The daemon sets it so operators can reach the offline RF-analysis console from the main UI in a new tab; the standalone `gophertrunk rfscope serve` command keeps using its own webserver at /.
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"`
// TalkerAliasUnreliable flags an over-the-air alias whose decode
// passed CRC but held non-ASCII-printable characters (#711); a
// client should render it as suspect.
TalkerAliasUnreliable bool `json:"talker_alias_unreliable,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 RRSystemResponse ¶ added in v0.3.6
type RRSystemResponse struct {
System radioreference.FullSystem `json:"system"`
Config config.SystemConfig `json:"config"`
Talkgroups []TalkgroupCSVRow `json:"talkgroups"`
}
RRSystemResponse couples the raw RadioReference detail with a config-ready SystemConfig + talkgroup rows the builder folds into its draft.
type RRVerifyResponse ¶ added in v0.3.7
type RRVerifyResponse struct {
OK bool `json:"ok"`
Premium bool `json:"premium"`
Username string `json:"username,omitempty"`
Expires string `json:"expires,omitempty"`
Fault string `json:"fault,omitempty"`
}
RRVerifyResponse is the result of POST /api/v1/config/rr/verify. OK reports that the supplied username/password authenticated; Premium reports whether the account's subscription is currently active. Fault carries the RR error message (bad login / expired) when OK is false.
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"`
RecordingSkipEncrypted bool `json:"recording_skip_encrypted"`
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"`
// IDBase selects how identity numbers (WACN, System ID, NAC, RFSS,
// Site) are rendered in the UIs: "hex" (default) or "dec". Sourced
// from web.id_base config; the raw decimal values are always present
// in the API/event payloads regardless.
IDBase string `json:"id_base,omitempty"`
// CryptolabConsole is true when the daemon mounts the Crypto Lab SPA at
// /cryptolab/ (a -tags cryptolab build). The web consoles use it to show a
// Crypto Lab link only when it is reachable. Injected by the API server, not
// the daemon's runtime provider.
CryptolabConsole bool `json:"cryptolab_console,omitempty"`
// SiglabConsole is true when the daemon mounts the Signal Lab SPA at
// /siglab/. The web consoles use it to show a Signal Lab link only when it
// is reachable. Injected by the API server, not the runtime provider.
SiglabConsole bool `json:"siglab_console,omitempty"`
// RFScopeConsole is true when the daemon mounts the RF Scope SPA at
// /rfscope/. The web consoles use it to show an RF Scope link only when it
// is reachable. Injected by the API server, not the runtime provider.
RFScopeConsole bool `json:"rfscope_console,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
// Sites is optional. When non-nil the server exposes
// GET /api/v1/sites (the discovered-P25-site table, issue #698).
Sites SitesProvider
// 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
// Hunt exposes the live system-discovery cockpit for GET /api/v1/hunt and
// its start/stop/export/commit routes. Optional; when nil, the routes
// return 503 (or an empty idle status for the read).
Hunt HuntCockpit
// 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
// ConfigActivator, when supplied, enables POST /api/v1/config/activate:
// the daemon loads/hot-swaps the config file the operator picks in the
// web Config Builder, either reloading in place (hot-applying what it
// can) or re-execing into the new file. nil disables the endpoint
// (returns 503) — e.g. the standalone `config serve`, which has no
// daemon to reconfigure.
ConfigActivator ConfigActivator
// 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
// Capture, when non-nil, enables the runtime
// POST /api/v1/siglab/capture route and its GET
// /api/v1/siglab/capture/devices picker — record a fixed-length
// raw-IQ capture off a live tuner, stage it into siglab, and hand
// back a .cfile download URL. The daemon implements this over its
// iqtap.Broker map; nil keeps the routes returning 503.
Capture CaptureProvider
// 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
// Symbols, when non-nil, enables the WS /api/v1/diag/symbols
// recovered-symbol live stream that backs the web Symbol scope
// panel. The daemon implements this over the iqtap broker +
// internal/scanner/symbolscope; nil keeps the route returning 503.
Symbols SymbolProvider
// Mixer, when non-nil, enables the WS /api/v1/diag/mixer
// channel-baseband spectrum stream that backs the web Mixer plot.
// The daemon implements this over the iqtap broker +
// internal/scanner/symbolscope; nil keeps the route returning 503.
Mixer MixerProvider
// Siglab, when Enabled, mounts the offline signal-analysis routes
// (/api/v1/siglab/*) and constructs the in-memory job/capture store.
// The daemon and the standalone `siglab serve` command both set it.
Siglab SiglabOptions
// ConfigBuilder, when Enabled, mounts the web Config Builder/Editor
// routes (/api/v1/config/*) and — when Assets is non-nil — the builder
// SPA at /config/. The daemon sets it so the operator can edit config
// from the main UI in a new tab; the standalone `gophertrunk config
// serve` command sets it (with the SPA in WebAssets, served at /).
ConfigBuilder ConfigBuilderOptions
// RFScope, when Enabled, mounts the RF Scope analysis routes
// (/api/v1/rfscope/*) and — when Assets is non-nil — the RF Scope SPA at
// /rfscope/. The daemon sets it so operators can reach the offline
// RF-analysis console from the main UI; the standalone `gophertrunk rfscope
// serve` command keeps its own webserver instead.
RFScope RFScopeOptions
// Diagnostics is the shared error-diagnostics collector (banner +
// system info). The daemon injects one seeded with its SDR pool
// snapshot so the error path never re-enumerates USB. Optional; nil
// disables banner enrichment and the GET /api/v1/diag/banner route.
Diagnostics *gtdiag.Collector
// VerboseErrors mirrors diagnostics.verbose_errors. When true, every
// JSON error envelope includes the diagnostics banner (exposing host
// + dongle info), and GET /api/v1/diag/banner is served to any
// caller; when false the banner is only attached on ?verbose=1 from
// a trusted (authorized) request.
VerboseErrors bool
// DisplayLoc is the timezone the GET /api/v1/health daemon-time renders in.
// Nil means time.Local (the daemon passes display.timezone).
DisplayLoc *time.Location
// 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
// LoRa, when non-nil, enables the
// GET /api/v1/lora/frames route serving recent decoded LoRa
// frames. Wired by the daemon over the SQLite-backed
// storage.LoRaLog.
LoRa LoRaProvider
// 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
// SetVoiceEnhanceEnabled toggles the opt-in voice enhancement chain
// at runtime. The change takes effect on the next call (each call
// builds a fresh decoder that reads the current config).
SetVoiceEnhanceEnabled(enabled bool)
}
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"`
RecordingsSkipEncrypted *bool `json:"recordings_skip_encrypted,omitempty"`
RecordingsEnhanceEnabled *bool `json:"recordings_enhance_enabled,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 SiglabOptions ¶ added in v0.3.3
type SiglabOptions struct {
Enabled bool
// TempDir is where uploaded captures are staged. Empty ⇒ a fresh
// os.MkdirTemp directory owned by the service.
TempDir string
// MaxUploadBytes bounds one capture upload. 0 ⇒ defaultSiglabMaxUploadBytes.
MaxUploadBytes int64
// Assets, when non-nil and containing an index.html, mounts the Signal
// Lab SPA at /siglab/ on the daemon (the standalone `siglab serve` command
// serves it at / via ServerOptions.WebAssets instead). Set it to
// siglabweb.Assets() so the daemon hosts the console alongside the main
// operator UI; nil keeps only the /api/v1/siglab/* routes.
Assets fs.FS
}
SiglabOptions configures the offline signal-analysis subsystem. When Enabled, NewServer constructs a siglabService and the /api/v1/siglab/* routes are mounted. Both the daemon and the standalone `siglab serve` command supply this.
type SiteDTO ¶ added in v0.4.6
type SiteDTO struct {
System string `json:"system"`
RFSSID uint8 `json:"rfss_id"`
SiteID uint8 `json:"site_id"`
ControlChannelHz uint32 `json:"control_channel_hz,omitempty"`
// ControlChannelCarrierOffsetHz is the demod's measured carrier offset (Hz)
// of the locked control carrier relative to the tuned centre. A large value
// flags a control lock sitting well off the configured frequency — e.g. an
// adjacent site bleeding through at 12.5 kHz spacing (issue #815).
ControlChannelCarrierOffsetHz int32 `json:"control_channel_carrier_offset_hz,omitempty"`
// ControlChannelTSBKErrorRate is the cumulative percentage of TSBK blocks on
// this site's control channel that failed Viterbi/CRC — a decode-quality
// signal independent of carrier lock, since a well-locked carrier at range
// can still show a high rate (issue #858). It is a frame-error rate, not a
// pre-FEC bit-error rate. A pointer so a genuine 0.0 (perfect decode) is
// distinguishable from "no data" (nil): nil until a TSBK has been attempted,
// and on non-TSBK Phase 2 (TDMA) paths. ControlChannelTSBKCount is the total
// attempts the rate was measured over (a confidence weight).
ControlChannelTSBKErrorRate *float64 `json:"control_channel_tsbk_error_rate,omitempty"`
ControlChannelTSBKCount int64 `json:"control_channel_tsbk_count,omitempty"`
// ControlChannelDecodeQuality buckets the error rate into clean / marginal /
// poor (SDRTrunk-style), for consumers that want a category rather than a
// number. Empty when there is no TSBK data yet. See decodeQuality.
ControlChannelDecodeQuality string `json:"control_channel_decode_quality,omitempty"`
Name string `json:"name"`
WACN uint32 `json:"wacn,omitempty"`
SystemID uint16 `json:"system_id,omitempty"`
// Hex renderings of the identity numbers, alongside the decimal fields.
WACNHex string `json:"wacn_hex,omitempty"`
SystemIDHex string `json:"system_id_hex,omitempty"`
RFSSIDHex string `json:"rfss_id_hex,omitempty"`
SiteIDHex string `json:"site_id_hex,omitempty"`
// Neighbors are the adjacent sites this site advertised over the air (P25
// Adjacent Site Status Broadcast, opcode 0x3C), overlaid from the live
// topology snapshot. Each carries the neighbour's RFSS/Site and its
// band-plan-resolved control-channel downlink (and uplink) frequency, so a
// consumer can backfill the wider network's control-channel map from a
// single camped site without decoding every site directly. Only the camped
// site of a system carries them (that is the site that broadcast the list);
// other rows leave it empty (issue #864).
Neighbors []NeighborDTO `json:"neighbors,omitempty"`
}
SiteDTO is one row of GET /api/v1/sites: a P25 site GT has either discovered from the control channel, named in the system config, or both (issue #698). Name is empty when the operator has not labelled the site; ControlChannelHz is zero for a configured site GT has not yet heard over the air.
type SitesProvider ¶ added in v0.4.6
SitesProvider is the read side of the site tracker, supplying the P25 sites discovered from the control channel for GET /api/v1/sites (issue #698).
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"`
// P25Modulation is the configured P25 Phase 1 demod mode of the
// system this SDR is decoding ("c4fm" or "cqpsk"), resolved by
// matching the device's tuning against the configured systems. Lets
// the web symbol/constellation panels auto-select the right mode
// instead of asking the operator to pick it. Empty when no P25
// Phase 1 system matches (e.g. a DMR-only device).
P25Modulation string `json:"p25_modulation,omitempty"`
// ControlChannelHz is the configured control-channel frequency that
// falls inside this SDR's passband (the one closest to centre when
// several are in band), or 0 when none matches. The web Plots panels
// rest their view on it so they default off the centre DC spike onto a
// decodable channel instead of the useless SDR centre. (#557)
ControlChannelHz uint32 `json:"control_channel_hz,omitempty"`
}
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 SymbolFrame ¶ added in v0.3.5
type SymbolFrame struct {
TimestampNs int64 `json:"ts_ns"`
SymbolRateHz float64 `json:"symbol_rate_hz"`
CenterHz uint32 `json:"center_hz"`
OffsetHz int32 `json:"offset_hz"`
Soft []float32 `json:"soft"`
SymI []float32 `json:"sym_i"`
SymQ []float32 `json:"sym_q"`
// EyeSoft is a bounded window of oversampled, AGC-scaled matched-
// filter output (EyeSPS samples per symbol) for the C4FM eye diagram;
// empty on CQPSK. Fold it over EyeSPS to render the 4-level eye.
EyeSoft []float32 `json:"eye_soft"`
EyeSPS int `json:"eye_sps"`
Dibits DibitArray `json:"dibits"`
IsBits bool `json:"is_bits"`
BaseIdx int `json:"base_idx"`
// Receiver-state metrics for the Tuning panel (see symbolscope.Frame).
CarrierOffsetHz float64 `json:"carrier_offset_hz"`
AGCLevel float64 `json:"agc_level"`
AGCTarget float64 `json:"agc_target"`
ClockMu float64 `json:"clock_mu"`
ClockSPS float64 `json:"clock_sps"`
CMAError float64 `json:"cma_error"`
}
SymbolFrame is the wire shape of one batch of recovered symbols for the web "Symbol" scope. It mirrors symbolscope.Frame so the api package stays free of a dependency on the DSP package; the SymbolProvider interface bridges the two.
Soft is the pre-slicer soft waveform (empty when the demod path has no soft tap, e.g. P25 CQPSK); Dibits are the sliced decisions (0..3 when IsBits is false, 0..1 when true). When Soft is non-empty it is aligned index-for-index with Dibits.
SymI/SymQ carry the complex symbol-decision points (the true constellation) on the linear/CQPSK path; empty on C4FM. When non-empty they are aligned index-for-index with Dibits.
type SymbolProvider ¶ added in v0.3.5
type SymbolProvider interface {
// OpenSymbolStream starts a per-request symbol-recovery engine on
// the named device. proto selects the receiver ("p25-c4fm" /
// "p25-cqpsk"); offsetHz tunes an off-centre channel down to
// baseband before channelizing. Returns the wire-frame channel and
// a cleanup func the caller MUST invoke on disconnect.
OpenSymbolStream(ctx context.Context, serial, proto string, offsetHz int32) (<-chan SymbolFrame, func(), error)
}
SymbolProvider is the daemon-side abstraction the symbol endpoint consumes. The daemon implements it on top of the iqtap broker map + internal/scanner/symbolscope; tests substitute a fake.
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"`
// Hex renderings of the identity numbers (P25 field convention),
// alongside the decimal fields above. Empty when the value is unknown.
WACNHex string `json:"wacn_hex,omitempty"`
SystemIDHex string `json:"system_id_hex,omitempty"`
RFSSHex string `json:"rfss_hex,omitempty"`
SiteHex string `json:"site_hex,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"`
// DMRBandPlan surfaces the active DMR Tier III LCN→frequency plan
// (operator-configured, or learned by the autoconfig learner and
// written back to config) so the web Systems panel can show how voice
// grants resolve. Nil when the system has no DMR band plan. (#638)
DMRBandPlan *DMRBandPlanDTO `json:"dmr_band_plan,omitempty"`
// Neighbors are the adjacent (neighbour) sites this system's control
// channel advertised over the air (P25 Adjacent Site Status Broadcast,
// opcode 0x3C), decoded live and overlaid from the SiteTracker's topology
// snapshot. Empty when none have been heard yet. Surfaced so the Systems
// panels can show them the way SDRtrunk's "Neighbor Sites" view does,
// instead of only the system drill-in report.
Neighbors []NeighborDTO `json:"neighbors,omitempty"`
// FrequencyBands is the decoded P25 IDEN_UP frequency-band table (channel
// ID → base/spacing/offset), overlaid live from the same topology snapshot
// as Neighbors. Surfaced so the Systems panel can show the band plan the way
// SDRtrunk's "Details" tab does, and so a neighbour's channel_id/
// channel_number can be mapped back to an absolute frequency. Empty until an
// IDEN_UP has been heard. (#814)
FrequencyBands []BandPlanSlotDTO `json:"frequency_bands,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 TalkgroupCSVRow ¶ added in v0.3.6
type TalkgroupCSVRow = configbuilder.TalkgroupCSVRow
TalkgroupCSVRow is one row of a talkgroup CSV sidecar written on save. Aliased to the shared configbuilder type so the TUI and web builder use one definition and the JSON wire shape is identical.
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"`
// RFSSID / SiteID name the radio's serving site (a genuine
// RID→site fix) and NAC is the coarse control channel access code
// (issue #698). Zero/omitted on non-P25 registrations and until the
// site's status broadcast has decoded.
RFSSID uint8 `json:"rfss_id,omitempty"`
SiteID uint8 `json:"site_id,omitempty"`
NAC uint16 `json:"nac,omitempty"`
}
UnitRegistrationDTO mirrors trunking.UnitRegistration.
type UnitToUnitRequestDTO ¶ added in v0.4.7
type UnitToUnitRequestDTO struct {
System string `json:"system"`
Protocol string `json:"protocol"`
SourceID uint32 `json:"source_id"`
TargetID uint32 `json:"target_id"`
ServiceOptions uint8 `json:"service_options,omitempty"`
}
UnitToUnitRequestDTO mirrors trunking.UnitToUnitRequest.
type ValidationError ¶ added in v0.3.6
ValidationError is one section-keyed config error.
type ValidationResult ¶ added in v0.3.6
type ValidationResult struct {
OK bool `json:"ok"`
Errors []ValidationError `json:"errors"`
}
ValidationResult is the structured outcome of a validate call.
Source Files
¶
- api.go
- audio_publisher.go
- auth.go
- capture.go
- config_builder.go
- config_builder_docs.go
- cors.go
- cryptolab_disabled.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_config_activate.go
- handlers_config_builder.go
- handlers_dsc.go
- handlers_hunt.go
- handlers_import.go
- handlers_locations.go
- handlers_lora.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
- handlers_siglab.go
- handlers_siglab_spectrum.go
- handlers_sites.go
- mixer.go
- rfscope.go
- sanitize.go
- server.go
- siglab_jobs.go
- spectrum.go
- sse.go
- storage_adapter.go
- symbols.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. |