Documentation
¶
Overview ¶
Package aprs parses and builds APRS (Automatic Packet Reporting System) packets carried in AX.25 UI frame information fields.
The package covers the packet types graywolf needs to decode and transmit for normal 144.39 MHz operation:
- Position reports (!/=, @/`) uncompressed and compressed, with or without timestamp
- Messages, bulletins, announcements, and NWS alerts (:)
- Telemetry (T#... and base-91 compressed form)
- Weather reports (_ positionless, @/` with weather appendix)
- Objects (;) and items ())
- Mic-E (' and `) with bit-packed latitude and manufacturer encoding
- Station capabilities (<IGATE,...>)
- Direction finding (DF reports with BRG/NRQ appendix)
The parser is fuzz-friendly: every entry point checks bounds and returns an error rather than panicking on malformed input.
Usage:
pkt, err := aprs.Parse(frame) // frame is *ax25.Frame
if err != nil { ... }
if pkt.Position != nil {
fmt.Println(pkt.Position.Latitude, pkt.Position.Longitude)
}
Reference material: goballoon (position / message / telemetry / base91 shapes were modernized from there), direwolf's decode_aprs.c and decode_mic_e.c (Mic-E bit layout), and the APRS Protocol Reference v1.0.1.
Index ¶
- Constants
- Variables
- func EncodeCompressedLatLon(lat, lon float64) []byte
- func EncodeMessage(addressee, text, id string) ([]byte, error)
- func EncodeMessageAck(addressee, id string) ([]byte, error)
- func EncodeMicEDest(lat float64, msgCode int, lonOffset100 bool, westLong bool) string
- func EncodePHG(watts, heightFt, gainDB, directivity int) (string, error)
- func EncodePosition(p Position, messaging bool) ([]byte, error)
- func EncodeTelemetry(t Telemetry) ([]byte, error)
- func HaversineDistanceMi(lat1, lon1, lat2, lon2 float64) float64
- type Capabilities
- type DecodedAPRSPacket
- type DeviceInfo
- type Direction
- type DirectionFinding
- type InboundPacket
- type Item
- type LogOutput
- type Message
- type MicE
- type Object
- type PHG
- type PacketInput
- type PacketOutput
- type PacketType
- type Position
- type Symbol
- type Telemetry
- type TelemetryMeta
- type Weather
Constants ¶
const DFReportPrefix = "DFS"
DFReportPrefix is the APRS direction-finding signalling prefix that may appear as the symbol code ('\' table + '#' code) or inline as "/BRG/NRQ" appended to a position comment.
Variables ¶
var ErrEmpty = errors.New("aprs: empty info field")
ErrEmpty is returned when the AX.25 info field contains no APRS data.
var ErrMicELonAmbiguous = errors.New("mic-e: longitude ambiguous (SPACE in info field)")
ErrMicELonAmbiguous reports that one of the three longitude bytes is a SPACE (0x20), which APRS101 ch 10 reserves as the "no/ambiguous data" marker for the Mic-E info-field longitude field. Some encoders (Yaesu FT-2D/FT-3D, Kenwood TH-D72) emit this state before GPS lock; the receiver MUST NOT combine the SPACE byte with the destination's longitude offset bit and pretend the result is a position. parseMicE surfaces this as a warn-and-drop rather than plotting the station 8000+ km from its actual location.
Functions ¶
func EncodeCompressedLatLon ¶
EncodeCompressedLatLon encodes a decimal latitude and longitude into the 4+4 character base-91 fields used by the compressed position report. Returns the concatenated 8 bytes YYYYXXXX.
func EncodeMessage ¶
EncodeMessage builds the info field for an APRS message. The result includes the leading ':' type indicator; callers concatenate it with the AX.25 header via ax25.NewUIFrame.
func EncodeMessageAck ¶
EncodeMessageAck builds an "ack{id}" reply targeted at the original sender.
func EncodeMicEDest ¶
EncodeMicEDest builds the 6-character destination callsign for a Mic-E transmission from a latitude and the message bits / hemisphere selectors. Exposed for the beacon encoder and unit tests.
func EncodePHG ¶
EncodePHG builds the on-air "PHGphgd" string (7 bytes) from decoded watts / feet HAAT / dB gain / directivity values. Input ranges:
watts 0..8281 (encoded as round(√watts), clamped 0..9) heightFt 10..5120 (encoded as round(log2(h/10)), clamped 0..9) gainDB 0..9 (clamped) directivity 0..8 (0 = omni, 1..8 = 45° × d)
Values outside the representable range are clamped with no error; the only error returned is for directivity > 8 or < 0 (structurally invalid, not merely coarse).
func EncodePosition ¶
EncodePosition builds the 19-byte uncompressed position field for a '!' or '=' packet. ambiguity is 0..4; non-zero replaces trailing minute/hundredth digits with spaces.
func EncodeTelemetry ¶
EncodeTelemetry builds the uncompressed "T#SSS,a1,a2,a3,a4,a5,DDDDDDDD" info field for a telemetry packet.
func HaversineDistanceMi ¶
HaversineDistanceMi returns the great-circle distance in statute miles between two points specified in decimal degrees.
Types ¶
type Capabilities ¶
type Capabilities struct {
Entries map[string]string // key → value (value empty for flag entries)
}
Capabilities is a <...> station capabilities advertisement (IGATE, etc.)
type DecodedAPRSPacket ¶
type DecodedAPRSPacket struct {
Raw []byte // original AX.25 frame bytes
Source string // callsign-SSID
Dest string
Path []string
Type PacketType
Position *Position
Message *Message
Weather *Weather
Telemetry *Telemetry
Object *Object
Item *Item
MicE *MicE
Caps *Capabilities
DF *DirectionFinding
TelemetryMeta *TelemetryMeta // PARM/UNIT/EQNS/BITS metadata (APRS101 ch 13)
ThirdParty *DecodedAPRSPacket // recursively-decoded inner packet for '}' traffic (APRS101 ch 20)
Status string // for '>' status reports
Comment string // residual free-form text after structured fields
Timestamp time.Time
Channel int
Quality int // modem-reported quality (0..100) if available
// Direction identifies the ingress path: DirectionRF for packets heard
// over RF via the modem bridge / KISS / AGW, DirectionIS for packets
// received from APRS-IS by the iGate. Unset (DirectionUnknown) when
// the packet is synthesized (e.g. inner third-party decode, tests) or
// constructed before ingress provenance is known.
Direction Direction
}
DecodedAPRSPacket is the canonical decoded form that flows through graywolf's PacketOutput pipeline.
func Parse ¶
func Parse(f *ax25.Frame) (*DecodedAPRSPacket, error)
Parse decodes an AX.25 UI frame into a DecodedAPRSPacket. The frame must already be UI (f.IsUI() == true); connected-mode frames return an error.
Parse is total: it never panics on malformed input. Fields for which no structured data could be extracted are left nil, and the packet type falls back to PacketUnknown (with the residual text in Comment).
func ParseInfo ¶
func ParseInfo(info []byte) (*DecodedAPRSPacket, error)
ParseInfo decodes an APRS info field (the bytes after the AX.25 PID) without an enclosing frame. Source/Dest/Path on the returned packet are empty. Useful for testdata loaders.
func (*DecodedAPRSPacket) DedupKey ¶
func (p *DecodedAPRSPacket) DedupKey() string
DedupKey returns a string suitable as a map key for APRS-level deduplication: the key is (source + info bytes), ignoring the AX.25 path and destination. This is the key the iGate uses for RF->IS duplicate suppression, where two identical payloads from the same source arriving via different geographic paths should be gated once (the first arrival) rather than once per path.
Returns an empty string if the packet has no recoverable info field; callers should treat an empty key as "do not dedup".
Distinct from ax25.Frame.DedupKey (which works at the frame layer with dest+source+info) and ax25.Frame.PathDedupKey (which the digipeater uses with source+dest+path+info). Those two operate on encoded frames; this one operates on a decoded APRS packet after the packet has been parsed out of the frame.
func (*DecodedAPRSPacket) FromAX25 ¶
func (p *DecodedAPRSPacket) FromAX25(f *ax25.Frame)
FromAX25 populates the Source/Dest/Path fields of a DecodedAPRSPacket from an AX.25 frame. Helper for parser entry points.
type DeviceInfo ¶
type DeviceInfo struct {
Vendor string `json:"vendor,omitempty"`
Model string `json:"model,omitempty"`
Class string `json:"class,omitempty"`
}
DeviceInfo identifies an APRS device from its tocall or mic-e identifier.
func LookupMicEDevice ¶
func LookupMicEDevice(comment string) *DeviceInfo
LookupMicEDevice returns device info from a Mic-E comment/status string. It checks both the modern 2-char suffix table and the legacy prefix/suffix table.
func LookupTocall ¶
func LookupTocall(dest string) *DeviceInfo
LookupTocall returns device info for an APRS destination callsign. Returns nil if no match found.
type Direction ¶
type Direction string
Direction identifies the provenance of a decoded packet as it flows through the APRS fan-out: RF (heard on-air via the modem / KISS / AGW ingress) vs IS (received from APRS-IS by the iGate). Downstream consumers (messages router, Source badge in the web UI, IS-mirror ack logic, RF-fallback policy) rely on this to make routing decisions.
type DirectionFinding ¶
type DirectionFinding struct {
Bearing int // degrees true
Number int // 0..9 station count
Range int // miles
Quality int // 0..9
}
DirectionFinding is a bearing/number/quality tuple attached to a position report (the "/BRG/NRQ" appendix).
type InboundPacket ¶
InboundPacket is a request from an external source to transmit an AX.25 frame on a specific channel.
type Item ¶
Item is an APRS item report (packet prefix ')'). Name is 3..9 chars terminated by '!' (live) or '_' (killed).
type LogOutput ¶
LogOutput is a PacketOutput that writes each decoded packet to a slog logger at info level. It is the default output wired up in cmd/graywolf and is safe for concurrent use.
func NewLogOutput ¶
NewLogOutput returns a LogOutput that falls back to slog.Default() when logger is nil.
func (*LogOutput) SendPacket ¶
func (l *LogOutput) SendPacket(_ context.Context, pkt *DecodedAPRSPacket) error
SendPacket emits a structured log record describing pkt.
type Message ¶
type Message struct {
Addressee string // 1..9 chars, space-padded in packet
Text string
MessageID string // optional identifier used for ACK/REJ correlation
ReplyAck string // piggybacked reply-ack id (aprs11/replyacks), empty if absent
HasReplyAck bool // true if a reply-ack trailer was present (ack may still be "")
IsAck bool
IsRej bool
IsBulletin bool // addressee starts with BLN
IsNWS bool // NWS-originated
}
Message is a directed-addressee message (addressee, text, id/ack/rej).
type MicE ¶
type MicE struct {
Position Position
MessageCode int // 0..7 index into the standard Mic-E message table
MessageText string
Manufacturer string // e.g. "Kenwood TH-D74", "" if unknown
Status string // trailing status text
}
MicE is a decoded Mic-E (' or `) position report.
type Object ¶
Object is an APRS object report (packet prefix ';'). Name is 9 chars exactly (space-padded). Live == false means "killed".
type PHG ¶
type PHG struct {
Raw string // four-digit "phgd" body (e.g. "7700"); never includes the "PHG" prefix
PowerWatts int // p² watts
HeightFt int // 10·2^h feet above average terrain
GainDB int // g dB (0..9)
Directivity int // 0=omni, 1..8 = 45°·d compass direction (N, NE, E, …)
}
PHG is the decoded APRS "PHGphgd" radio-capability extension (APRS101 chapter 7). It is carried in the position extension slot of fixed- station position, object, and item reports and advertises the transmitter's power, antenna height above average terrain, antenna gain, and antenna directivity so other stations can estimate coverage.
On the wire the extension is exactly seven ASCII bytes: the literal "PHG" followed by four digits "phgd". The digits are not the raw values; they are coarsely quantised exponents (see the PHG* helpers below for the exact formulas).
type PacketInput ¶
type PacketInput interface {
RecvPacket(ctx context.Context) (*InboundPacket, error)
Close() error
}
PacketInput is the pluggable source of external TX requests (KISS, AGW, APRS-IS, gRPC, ...).
type PacketOutput ¶
type PacketOutput interface {
SendPacket(ctx context.Context, pkt *DecodedAPRSPacket) error
Close() error
}
PacketOutput is the pluggable sink for decoded packets (log, KISS rebroadcast, iGate, gRPC, ...). Implementations must be safe for concurrent use.
type PacketType ¶
type PacketType string
PacketType is the high-level classification of an APRS packet, suitable for metrics and log filtering.
const ( PacketUnknown PacketType = "unknown" PacketPosition PacketType = "position" PacketMessage PacketType = "message" PacketTelemetry PacketType = "telemetry" PacketWeather PacketType = "weather" PacketObject PacketType = "object" PacketItem PacketType = "item" PacketMicE PacketType = "mic-e" PacketStatus PacketType = "status" PacketCapabilities PacketType = "capabilities" PacketDF PacketType = "df-report" PacketQuery PacketType = "query" PacketThirdParty PacketType = "third-party" )
type Position ¶
type Position struct {
Latitude float64 // decimal degrees, positive north
Longitude float64 // decimal degrees, positive east
Ambiguity int // 0..4, digits of ambiguity introduced by spaces
Altitude float64 // meters (0 if none reported)
HasAlt bool
Speed float64 // knots
Course int // degrees true (0..359)
HasCourse bool
Symbol Symbol
Compressed bool
Timestamp *time.Time // nil if positionless or no embedded time
LocalTime bool // true if the timestamp was the '/' local-time form (APRS101 ch 6)
PHG *PHG // decoded Power/Height/Gain/Directivity extension (APRS101 ch 7), nil if not present
DAODatum byte // DAO datum byte (APRS101 DAO extension), 0 if not present
}
Position is the decoded geographic location carried by a packet. Not every packet type has one (messages and telemetry do not).
type Telemetry ¶
type Telemetry struct {
Seq int // 0..999, -1 if absent
Analog [5]float64
AnalogHas [5]bool // true for channels actually reported (distinguishes 0 from missing)
Digital uint8 // bits 0..7 (only lower 8)
HasDigital bool
Comment string // trailing free-form
}
Telemetry is an APRS telemetry packet (T# uncompressed or base-91 compressed form). Values are raw (unscaled) analog and digital channels; calibration coefficients live in parameter/equation/unit packets that are out-of-scope for Phase 3 decoding.
type TelemetryMeta ¶
type TelemetryMeta struct {
Kind string // "parm", "unit", "eqns", or "bits"
Parm [13]string
Unit [13]string
Eqns [5][3]float64 // a, b, c coefficients per analog channel
Bits uint8 // BITS. sense-bits bitmap (active-high per bit)
ProjectName string // BITS. project title
}
TelemetryMeta carries PARM/UNIT/EQNS/BITS metadata messages (APRS101 ch 13). These arrive as messages addressed to the telemetering station itself and are required to scale raw analog channels.
type Weather ¶
type Weather struct {
WindDirection int // degrees true
HasWindDir bool
WindSpeed float64 // mph (1-minute sustained)
HasWindSpeed bool
WindGust float64 // mph (5-minute peak)
HasWindGust bool
Temperature float64 // degrees F
HasTemp bool
Rain1Hour float64 // hundredths of an inch
HasRain1h bool
Rain24Hour float64
HasRain24h bool
RainSinceMid float64
HasRainMid bool
Humidity int // percent (0..100)
HasHumidity bool
Pressure float64 // tenths of millibar (e.g. 10132 = 1013.2)
HasPressure bool
Luminosity int // watts/m^2
HasLuminosity bool
Snowfall24h float64 // inches (via 's' after 'g')
HasSnow bool
RawRainCounter int // raw rain counter ('#' field)
HasRawRain bool
SoftwareType string // one-letter software code (e.g. 'w', 'x', 'd')
WeatherUnitTag string // 2..4 ASCII letters identifying the unit/model
}
Weather carries the APRS weather report fields (APRS101 ch 12). Unreported fields leave the corresponding Has* flag false.