aprs-decode 0.1.2

Robust APRS packet parsing — text (APRS-IS) and binary (AX.25) input
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# 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:DATA` string 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

```toml
[dependencies]
aprs-decode = { path = "..." }          # local path while the crate is unpublished

# Enable JSON/serde support (optional):
aprs-decode = { path = "...", features = ["serde"] }
```

---

## Quick example

```rust
use aprs_decode::{AprsData, AprsPacket};

let raw = b"W1AW-9>APRS,WIDE1-1,WIDE2-2:!4903.50N/07201.75W-Home station";
let pkt = AprsPacket::decode_textual(raw)?;

println!("from: {}  to: {}", pkt.from, pkt.to);

if let AprsData::Position(pos) = &pkt.data {
    println!("lat: {:.4}  lon: {:.4}",
        pos.position.latitude.value(),
        pos.position.longitude.value());
    println!("symbol: {}/{}", pos.position.symbol.table, pos.position.symbol.code);
}
```

A runnable tour of all packet types is in [`examples/decode.rs`](examples/decode.rs):

```
cargo run --example decode
```

---

## Core API

### `AprsPacket`

The top-level type. Every decoded packet is an `AprsPacket`.

```rust
pub struct AprsPacket {
    pub from: Callsign,
    pub to:   Callsign,
    pub via:  Vec<Digipeater>,
    pub data: AprsData,
}
```

| 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

```rust
// bytes from a TNC, soundmodem, or rtl-sdr + multimon-ng
let pkt = AprsPacket::decode_ax25(&frame_bytes)?;
```

### Re-encoding

```rust
let pkt = AprsPacket::decode_textual(raw)?;

// convert text → binary
let ax25_frame = pkt.encode_ax25()?;

// convert binary → text
let pkt2 = AprsPacket::decode_ax25(&ax25_frame)?;
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.

```rust
pub struct AprsPosition {
    pub timestamp:           Option<Timestamp>,
    pub messaging_supported: bool,
    pub position:            Position,
    pub extension:           Option<Extension>,
    pub weather:             Option<AprsWeatherData>,  // set when symbol is /_
    pub frequency_mhz:       Option<f32>,
    pub comment:             Vec<u8>,
}
```

- `!` / `=` — no timestamp; `=` additionally signals that the station can receive messages
- `/` / `@` — timestamp present; `@` additionally signals messaging support
- `position.latitude.value()` and `position.longitude.value()` return decimal degrees as `f64`
- `position.symbol` identifies the map icon (see [Symbols]#symbols)
- `extension` may contain course/speed, PHG (power-height-gain), RNG, or DFS data
- `weather` is populated when the symbol is `/_` (weather station); see [Weather]#weather-aprsdata-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.

```rust
pub struct AprsMicE {
    pub latitude:     Latitude,
    pub longitude:    Longitude,
    pub precision:    Precision,
    pub message:      MicEMessage,   // M0–M6, C0–C6, Emergency, Unknown
    pub speed:        MicESpeed,     // .knots()
    pub course:       MicECourse,    // .degrees()
    pub symbol_code:  char,
    pub symbol_table: char,
    pub altitude_m:   Option<f64>,
    pub device:       Option<MicEDevice>,  // manufacturer + model if recognized
    pub comment:      Vec<u8>,
    pub is_current:   bool,          // true = current position, false = old
}
```

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

```rust
pub struct AprsMessage {
    pub addressee: Vec<u8>,        // destination call, trimmed of padding
    pub text:      Vec<u8>,
    pub subtype:   MessageSubtype,
}
```

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

```rust
pub struct AprsObject {
    pub name:          Vec<u8>,
    pub live:          bool,       // false = object has been killed/removed
    pub timestamp:     Timestamp,
    pub position:      Position,
    pub extension:     Option<Extension>,
    pub frequency_mhz: Option<f32>,
    pub comment:       Vec<u8>,
}
```

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

```rust
pub struct AprsTelemetry {
    pub sequence: Vec<u8>,          // sequence number (000–999)
    pub analog:   [Option<f32>; 5], // five analog channels; None = absent/unparseable
    pub digital:  u8,               // eight digital bits packed (bit 7 = channel 1)
    pub comment:  Vec<u8>,
}
```

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.

```rust
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)` — the `bool` is 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`

```rust
Timestamp::Ddhhmm(day, hour, minute)  // "282245z" → Ddhhmm(28, 22, 45) UTC
Timestamp::Hhmmss(hour, minute, sec)  // "074849h" → Hhmmss(7, 48, 49) UTC
Timestamp::Unsupported(raw)           // 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`

```rust
pub struct Position {
    pub latitude:  Latitude,
    pub longitude: Longitude,
    pub symbol:    Symbol,
    pub precision: Precision,
    pub altitude:  Option<Altitude>,  // present only in compressed position format
}
```

`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

```rust
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:

```rust
Extension::DirectionSpeed { direction_degrees, speed_knots }
Extension::Phg { power_watts, antenna_height_feet, antenna_gain_db, directivity }
Extension::Rng { range_miles }
Extension::Dfs { s_points, antenna_height_feet, antenna_gain_db, directivity }
```

---

## Error handling

All decode functions return `Result<_, AprsError>`. `AprsError` implements `std::error::Error`
via [`thiserror`](https://docs.rs/thiserror) and has a structured variant for every failure mode.

```rust
match AprsPacket::decode_textual(raw) {
    Ok(pkt) => { /* use pkt */ }
    Err(e)  => eprintln!("parse error: {e}"),
}
```

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 |

```toml
aprs-decode = { path = "...", features = ["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 `qAX` via tokens added by IGates

---

## License

Licensed under either of:

- **MIT License** ([LICENSE-MIT]LICENSE-MIT or http://opensource.org/licenses/MIT)
- **Apache License, Version 2.0** ([LICENSE-APACHE]LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)

at your option.