APRS has three ways to put a position on the air. The uncompressed format is human-readable and wasteful; the compressed format halves it; Mic-E hides half the payload inside the AX.25 destination address and is what most mobile radios actually transmit.
This guide decodes the latter two by hand. If you are writing a parser, building an IGate, or trying to work out why a tracker reports the wrong hemisphere, this is the layer where the answer lives.
Why not just send decimal degrees?
At 1200 baud on a shared channel, an uncompressed position report costs 19 characters before you have said anything else:
3554.12N/08646.50W>
The compressed format expresses the same position, plus course and speed, in 13 characters, with better resolution. On a channel where every station shares one frequency, that is not micro-optimisation — it is a direct reduction in collision probability.
Compressed position format
Thirteen bytes, fixed, immediately after the data type identifier:
offset: 0 1 2 3 4 5 6 7 8 9 10 11 12
/ Y Y Y Y X X X X $ c s T
│ └────┬────┘ └────┬────┘ │ └──┬─┘ │
│ latitude longitude │ course/ └ compression
│ (base-91) (base-91) │ speed type byte
└ symbol table └ symbol code
Base-91 encoding
Four printable ASCII characters encode one coordinate. Each byte carries a digit in base 91, offset so the result is always printable:
value = (b0-33)·91³ + (b1-33)·91² + (b2-33)·91 + (b3-33)
Subtracting 33 maps the printable range ! (0x21) through { (0x7B) onto 0–90. Then:
latitude = 90 − (value / 380926)
longitude = −180 + (value / 190463)
Those divisors are simply the range scaled to the encodable space: 91⁴ = 68,574,961, and 68574961 / 180 ≈ 380,972 for latitude, 68574961 / 360 ≈ 190,486 for longitude. The specification fixes the constants at 380926 and 190463, and a parser must use those exact values or it will drift at the extremes.
Resolution works out to roughly 0.3 metres — considerably finer than the uncompressed format’s hundredths of a minute, in fewer bytes.
Worked decode
Take the compressed position /5L!!<*e7>{?!:
- Byte 0 is
/— primary symbol table. - Bytes 1–4 are
5L!!. Values:5=53−33=20,L=76−33=43,!=0,!=0.20·753571 + 43·8281 + 0·91 + 0 = 15071420 + 356083 = 15427503latitude = 90 − (15427503 / 380926) = 90 − 40.4999… ≈ 40.50°N - Bytes 5–8 are
<*e7. Values:<=27,*=9,e=68,7=22.27·753571 + 9·8281 + 68·91 + 22 = 20346417 + 74529 + 6188 + 22 = 20427156longitude = −180 + (20427156 / 190463) = −180 + 107.25… ≈ −72.75° - Byte 9 is
>— symbol code, a car. - Bytes 10–12 are
{?!— the csT group.
The csT bytes
Byte 12, the compression type byte, decides how bytes 10 and 11 are read. Subtract 33 from each byte first, then inspect bits 3–4 of T:
T & 0x18 == 0x10→ altitude.altitude_feet = 1.002^(c·91 + s), giving a range up to roughly 10,000 km with logarithmic resolution.cin 0–89 → course and speed.course = c · 4degrees;speed = 1.08^s − 1knots. The exponential speed scale gives fine resolution at walking pace and coarse resolution at aircraft speeds, which is exactly the right trade.c == 90→ pre-calculated radio range.range = 2 · 1.08^smiles.c == ' '(space, i.e. 0 after offset) → no course/speed data present.
Bit 5 of T (0x20) is the GPS fix status: set means a current fix, clear means the position is old or estimated. Bits 0–2 encode the NMEA source and origin.
1.08^s − 1 over 90 values spans 0 to roughly 1,000 knots. Near zero the steps are fractions of a knot; near the top they are tens of knots. A linear scale over the same byte would give you 11-knot granularity for a pedestrian, which is useless, or a ceiling of 90 knots, which excludes aircraft. The logarithmic mapping gets both.
Mic-E
Mic-E is the clever and genuinely confusing one. It compresses a position report into the destination address field plus a short information field, exploiting the fact that the destination address is seven bytes that APRS does not otherwise need.
A Mic-E frame is identified by a data type identifier of ` (current GPS data) or ' (old data). Some older implementations use 0x1C and 0x1D.
Latitude in the destination address
The six call sign characters of the destination encode latitude as DD MM hh — degrees, minutes, hundredths of minutes. But each character simultaneously carries a message bit, so the same character position encodes both a digit and a status bit. That is why the mapping looks arbitrary:
| Character | Digit | Message bit | Notes |
|---|---|---|---|
0–9 |
0–9 | 0 | Digit as-is |
A–J |
0–9 | 1 (custom) | Subtract 17 from ASCII |
K |
space | 1 (custom) | Position ambiguity |
L |
space | 0 | Position ambiguity |
P–Y |
0–9 | 1 (standard) | Subtract 48 from ASCII |
Z |
space | 1 (standard) | Position ambiguity |
So a destination of T7SVSU decodes digit-wise as 4 7 3 6 3 6 → 47° 36.36′, while the first three characters T, 7, S contribute message bits 1, 0, 1.
The message bits matter. Three bits give eight message codes. If any of the three characters came from the A–K group, the custom table applies; otherwise the standard table:
| Bits | Standard | Custom |
|---|---|---|
| 111 | Off duty | Custom-0 |
| 110 | En route | Custom-1 |
| 101 | In service | Custom-2 |
| 100 | Returning | Custom-3 |
| 011 | Committed | Custom-4 |
| 010 | Special | Custom-5 |
| 001 | Priority | Custom-6 |
| 000 | Emergency | Emergency |
000 is Emergency in both tables. A Mic-E frame with all three message bits clear is an emergency declaration, and APRS clients will alarm on it. This is worth knowing before you experiment with hand-crafted Mic-E frames.
Three more flags ride in the remaining destination characters:
- Character 4 sets the N/S hemisphere: ASCII ≤
L(0x4C) means South, above means North. - Character 5 sets the longitude offset: ≥
P(0x50) adds 100° to the decoded longitude degrees. - Character 6 sets E/W: ≥
P(0x50) means West.
Longitude in the information field
The first three bytes after the data type identifier carry longitude, each offset by 28:
d = byte0 − 28 degrees
if longitude_offset: d += 100
if 180 ≤ d ≤ 189: d −= 80
if 190 ≤ d ≤ 199: d −= 190
m = byte1 − 28 minutes
if m ≥ 60: m −= 60
h = byte2 − 28 hundredths of minutes
longitude = d + (m + h/100) / 60 (negated if W)
Those −80 and −190 corrections are not arbitrary: they exist because the encoded byte range wraps, and the corrections fold the wrapped values back onto valid degrees. Getting them wrong produces positions in the ocean, which is the classic symptom of a half-finished Mic-E parser.
Speed and course
The next three bytes, again offset by 28:
speed = (byte3 − 28) · 10 + (byte4 − 28) / 10 knots
course = ((byte4 − 28) mod 10) · 100 + (byte5 − 28) degrees
if speed ≥ 800: speed −= 800
if course ≥ 400: course −= 400
Byte 4 is split: its tens digit belongs to speed, its units digit to course. That packing is the reason a naive parser produces plausible-looking speeds with wildly wrong headings.
After these six bytes come the symbol code and symbol table identifier — note the reversed order compared to every other APRS position format — and then optional telemetry or a comment, which may contain an altitude in the form xxx} where xxx is base-91 metres above 10,000 m below sea level.
A Mic-E destination address looks like a call sign and is not one. Tools that “validate” the destination as a call sign will reject perfectly good Mic-E frames, and logs that show you T7SVSU as a destination are showing you a latitude. This trips up almost everyone the first time.
Which format should you transmit?
You largely do not choose — your radio or tracker does. But the trade-offs are worth knowing:
- Uncompressed is readable in a raw packet monitor and trivially debugged. Fine for a fixed station beaconing every 30 minutes, where airtime is not a concern.
- Compressed is strictly better than uncompressed for anything mobile: fewer bytes, more resolution, course and speed included. Use it if your software offers it.
- Mic-E is the most efficient of the three and what most commercial mobile radios emit. Its one real cost is debuggability.
For a weather station, the relevant format is different again — see APRS Weather Stations.
Next: APRS-IS: Servers, Filters, and Gateways covers what happens once a frame leaves RF.