storage

package
v0.2.4 Latest Latest
Warning

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

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

Documentation

Overview

APRS log writer — drains KindAPRSPacket events off the shared bus and writes one row per decoded packet to the SQLite aprs_log table. Mirrors pagerlog.go and locationlog.go in structure.

Bookmark storage — operator-managed conventional channel bookmarks. Persisted to SQLite alongside the call log and location log so they survive a daemon restart and back up with the rest of the daemon state.

The store is decoupled from the SDR pool and the scanner — a bookmark is plain metadata (frequency + display name + mode + notes); the spectrum panel uses it to render click-to-tune markers, the conventional scanner uses it as an alternative to the YAML channel list. Mutations are bus-published so subscribed surfaces (web SPA, TUI) can re-render without polling.

PagerLog persists POCSAG (and eventually FLEX) pager messages to the SQLite pager_log table by subscribing to events.KindPagerMessage on the shared bus. The decoder pipeline publishes one event per fully-reassembled page; this consumer writes a row and the web panel queries the recent rows via /api/v1/pager/messages.

Package storage persists GopherTrunk's runtime data to disk.

The default backend is SQLite via the pure-Go `modernc.org/sqlite` driver — CGO_ENABLED=0 stays true across the daemon and the daemon cross-compiles to linux/arm64 without toolchain gymnastics.

Layout:

sqlite.go     Open + schema migrations. One-shot at startup.
calllog.go    CallLog: subscribes to events.KindCallStart /
              KindCallEnd from the trunking engine, writes rows
              keyed by (device serial, started_at).
retention.go  Background sweeper that deletes DB rows + the WAV /
              raw files written by internal/voice older than a
              configurable cutoff.

The API's /api/v1/calls/history endpoint reads through the `History` query helpers exposed here. There is no gRPC call-log service today (history is REST only).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type APRSLog added in v0.2.4

type APRSLog struct {
	// contains filtered or unexported fields
}

APRSLog drains KindAPRSPacket events until ctx cancels or the bus closes.

func NewAPRSLog added in v0.2.4

func NewAPRSLog(db *DB, bus *events.Bus, logger *slog.Logger) (*APRSLog, error)

NewAPRSLog wires the log to the bus. Subscription happens at construction so events published before Run() begins aren't lost.

func (*APRSLog) Close added in v0.2.4

func (a *APRSLog) Close() error

Close releases the bus subscription and waits for Run to drain.

func (*APRSLog) Recent added in v0.2.4

func (a *APRSLog) Recent(limit int) ([]APRSPacket, error)

Recent returns the most recent packets, newest first, capped at limit. limit ≤ 0 picks 200; limit > 5000 caps at 5000.

func (*APRSLog) Run added in v0.2.4

func (a *APRSLog) Run(ctx context.Context) error

Run drains KindAPRSPacket events until ctx cancels or the bus closes.

type APRSPacket added in v0.2.4

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

APRSPacket is one persisted decoded APRS packet. Mirrors the aprs.Packet shape with the AX.25 envelope flattened to a callsign-string triple (src, dst, path) and the sub-payload summarised into a single "body" field for compact rendering. Latitude / Longitude are populated only for position-bearing types and stay 0 otherwise.

type Bookmark added in v0.2.3

type Bookmark struct {
	ID        int64     `json:"id"`
	Name      string    `json:"name"`
	FreqHz    uint32    `json:"freq_hz"`
	Mode      string    `json:"mode"` // "FM", "NFM", "AM", "USB", "LSB", "CW", "DMR", ...
	CTCSSHz   float64   `json:"ctcss_hz,omitempty"`
	DCSCode   uint16    `json:"dcs_code,omitempty"`
	Notes     string    `json:"notes,omitempty"`
	Group     string    `json:"group,omitempty"` // operator-defined category
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Bookmark is one row in the bookmarks table.

type BookmarkStore added in v0.2.3

type BookmarkStore struct {
	// contains filtered or unexported fields
}

BookmarkStore is the CRUD layer over the bookmarks table.

func NewBookmarkStore added in v0.2.3

func NewBookmarkStore(db *DB, bus *events.Bus) (*BookmarkStore, error)

NewBookmarkStore returns a store backed by the given DB. The bus is optional — when nil, no mutation events are published (useful for tests). When non-nil, every Create / Update / Delete publishes a bookmark.{created,updated,deleted} event so UI surfaces refresh without polling.

func (*BookmarkStore) Create added in v0.2.3

func (s *BookmarkStore) Create(ctx context.Context, b Bookmark) (Bookmark, error)

Create inserts a bookmark and returns it with ID + timestamps populated. Name is required; mode defaults to "FM" when empty.

func (*BookmarkStore) Delete added in v0.2.3

func (s *BookmarkStore) Delete(ctx context.Context, id int64) error

Delete removes the bookmark by id. Returns sql.ErrNoRows when the id is unknown.

func (*BookmarkStore) Get added in v0.2.3

func (s *BookmarkStore) Get(ctx context.Context, id int64) (Bookmark, error)

Get returns the bookmark with the given id, or sql.ErrNoRows when it's missing.

func (*BookmarkStore) List added in v0.2.3

func (s *BookmarkStore) List(ctx context.Context) ([]Bookmark, error)

List returns all bookmarks sorted by group then name. Empty result is not an error.

func (*BookmarkStore) Update added in v0.2.3

func (s *BookmarkStore) Update(ctx context.Context, b Bookmark) (Bookmark, error)

Update modifies the bookmark identified by b.ID. Only the editable fields (name, freq, mode, ctcss, dcs, notes, group) are written; created_at is preserved, updated_at refreshed. Returns the new row.

type CallLog

type CallLog struct {
	// contains filtered or unexported fields
}

CallLog persists trunking calls to the SQLite call_log table by subscribing to events.KindCallStart and events.KindCallEnd on the shared events bus.

Rows are keyed by (device_serial, started_at). On CallStart we INSERT with a NULL ended_at; on CallEnd we UPDATE the matching row with the ended_at, duration, and end-reason. The unique index in the schema keeps duplicate-start events idempotent.

func NewCallLog

func NewCallLog(db *DB, bus *events.Bus, logger *slog.Logger) (*CallLog, error)

NewCallLog wires the call log to the bus. It subscribes immediately so callers can publish events before Run is called.

func (*CallLog) Close

func (c *CallLog) Close() error

Close releases the bus subscription and waits for Run to drain.

func (*CallLog) Run

func (c *CallLog) Run(ctx context.Context) error

Run drains call.start / call.end events until ctx cancels.

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"`
	AlgorithmID    uint8     `json:"algorithm_id"`
	KeyID          uint16    `json:"key_id"`
	Emergency      bool      `json:"emergency"`
	DataCall       bool      `json:"data_call"`
	DeviceSerial   string    `json:"device_serial"`
	StartedAt      time.Time `json:"started_at"`
	EndedAt        time.Time `json:"ended_at,omitempty"` // zero if call still active
	DurationMs     int64     `json:"duration_ms,omitempty"`
	EndReason      string    `json:"end_reason,omitempty"`
	TalkgroupAlpha string    `json:"talkgroup_alpha,omitempty"`
}

CallRow is one row from the call_log table.

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB is a thin wrapper over *sql.DB that lets the call-log + retention helpers share a typed handle. The schema is migrated on Open.

func Open

func Open(path string) (*DB, error)

Open creates (or opens) a SQLite database at path and applies the embedded schema migrations. The path's parent directory is created if missing.

`:memory:` and the standard "file:..." DSN forms are passed through to the driver — useful for tests.

func (*DB) Close

func (d *DB) Close() error

Close releases the connection.

func (*DB) History

func (d *DB) History(ctx context.Context, f HistoryFilter) ([]CallRow, error)

History queries the call_log with the supplied filter, newest-first.

func (*DB) SQL

func (d *DB) SQL() *sql.DB

SQL returns the underlying *sql.DB. Exposed so tests and future integrations (an /api/v1/calls/history handler, etc.) can run their own queries without adding a method here for every shape.

type HistoryFilter

type HistoryFilter struct {
	System    string
	GroupID   uint32 // 0 = no filter (filters call_log.group_id)
	SourceID  uint32 // 0 = no filter (filters call_log.source_id — RID)
	Since     time.Time
	Until     time.Time
	Limit     int
	OnlyEnded bool
}

HistoryFilter narrows a History query.

type LocationLog added in v0.1.9

type LocationLog struct {
	// contains filtered or unexported fields
}

LocationLog persists geographic fixes to the SQLite location_log table by subscribing to events.KindLocation on the shared bus.

func NewLocationLog added in v0.1.9

func NewLocationLog(db *DB, bus *events.Bus, logger *slog.Logger) (*LocationLog, error)

NewLocationLog wires the location log to the bus. It subscribes immediately so callers can publish before Run starts.

func (*LocationLog) Close added in v0.1.9

func (l *LocationLog) Close() error

Close releases the bus subscription and waits for Run to drain.

func (*LocationLog) Recent added in v0.1.9

func (l *LocationLog) Recent(limit int) ([]LocationRow, error)

Recent returns the most recent fixes, newest first, capped at limit.

func (*LocationLog) Run added in v0.1.9

func (l *LocationLog) Run(ctx context.Context) error

Run drains KindLocation events until ctx cancels or the bus closes.

type LocationRow added in v0.1.9

type LocationRow struct {
	ID         int64     `json:"id"`
	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 time.Time `json:"reported_at"`
}

LocationRow is one persisted fix, returned by Recent.

type PagerLog added in v0.2.3

type PagerLog struct {
	// contains filtered or unexported fields
}

PagerLog drains KindPagerMessage off the bus and writes one row per page. Mirrors LocationLog's Run/Close lifecycle.

func NewPagerLog added in v0.2.3

func NewPagerLog(db *DB, bus *events.Bus, logger *slog.Logger) (*PagerLog, error)

NewPagerLog wires the log to the bus. The bus subscription happens at construction time so the decoder can publish before Run begins without losing events.

func (*PagerLog) Close added in v0.2.3

func (p *PagerLog) Close() error

Close releases the bus subscription and waits for Run to drain.

func (*PagerLog) Recent added in v0.2.3

func (p *PagerLog) Recent(limit int) ([]PagerMessage, error)

Recent returns the most recent pages, newest first, capped at limit. limit ≤ 0 picks 200; limit > 5000 caps at 5000.

func (*PagerLog) Run added in v0.2.3

func (p *PagerLog) Run(ctx context.Context) error

Run drains KindPagerMessage events until ctx cancels or the bus closes.

type PagerMessage added in v0.2.3

type PagerMessage struct {
	ID         int64     `json:"id"`
	ReceivedAt time.Time `json:"received_at"`
	RIC        uint32    `json:"ric"`
	Func       uint8     `json:"func"` // 0..3 = A..D
	Encoding   string    `json:"encoding"`
	Body       string    `json:"body"`
	Corrected  int       `json:"corrected"`
}

PagerMessage is one persisted page. Mirrors the pocsag.Page shape but with the "encoding" enum widened to a string for storage + JSON convenience.

type Retention

type Retention struct {
	// contains filtered or unexported fields
}

Retention deletes old data on a schedule:

  1. call_log rows with started_at older than CallRowMaxAge.
  2. WAV / raw files under FilesRoot whose modification time is older than FilesMaxAge.

File deletion is opt-in by setting FilesRoot; an empty value skips the filesystem sweep. The sweeper is idempotent and safe to run concurrently with the call-log writer (SQLite serialises).

func NewRetention

func NewRetention(opts RetentionOptions) (*Retention, error)

func (*Retention) Run

func (r *Retention) Run(ctx context.Context) error

Run sweeps once at startup and then every Interval until ctx cancels.

func (*Retention) SweepOnce

func (r *Retention) SweepOnce(ctx context.Context)

SweepOnce runs the configured deletions. Errors are logged and swallowed so a transient FS or DB problem doesn't kill the loop.

type RetentionOptions

type RetentionOptions struct {
	DB *DB
	// FilesRoot is the directory the voice recorder writes WAV / raw
	// files under. Empty disables the filesystem sweep.
	FilesRoot string
	// CallRowMaxAge: rows with started_at older than this are deleted.
	// Zero (the default) disables row deletion.
	CallRowMaxAge time.Duration
	// FilesMaxAge: files older than this (mtime) are deleted. Zero
	// disables file deletion.
	FilesMaxAge time.Duration
	// Interval between sweeps. Default 1 h.
	Interval time.Duration
	Log      *slog.Logger
}

RetentionOptions configure a Retention sweeper.

Jump to

Keyboard shortcuts

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