beacon

package
v0.13.3 Latest Latest
Warning

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

Go to latest
Published: May 9, 2026 License: GPL-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package beacon implements the graywolf beacon scheduler: position, object, tracker, custom, and igate beacons driven by the configstore `beacons` table, with optional SmartBeaconing for tracker beacons and safe `comment_cmd` execution for dynamic comments. All outgoing frames are submitted through a txgovernor.TxSink at PriorityBeacon.

Runtime model: a single scheduler goroutine maintains a min-heap of *beaconPlan keyed by nextFire. Each tick pops the earliest plan, dispatches it onto a bounded worker pool, then pushes the rescheduled plan back onto the heap. Reloads are serviced on the same goroutine, so there is no interleaving between the old and new schedules.

Index

Constants

View Source
const DefaultMaxConcurrentFires = 4

DefaultMaxConcurrentFires is the default size of the fire worker pool when Options.MaxConcurrentFires is zero. Four workers is enough for realistic home-station configurations; operators with dozens of beacons can raise the limit explicitly.

Variables

This section is empty.

Functions

func CompressedPositionInfo

func CompressedPositionInfo(lat, lon float64, course int, speedKt float64, altM float64, symbolTable, symbolCode byte, messaging bool, phg string, comment string) string

CompressedPositionInfo builds a 13-byte base-91 compressed APRS position info-field per APRS101 ch 9:

!<sym_table>YYYYXXXX<sym_code><cs><T>[PHGphgd][/A=NNNNNN][comment]

cs holds course/speed when either is set, otherwise two spaces ("no data"). Altitude is emitted via the "/A=" extension rather than the cs-altitude form so the uncompressed and compressed paths produce equivalent altitude precision and the caller can always supply both course/speed and altitude.

The compression type byte T advertises: current GPS fix, NMEA source "other", origin "software" — matching the value most APRS software trackers emit.

phg, when non-empty, is the 7-byte "PHGphgd" extension appended after the compressed block and before any /A= altitude. It is only emitted when both course and speed are zero (PHG is for fixed stations; CSE/SPD is already encoded in cs for moving ones).

func ExpandComment

func ExpandComment(comment, version string) string

ExpandComment renders an APRS beacon comment through text/template so operators can embed dynamic tags like {{version}} in their static comment strings. Supported tags:

{{version}}  → the running graywolf version string

Comments without "{{" are returned unchanged to avoid parsing overhead on the common case. On parse or execution error the original comment is returned so a typo can't silently blank every beacon.

func HeadingDelta

func HeadingDelta(a, b float64) float64

HeadingDelta returns the absolute angular difference between two headings in degrees, wrapped to [0, 180].

func ObjectInfo

func ObjectInfo(objectName string, live bool, timestampDHM string, lat, lon float64, symbolTable, symbolCode byte, phg string, comment string) string

ObjectInfo builds an APRS object report info-field.

;NAME     *DDHHMMzDDMM.hhN/DDDMM.hhW>[PHGphgd]comment

objectName is padded/truncated to 9 characters. live=true sets '*' (live) rather than '_' (killed). timestampDHM is a 6-char "DDHHMMz" string; if empty, "111111z" is used (APRS wildcard). phg is the already-encoded "PHGphgd" 7-byte string (or "" for no extension).

func PositionInfo

func PositionInfo(lat, lon float64, course int, speedKt float64, altM float64, symbolTable, symbolCode byte, messaging bool, phg string, comment string) string

PositionInfo builds an uncompressed APRS position info-field.

!DDMM.hhN/DDDMM.hhW>comment     — no timestamp, no messaging
=DDMM.hhN/DDDMM.hhW>comment     — no timestamp, messaging capable

symbolTable/symbolCode default to '/' and '-' if zero. course is degrees (1..360, 0 means "not set"); speed is knots; altitude is metres and is appended as "/A=NNNNNN" (in feet per APRS101) when non-zero.

phg is the already-encoded "PHGphgd" 7-byte string (or "" for no PHG extension). PHG occupies the same slot as CSE/SPD and is meaningless for moving stations, so it is only emitted when both course and speed are zero.

func RunCommentCmd

func RunCommentCmd(ctx context.Context, argv []string, timeout time.Duration) (string, error)

RunCommentCmd executes argv with the given timeout and returns the trimmed stdout. argv[0] is the program; subsequent entries are literal arguments (no shell interpretation). On non-zero exit, timeout, or missing program, an error is returned and captured stdout so far is also returned (possibly empty). The caller is expected to fall back to the static comment on error.

func SplitArgv

func SplitArgv(s string) ([]string, error)

SplitArgv splits a command string into argv tokens, honoring single and double quotes and backslash escapes. This is a conservative shell-word-like splitter used ONLY when the configstore stores comment_cmd as a single string. Unlike sh, it does NOT expand variables, globs, backticks, or $(...) — metacharacters are treated as literal arguments. Empty string → zero args.

func StatusInfo

func StatusInfo(comment string) string

StatusInfo builds an APRS status report: ">comment".

Types

type Clock

type Clock interface {
	Now() time.Time
	After(time.Duration) <-chan time.Time
}

Clock abstracts time for deterministic tests.

type Config

type Config struct {
	ID          uint32
	Type        Type
	Channel     uint32 // send_to parsed as channel number (IG/APP handled by caller)
	Source      ax25.Address
	Dest        ax25.Address
	Path        []ax25.Address
	Delay       time.Duration // initial delay
	Every       time.Duration // periodic interval
	Slot        int           // seconds past the hour; -1 means unset
	UseGps      bool          // if true, source lat/lon/alt from the GPS cache instead of Lat/Lon/AltFt
	Lat, Lon    float64       // fixed position
	AltFt       float64
	SymbolTable byte
	SymbolCode  byte
	Comment     string
	CommentCmd  []string // already-split argv; empty = static comment
	Compress    bool     // use 13-byte base-91 compressed position format
	Messaging   bool
	ObjectName  string             // for TypeObject
	CustomInfo  string             // for TypeCustom (raw info field override)
	SmartBeacon *SmartBeaconConfig // non-nil + .Enabled → use for tracker
	// PHG radio-capability extension (APRS101 ch 7) for fixed-station
	// position, igate, and object beacons. Emitted only when PHGPower
	// > 0. Not valid for trackers (CSE/SPD occupies the same slot).
	PHGPower       int // watts
	PHGHeightFt    int // feet above average terrain
	PHGGainDB      int // dBi
	PHGDirectivity int // 0 = omni, 1..8 = 45° × d compass direction
	Enabled        bool
	SendToAPRSIS   bool // also send this beacon to APRS-IS (default off)
}

Config describes one beacon entry from the beacons table. Fields match the SQL schema in .context/graywolf-implementation-plan.md §beacons.

type ErrorObserver

type ErrorObserver interface {
	OnEncodeError(beaconName string)
	OnSubmitError(beaconName string, reason string)
}

ErrorObserver is an optional interface the Observer may implement to receive beacon failure notifications. The scheduler performs a type assertion at call time, so existing Observer implementations (which do not know about encode/submit errors) keep working unmodified.

OnEncodeError fires once per beacon whose AX.25 encoding step returned a non-nil error — typically a misconfigured source or destination address. The beacon is dropped on the floor; the scheduler will retry at the next tick but nothing changes in the configuration, so a sustained non-zero count is an operator signal.

OnSubmitError fires once per Submit call that returned an error. reason is one of "queue_full", "timeout", or "other" — see classifySubmitError.

type ISSink

type ISSink interface {
	SendLine(line string) error
}

ISSink is an optional destination for sending beacons to APRS-IS. When non-nil and Config.SendToAPRSIS is true, the scheduler sends a TNC-2 formatted copy of the beacon to APRS-IS after RF submission.

type Observer

type Observer interface {
	OnBeaconSent(beaconType Type)
	OnSmartBeaconRate(channel uint32, interval time.Duration)
}

Observer is an optional hook for metrics. Scheduler calls these on beacon send; nil methods are skipped.

type Options

type Options struct {
	Sink     txgovernor.TxSink
	Cache    gps.PositionCache // may be nil for fixed/igate-only deployments
	Logger   *slog.Logger
	Observer Observer
	Clock    Clock  // defaults to wall clock
	Version  string // running graywolf version, used to expand {{version}} in comments
	ISSink   ISSink // optional APRS-IS line sender for beacons with SendToAPRSIS
	// MaxConcurrentFires bounds how many beacon fires can be in flight
	// at once. Zero selects DefaultMaxConcurrentFires. The scheduler
	// never blocks on submit — if all workers are busy when a beacon is
	// due, the fire is dropped and a skipped_busy event is recorded.
	MaxConcurrentFires int
	// ChannelModes resolves Channel.Mode at TX time. Beacons whose
	// channel is "packet" are skipped silently. Nil = treat every
	// channel as ChannelModeAPRS (preserves the legacy any-channel-
	// does-anything behavior). Lookup errors are silently ignored
	// (fail-open): a DB failure does not suppress beaconing.
	ChannelModes configstore.ChannelModeLookup
}

Options configures a Scheduler.

type Scheduler

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

Scheduler owns the run-loop goroutine and the bounded worker pool that dispatches beacon fires. Configure via New, drive with Run; SetBeacons / Reload / SendNow are safe to call from any goroutine.

func New

func New(opts Options) (*Scheduler, error)

New constructs a Scheduler.

func (*Scheduler) Reload

func (s *Scheduler) Reload(b []Config)

Reload atomically swaps in a new beacon list and signals Run to rebuild its heap from the new config. Safe to call from any goroutine; non-blocking — rapid successive calls coalesce into one rebuild.

The rebuild happens on the scheduler's single run-loop goroutine, so there is no interleaving between the old and new schedules: a beacon either fires from the pre-reload heap or from the post-reload heap, never both.

func (*Scheduler) Run

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

Run drives the scheduler's single heap-based run loop until ctx is cancelled. It returns nil on clean shutdown. In-flight worker goroutines detach from Run and complete (or cancel via ctx) on their own; Run returning does not wait for them.

func (*Scheduler) SendNow

func (s *Scheduler) SendNow(ctx context.Context, id uint32) error

SendNow finds the beacon with the given id in the current beacon list and transmits it once immediately, independently of its scheduled interval. Returns an error if the id is not present. The Enabled flag is intentionally ignored — operators may want to test a beacon that is otherwise disabled.

func (*Scheduler) SetBeacons

func (s *Scheduler) SetBeacons(b []Config)

SetBeacons replaces the beacon list. If Run is active, call Reload instead to also tell the scheduler to pick up the new config.

func (*Scheduler) SetISSink

func (s *Scheduler) SetISSink(sink ISSink)

SetISSink sets the optional APRS-IS sink. Safe to call before Run.

type SendNowError added in v0.13.3

type SendNowError struct {
	Kind SendNowErrorKind
	Err  error
}

SendNowError is the typed error returned by Scheduler.SendNow when an explicit operator-driven send fails. Kind selects the failure category; Err preserves the underlying cause for unwrap/errors.Is.

func (*SendNowError) Error added in v0.13.3

func (e *SendNowError) Error() string

func (*SendNowError) Unwrap added in v0.13.3

func (e *SendNowError) Unwrap() error

type SendNowErrorKind added in v0.13.3

type SendNowErrorKind int

SendNowErrorKind classifies a SendNow failure so callers (notably the REST API handler) can map it to an appropriate HTTP status without string-matching the wrapped error.

const (
	// SendNowErrorBuild is a beacon-build failure: the operator's
	// configuration is internally consistent but cannot produce a frame
	// right now (no GPS fix, fixed coords 0/0, missing PHG, etc.).
	SendNowErrorBuild SendNowErrorKind = iota
	// SendNowErrorEncode is an AX.25 frame-encode failure, almost
	// always a malformed callsign.
	SendNowErrorEncode
	// SendNowErrorChannelMode means the target channel is in
	// packet-only mode and cannot transmit APRS beacons. The scheduled
	// fire path skips silently; SendNow surfaces this as an error so
	// the operator's explicit click does not appear to succeed.
	SendNowErrorChannelMode
	// SendNowErrorSubmit means the TX governor refused the frame
	// (queue full, deadline, etc.). The wrapped Err preserves the
	// original sentinel so callers can errors.Is against
	// txgovernor.ErrQueueFull, context.DeadlineExceeded, etc.
	SendNowErrorSubmit
)

func (SendNowErrorKind) String added in v0.13.3

func (k SendNowErrorKind) String() string

type SkipObserver

type SkipObserver interface {
	OnBeaconSkipped(beaconName string, reason string)
}

SkipObserver is an optional interface for observers that want to know when the scheduler skipped a fire because the worker pool was saturated. reason is currently always "busy"; the argument exists so future skip causes (e.g. shutdown-in-progress) get their own bucket without a signature change.

type SmartBeaconConfig

type SmartBeaconConfig struct {
	Enabled   bool
	FastSpeed float64       // knots; at and above → FastRate
	FastRate  time.Duration // beacon interval at high speed
	SlowSpeed float64       // knots; at and below → SlowRate
	SlowRate  time.Duration // beacon interval at low speed
	TurnTime  time.Duration // minimum interval between turn-triggered beacons
	TurnAngle float64       // degrees; fixed component of turn threshold
	TurnSlope float64       // degrees · knots; divided by speed for speed-dep component
}

SmartBeaconConfig mirrors direwolf's SMARTBEACON / HamHUD parameters. All speeds are in knots (beacon code operates in the APRS canonical unit; callers convert from GPS m/s if needed).

func DefaultSmartBeacon

func DefaultSmartBeacon() SmartBeaconConfig

DefaultSmartBeacon matches direwolf's defaults.

func (SmartBeaconConfig) Interval

func (s SmartBeaconConfig) Interval(speedKt float64) time.Duration

Interval returns the current smart beacon interval for a given speed. HamHUD canonical formula:

speed <= slow_speed  → slow_rate
speed >= fast_speed  → fast_rate
otherwise            → fast_rate * fast_speed / speed

This is NOT linear interpolation — it is inverse-proportional to speed so a vehicle at half fast_speed beacons at twice fast_rate. That's the algorithm as documented on hamhud.net.

func (SmartBeaconConfig) TurnThreshold

func (s SmartBeaconConfig) TurnThreshold(speedKt float64) float64

TurnThreshold returns the heading-change angle (degrees) that triggers a corner-pegging beacon at the given speed. Per HamHUD:

threshold = turn_angle + turn_slope / speed

Higher speed → smaller threshold (corner pegs fire sooner). At speed == 0 the threshold is infinite (no turn-triggered beacons while stopped).

type Type

type Type string

Type enumerates the supported beacon kinds.

const (
	TypePosition Type = "position"
	TypeObject   Type = "object"
	TypeTracker  Type = "tracker"
	TypeCustom   Type = "custom"
	TypeIGate    Type = "igate"
)

Jump to

Keyboard shortcuts

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