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
- func CompressedPositionInfo(lat, lon float64, course int, speedKt float64, altM float64, ...) string
- func ExpandComment(comment, version string) string
- func HeadingDelta(a, b float64) float64
- func ObjectInfo(objectName string, live bool, timestampDHM string, lat, lon float64, ...) string
- func PositionInfo(lat, lon float64, course int, speedKt float64, altM float64, ...) string
- func RunCommentCmd(ctx context.Context, argv []string, timeout time.Duration) (string, error)
- func SplitArgv(s string) ([]string, error)
- func StatusInfo(comment string) string
- type Clock
- type Config
- type ErrorObserver
- type ISSink
- type Observer
- type Options
- type Scheduler
- type SendNowError
- type SendNowErrorKind
- type SkipObserver
- type SmartBeaconConfig
- type Type
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
StatusInfo builds an APRS status report: ">comment".
Types ¶
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 ¶
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 (*Scheduler) Reload ¶
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 ¶
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 ¶
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 ¶
SetBeacons replaces the beacon list. If Run is active, call Reload instead to also tell the scheduler to pick up the new config.
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 ¶
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).