aprs

package
v0.12.4 Latest Latest
Warning

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

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

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

View Source
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

View Source
var ErrEmpty = errors.New("aprs: empty info field")

ErrEmpty is returned when the AX.25 info field contains no APRS data.

View Source
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

func EncodeCompressedLatLon(lat, lon float64) []byte

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

func EncodeMessage(addressee, text, id string) ([]byte, error)

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

func EncodeMessageAck(addressee, id string) ([]byte, error)

EncodeMessageAck builds an "ack{id}" reply targeted at the original sender.

func EncodeMicEDest

func EncodeMicEDest(lat float64, msgCode int, lonOffset100 bool, westLong bool) string

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

func EncodePHG(watts, heightFt, gainDB, directivity int) (string, error)

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

func EncodePosition(p Position, messaging bool) ([]byte, error)

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

func EncodeTelemetry(t Telemetry) ([]byte, error)

EncodeTelemetry builds the uncompressed "T#SSS,a1,a2,a3,a4,a5,DDDDDDDD" info field for a telemetry packet.

func HaversineDistanceMi

func HaversineDistanceMi(lat1, lon1, lat2, lon2 float64) float64

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.

const (
	DirectionUnknown Direction = ""
	DirectionRF      Direction = "rf"
	DirectionIS      Direction = "is"
)

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

type InboundPacket struct {
	Raw     []byte
	Source  string
	Channel int
}

InboundPacket is a request from an external source to transmit an AX.25 frame on a specific channel.

type Item

type Item struct {
	Name     string
	Live     bool
	Position *Position
	Comment  string
}

Item is an APRS item report (packet prefix ')'). Name is 3..9 chars terminated by '!' (live) or '_' (killed).

type LogOutput

type LogOutput struct {
	Logger *slog.Logger
}

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

func NewLogOutput(logger *slog.Logger) *LogOutput

NewLogOutput returns a LogOutput that falls back to slog.Default() when logger is nil.

func (*LogOutput) Close

func (l *LogOutput) Close() error

Close is a no-op; log handlers flush themselves.

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

type Object struct {
	Name      string
	Live      bool
	Timestamp *time.Time
	Position  *Position
	Comment   string
}

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).

func ParsePHG

func ParsePHG(body string) (*PHG, error)

ParsePHG decodes a four-character "phgd" body (no "PHG" prefix) into a *PHG. The input must be exactly four ASCII digits.

func (*PHG) String

func (p *PHG) String() string

String returns the on-air encoding of the PHG extension including the "PHG" prefix (seven bytes). If the receiver is nil it returns "".

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 Symbol

type Symbol struct {
	Table byte
	Code  byte
}

Symbol is the APRS map symbol: table+code (e.g. '/' + '>' = car).

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.

Jump to

Keyboard shortcuts

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