aprs-decode
A Rust library for parsing and encoding APRS (Automatic Packet Reporting System) packets.
Two input formats are supported:
- Text (APRS-IS) — the human-readable
FROM>TO,VIA:DATAstring format used by internet servers and log files - Binary (AX.25) — raw UI frame bytes as received from a TNC or software modem
Both formats round-trip: every decoded packet can be re-encoded back to the original wire representation.
Adding to your project
[]
= { = "..." } # local path while the crate is unpublished
# Enable JSON/serde support (optional):
= { = "...", = ["serde"] }
Quick example
use ;
let raw = b"W1AW-9>APRS,WIDE1-1,WIDE2-2:!4903.50N/07201.75W-Home station";
let pkt = decode_textual?;
println!;
if let Position = &pkt.data
A runnable tour of all packet types is in examples/decode.rs:
cargo run --example decode
Core API
AprsPacket
The top-level type. Every decoded packet is an AprsPacket.
| Method | Description |
|---|---|
AprsPacket::decode_textual(&[u8]) |
Parse an APRS-IS text frame |
AprsPacket::decode_ax25(&[u8]) |
Parse a raw AX.25 UI frame |
pkt.encode_textual() |
Re-encode to APRS-IS text → Vec<u8> |
pkt.encode_ax25() |
Re-encode to AX.25 binary → Vec<u8> |
Both decode methods accept &[u8] rather than &str because APRS information fields can
contain arbitrary bytes (MIC-E in particular uses \x1c and \x1d).
Decoding from AX.25
// bytes from a TNC, soundmodem, or rtl-sdr + multimon-ng
let pkt = decode_ax25?;
Re-encoding
let pkt = decode_textual?;
// convert text → binary
let ax25_frame = pkt.encode_ax25?;
// convert binary → text
let pkt2 = decode_ax25?;
let text = pkt2.encode_textual?;
Packet types — AprsData
The data field is an enum dispatched by the Data Type Indicator (DTI) — the first byte
of the information field.
Position — AprsData::Position(AprsPosition)
DTI: ! = / @
The most common packet type. Reports a station's geographic location.
!/=— no timestamp;=additionally signals that the station can receive messages//@— timestamp present;@additionally signals messaging supportposition.latitude.value()andposition.longitude.value()return decimal degrees asf64position.symbolidentifies the map icon (see Symbols)extensionmay contain course/speed, PHG (power-height-gain), RNG, or DFS dataweatheris populated when the symbol is/_(weather station); see Weather
MIC-E — AprsData::MicE(AprsMicE)
DTI: ` ' \x1c \x1d
A compact encoding used by Kenwood and Yaesu HTs and mobiles. Latitude and message type are encoded in the destination callsign; longitude, speed, and course are in the first 8 bytes of the information field.
Important: MIC-E packets frequently contain non-printable bytes. Copy/pasting from web
tools like aprs.fi silently drops them, producing garbage speed, course, and altitude values.
When embedding MIC-E frames in source code, use \xNN escape sequences for any byte outside
printable ASCII. Reading directly from an APRS-IS socket or a saved .tnc2 log file gives
byte-accurate data.
Message — AprsData::Message(AprsMessage)
DTI: :
Directed messages, bulletins, ACKs, REJs, and telemetry metadata.
MessageSubtype distinguishes:
| Variant | Meaning |
|---|---|
Directed { id } |
Directed message; id is the optional message number |
Ack { id } |
Acknowledgement |
Rej { id } |
Rejection |
Bulletin |
General bulletin (addressee starts with BLN) |
NwsBulletin |
NWS / weather alert bulletin |
TelemetryParm / TelemetryUnit / TelemetryEqns / TelemetryBits |
Telemetry metadata |
DirectedQuery |
Directed station query |
Status — AprsData::Status(AprsStatus)
DTI: >
Free-text status message, optionally with a Maidenhead grid square or timestamp.
Object — AprsData::Object(AprsObject)
DTI: ;
Reports the location of something other than the sending station — a storm, event, or fixed infrastructure. Objects have a 9-character name and a mandatory timestamp.
Item — AprsData::Item(AprsItem)
DTI: )
Similar to an Object but with a shorter name (1–9 characters) and no timestamp. Typically used for points of interest.
Weather — AprsData::Weather(AprsPositionlessWeather)
DTI: _
A positionless weather report (no coordinates). When a weather station transmits its position
and weather data simultaneously, the weather fields are embedded in a Position packet and
found in AprsPosition::weather instead.
Weather fields use unit-aware newtypes with conversion methods:
| Type | Native APRS unit | Conversion methods |
|---|---|---|
WindDirection |
degrees | .degrees() |
WindSpeed |
mph | .mph() .knots() .kph() .m_per_s() |
Temperature |
°F | .fahrenheit() .celsius() .kelvin() |
Rainfall |
1/100 inch | .hundredths_inch() .inches() .mm() |
Humidity |
% | .percent() |
Pressure |
1/10 mbar | .tenths_mbar() .hpa() .mbar() |
Telemetry — AprsData::Telemetry(AprsTelemetry)
DTI: T#
Numeric sensor data from remote stations.
To interpret the raw analog values with engineering units, the station must also transmit
telemetry metadata packets (PARM. / UNIT. / EQNS.), which arrive as
AprsData::Message with the appropriate MessageSubtype.
Other types
| Variant | DTI | Description |
|---|---|---|
Capabilities(AprsCapabilities) |
< |
Station capabilities list |
Query(AprsQuery) |
? |
General network query |
GridLocator(AprsGridLocator) |
[ |
Maidenhead grid locator beacon |
Nmea(AprsNmea) |
$ |
Raw NMEA sentence pass-through |
ThirdParty(AprsThirdParty) |
} |
Packet forwarded from another network |
UserDefined(AprsUserDefined) |
{ |
Experimental / application-specific |
Unknown { dti, data } |
any | Unrecognized DTI; raw bytes preserved |
The Unknown variant is intentional — unrecognized packets are not errors. The dti byte
and the full raw data are preserved for the caller to inspect.
Supporting types
Callsign
Stored as uppercase ASCII in a fixed inline buffer (no heap allocation). SSID range is 0–15.
pkt.from.as_str // "W1AW"
pkt.from.ssid // Some(9)
pkt.from.to_string // "W1AW-9"
Digipeater
Each element of pkt.via is one of:
Digipeater::Callsign(Callsign, bool)— theboolis the "has been heard" flag (*suffix on wire)Digipeater::QConstruct(QConstruct, Callsign)— APRS-IS Q-construct paired with the IGate callsign
Common Q-constructs: qAR (bidirectional IGate), qAO (RF origin), qAC (verified login).
Timestamp
Ddhhmm // "282245z" → Ddhhmm(28, 22, 45) UTC
Hhmmss // "074849h" → Hhmmss(7, 48, 49) UTC
Unsupported // local-time "/" suffix (deprecated in APRS101)
Note that Ddhhmm carries only the day-of-month, not the full date. The current month and
year must be inferred from wall-clock time by the application.
Position
Latitude::value() and Longitude::value() return decimal degrees as f64
(negative = South / West).
Precision reflects how many digits were significant in the wire encoding:
HundredthMinute (full precision, ≈18 m) down to TenDegree (very coarse, ≈1100 km).
Symbols
let sym = &pos.position.symbol;
sym.table // '/' = primary table, '\\' = alternate, 'A'–'Z'/'0'–'9' = overlay
sym.code // specific icon within the table
sym.description // Option<&'static str>, e.g. Some("Car"), Some("Weather Station")
sym.is_primary_table
sym.is_alternate_table
sym.overlay // Some('3') if an alphanumeric overlay, else None
Extension
Optional 7-byte data extension that follows the position in the comment field:
DirectionSpeed
Phg
Rng
Dfs
Error handling
All decode functions return Result<_, AprsError>. AprsError implements std::error::Error
via thiserror and has a structured variant for every failure mode.
match decode_textual
Notable variants:
| Variant | Cause |
|---|---|
EmptyPacket |
Zero-length input |
MissingInfoDelimiter |
No : separating header from data |
MissingDestinationDelimiter |
No > separating source from destination |
InvalidCallsign |
Malformed callsign or SSID out of range 0–15 |
Ax25FrameTooShort |
Binary frame shorter than minimum AX.25 UI frame |
Ax25NotUiFrame |
Control byte is not 0x03 |
Ax25NotAprsPid |
PID byte is not 0xF0 |
TruncatedPacket |
Field shorter than its required length |
InvalidLatitude / InvalidLongitude |
Coordinate format error |
MicETooShort |
MIC-E info field shorter than 8 bytes |
Optional features
| Feature | Effect |
|---|---|
serde |
Derives Serialize / Deserialize on all public types; Callsign serializes as a plain string |
= { = "...", = ["serde"] }
APRS-IS connection note
To decode live packets, open a TCP connection to rotate.aprs2.net:14580, send a login line,
and read newline-delimited frames. Each line (excluding server comments that begin with #) is
a complete APRS-IS frame suitable for AprsPacket::decode_textual. Lines include a trailing
\r\n that the parser tolerates. Lines starting with # should be skipped — passing them to
decode_textual will return Err(MissingInfoDelimiter).
Specification references
- APRS Protocol Reference 1.0.1 (APRS101.pdf) — the primary APRS specification
- AX.25 Link Access Protocol for Amateur Packet Radio v2.2 — defines the binary frame format
- APRS-IS Q-construct specification — documents the
qAXvia tokens added by IGates
License
Licensed under either of:
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
at your option.