1.0 Candidate (2026-10-30), open for public comment. The frame layout is frozen at header version 1 (§9.3). 1.0 final follows the field trial and review (decision 0020), with clarifications only in between. Every choice in this document has a decision record in decisions/; §16 lists them, with the work left for later versions. Qn refers to OPEN-QUESTIONS.md, where each question names the decision that answered it. Facts cited as “F§n” are in research/00-findings.md.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be read as in RFC 2119.
Reading order: §9 (frame format) defines the common header that every subtype in §5-8 starts with.
APRS (APRS-IS and RF) and MeshCore (a LoRa mesh on 915-928 MHz LIPD in Australia) both carry station positions, short messages and telemetry, with no common rules between them. Without rules, gateways flood each side with the other’s traffic, loop packets back, and put duplicate or unverified stations on aprs.fi. MeshPRS defines:
The name stands for Mesh Position Reporting System (decision 0014).
Kept from APRS: the station model (callsign-SSID), position reports with symbols and comments, messages with IDs and acks, bulletins, objects and items, telemetry (T#, PARM, UNIT, EQNS, BITS), weather, status, and queries.
Dropped: AX.25 addressing (the mesh frame carries one source callsign and, where needed, one addressee, nothing else), digipeater paths and WIDEn-N aliases (the mesh does its own flooding), KISS and HDLC framing, the AX.25 destination field as a carrier of data (Mic-E), and the q-construct on the mesh side.
Out of scope:
Which subtypes 1.0 requires is in §15 (decision 0007). Every subtype in decision 0002 is fully defined, required or not.
Kelly Page’s receive-only iGate (kelly-page/js/igate.js, released in Kelly 0.4.34) gates MeshCore adverts from whitelisted keys to APRS-IS as CALL-SSID>APZKPG,qAO,IGATECALL:!DDMM.mmS/DDDMM.mmE>comment. MeshPRS calls this profile 0, the advert profile. The name is chosen so it does not clash with header version 0 (§9.3).
Profile 0 stays valid (§5.4), and this specification stays compatible with it: same tocall, q-construct, position text format, gate order and drop-reason model.
qAO). Roles are in §3. MeshCore station MeshCore mesh (LIPD, 923.125 MHz) Gateway APRS-IS
(app + stock companion) --GRP_DATA flood, MeshPRS channel--> repeaters --> decode, trust, --qAO--> aunz.aprs2.net
<--GRP_DATA (G=1), messages only---- repeaters <-- IS->mesh rules <--filter-
CMD_SEND_CHANNEL_DATA, with the compact GRP_TXT text profile (§9.7) as the fallback for stock phone apps. Stock repeaters flood both without decrypting them (F§2, F§2.1; decision 0001).There are three gateway roles (decision 0025). One box MAY hold several.
| Role | Mesh → APRS-IS | APRS-IS → mesh | Notes |
|---|---|---|---|
| iGate (receive-only) | Positions, objects, weather, telemetry, status, bulletins, messages and acks from bound stations | Nothing | Today’s Kelly iGate role. qAO (§11) |
| Messaging gateway | As the iGate | Messages and acks addressed to stations heard on the mesh, sent direct only, never flooded (§6.5) | Acks are end-to-end (§6.5) |
| Object server | Objects it owns (repeaters, EOCs, evacuation centres) | Nothing | §8.2 |
A gateway MUST NOT carry anything onto the mesh that this section does not list for its role. In particular, a gateway MUST NOT put general APRS-IS positions onto the mesh (no IS → mesh flooding).
Every callsign in a frame (source, addressee, ack target, query target) is a 6-byte base-40 callsign:
A-Z, 27-36 = 0-9, 37 = -, 38 = /, 39 = ..Σ c[i]·40^i for the characters c[0]..c[k-1] of the text, first character least significant, so the padding never appears in the value.This is the M17 address encoding, chosen so implementations can reuse M17 code and so the full APRS-IS callsign shape (up to 9 characters, alphanumeric SSID) fits in 6 bytes.
M17_spec.tex Appendix “Address Encoding”, commit d3995d3, 2026-10-16): the same 40-character alphabet in the same order, the first character in the least significant position, and the 6-byte address stored big-endian (“network byte order”).AB1CD = 0x9FDD51.0xFFFFFFFFFFFF as BROADCAST and 0xEE6B28000000-0xFFFFFFFFFFFE as an application range. MeshPRS rejects all values ≥ 40⁹ instead.Examples (verified by script): VK4ABC = 00 00 12 9F 5D 8E; VK4AZ-9 = 00 23 3A DD DD 8E.
-0 MUST NOT be used; no SSID is allowed.validBase: 1-3 prefix characters, a digit, 1-4 letters, at most 6 in all), then optionally - and a 1-2 character alphanumeric SSID (F§4.1). A gateway MUST drop a frame that fails (bad callsign).A GRP_DATA packet carries no sender identity: only a 1-byte channel hash in the clear, then ciphertext under a key anyone can derive (F§1.5). The callsign in a frame is therefore self-asserted. MeshPRS ties it to a node key like this:
untrusted, no key, key mismatch). A frame without K is therefore mesh-only. This is default deny, as in profile 0, and finding R2 (F§5.5).After the common header (§9.3: 9 bytes, or 13 with K):
| Field | Type | Size | Units / scaling | Range / notes |
|---|---|---|---|---|
| lat | i24 LE | 3 | degrees = v × 90 / 2²³ | −90 to +89.99999°. Resolution 1.07×10⁻⁵° ≈ 1.19 m |
| lon | i24 LE | 3 | degrees = v × 180 / 2²³ | −180 to +179.99998°. Resolution 2.15×10⁻⁵° ≈ 2.39 m at the equator |
| symbol | 2 × ASCII | 2 | APRS symbol table/overlay, then symbol code | Table /, \, or overlay 0-9 A-Z. Code 0x21-0x7E |
| pflags | u8 | 1 | bit 0 ALT, bit 1 CSE, bit 2 TS, bits 3-5 ambiguity 0-4 (5-7 reserved), bit 6 MSG, bit 7 reserved | |
| alt | i16 LE | 2 if ALT | metres | −32768 to 32767 |
| course | u8 | 1 if CSE | degrees = v × 360 / 256 (0 = north) | 1.4° steps |
| speed | u8 | 1 if CSE | km/h | 0-254; 255 = 255 km/h or more |
| ts | u32 LE | 4 if TS | sender’s Unix time, s | Informative only (§5.3) |
| comment | ASCII | rest of blob | printable 0x20-0x7E, no \| or ~ |
0-43 bytes. The gateway truncates to fit APRS (§10.3) |
= instead of !.fmt): 1-4 blank the hundredths, tenths, units and tens of minutes. A sender that sets ambiguity SHOULD also round lat/lon itself, because a gateway that is not MeshPRS-aware would show the full precision.v = sign(x) · ⌊|x| · 2²³ / 90 + 0.5⌋ for latitude (/ 180 for longitude): round half away from zero, clamped to the i24 range. −27.4680 encodes to −2560203.VK4ABC at −27.4680, 153.0235, car symbol />, comment “Mobile”, K set, seq 0x2A. The node key is illustrative: its prefix comes from sha256("MeshPRS illustrative node key VK4ABC").
00: 50 00 2A 00 00 12 9F 5D 8E 02 F9 52 3D 35 EF D8
10: 14 D1 6C 2F 3E 00 4D 6F 62 69 6C 65
| Bytes | Field | Value |
|---|---|---|
50 |
header | version 1, G 0, K 1, gating 0, retry 0. A first retry would be 51, with every other byte the same |
00 |
subtype | Position |
2A |
seq | 42 |
00 00 12 9F 5D 8E |
source | VK4ABC |
02 F9 52 3D |
key prefix | (illustrative) |
35 EF D8 |
lat | −27.467998° |
14 D1 6C |
lon | 153.023500° |
2F 3E |
symbol | /> |
00 |
pflags | no options, no ambiguity, not messaging-capable |
4D 6F 62 69 6C 65 |
comment | “Mobile” |
!2728.08S/15301.41E>Mobile is 26 bytes. The gated line VK4ABC>APZKPG,qAO,VK4AZ-10:!2728.08S/15301.41E>Mobile is 53 bytes plus CR/LF.VK4ABC Mobile: MPRS VK4ABC !2728.08S/15301.41E>Mobile is 53 bytes of the 160, and 69 bytes on air.Second example, with K, ALT 45 m, CSE 90° at 60 km/h, no comment, from VK4ABC-9: 50 00 2B 05 80 A6 53 5D 8E 02 F9 52 3D 35 EF D8 14 D1 6C 2F 3E 03 2D 00 40 3C.
!2728.08S/15301.41E>090/032/A=000148 (36 bytes).SRC>TOCALL,qAO,IGATE: followed by ! (or = when MSG is set), then the uncompressed position, the CSE extension, the altitude and the comment.checkPosition: no fix, out of range, 0,0, stale.A station sending position frames SHOULD beacon when any one of these holds since its last sent position:
| Trigger | Default | Notes |
|---|---|---|
| Moved more than | 100 m | Great-circle distance from the last sent position |
| Heading changed by more than | 15° | Only while speed ≥ 5 km/h; GPS heading is noise when stopped |
| Time since last beacon | 30 min | The maximum interval; also the beacon rate when stationary |
Rules on top of the triggers:
../design/reliability.md).Airtime, for context. At SF8, 62.5 kHz, CR 4/5 with MeshCore’s 32-symbol preamble (F§1.4):
| On air | Airtime |
|---|---|
| 21 bytes | 304 ms |
| 37 bytes | 386 ms |
| 53 bytes | 468 ms |
| 101 bytes | 714 ms |
A flood costs that once per repeater that relays it. The network-wide budget is decision 0011, the defaults above are decision 0024, and the mobile 60 s option is decision 0027; ../design/reliability.md has the full table.
APRS-IS positions MUST NOT be put onto the mesh in 1.0 (§3.2). That includes a message sender’s next position: the IS → RF practice of passing it on (F§4.6) is left for a later version (decision 0025).
| Field | Type | Size | Notes |
|---|---|---|---|
| header | 9 / 13 | Source = sender | |
| addressee | base-40 callsign | 6 | §4.1 |
| msgno | u8 | 1 | 1-127. 0 = no ack wanted (an APRS message with no {ID). 128-255 reserved; bit 7 is the rej flag in acks |
| text | ASCII | rest, 1-67 | Printable 0x20-0x7E. MUST NOT contain {, \| or ~ |
Example: VK4ABC → VK4AZ-9, msgno 7, “Meet at Ipswich 1300”.
00: 40 01 2C 00 00 12 9F 5D 8E 00 23 3A DD DD 8E 07
10: 4D 65 65 74 20 61 74 20 49 70 73 77 69 63 68 20
20: 31 33 30 30
:VK4AZ-9 :Meet at Ipswich 1300{7 (33 bytes); the gated line is 60 bytes.| Field | Type | Size | Notes |
|---|---|---|---|
| header | 9 / 13 | Source = the station acknowledging | |
| acked seq | u8 | 1 | The header seq of the acknowledged message frame |
| msgno / kind | u8 | 1 | bits 0-6: the message’s msgno (1-127); bit 7: 0 = ack, 1 = rej |
| to hash | 2 bytes | 2 | The first 2 bytes of SHA-256 of the message sender’s 6-byte encoded callsign (§4.1) |
Without K an ack is 13 bytes: one AES block, 21 bytes on air.
unknown ack), which is harmless: a gateway that never saw the message never gated it.untrusted). An ack only confirms delivery, and requiring K would push every ack to 17 bytes.Example: VK4AZ-9 acks VK4ABC’s msgno 7, sent with seq 0x2C (§6.1): 40 02 11 00 23 3A DD DD 8E 2C 07 75 E7.
:VK4ABC :ack7 (15 bytes); the gated line is 43 bytes.../design/reliability.md.
bad text, §6.5; decision 0022).Mesh → APRS-IS
SRC>TOCALL,qAO,IGATE::ADDRESSEE:text{ID, with the addressee padded to 9 characters and ID = msgno in decimal (7). When msgno is 0 there is no {ID.rejID.:TO :ackID, with TO and ID recovered from the gateway’s record of the message (§6.2).APRS-IS → mesh (messaging gateway only)
addressee not heard);sender on mesh);addressee on IS);TCPXX, NOGATE or RFONLY (else path excluded);qAO MUST NOT be injected (looped);../design/reliability.md) (else airtime).
The first four mirror the IS → RF rules (F§4.6). The fifth stops loops.no path). APRS-IS never causes a mesh flood.|, ~ or {, is not injected (bad text). It is never transliterated, so every gateway makes the same decision. Text over 67 characters is likewise refused (bad text).H = SHA-256(APRS source ‖ ":" ‖ APRS ID), with the source in uppercase text as on APRS-IS:
H[0];H[1] & 0x7F, or 1 if that is 0;H = SHA-256(APRS source ‖ ":" ‖ text), and msgno = 0.ackID with the original 1-5 character ID. A collision between two live messages to the same addressee is possible but rare (1 in 32,768 per pair). The gateway MUST then not inject the second until the first is acked or expired (id clash).{MM}AA is treated as message ID MM plus an ack of AA. UNVERIFIED: whether each half is limited to 2 characters (F§4.5).duplicate).AB12. Every gateway builds the same frame:
60 01 A7 05 34 EB 0D B7 0E 00 00 12 9F 5D 8E 4B followed by the text. Header 60 (version 1, G = 1), seq 0xA7, msgno 75. N = 38, 53 bytes on air.40 02 2E 00 00 12 9F 5D 8E A7 4B 1A F7 (13 bytes), which a gateway renders as :VK2XYZ-7 :ackAB12.ackID to APRS-IS until the mesh addressee’s ack frame arrives (decision 0022).| Field | Type | Size | Notes |
|---|---|---|---|
| header | 9 / 13 | ||
| glen | u8 | 1 | bits 0-2: group name length 0-5 (6-7 invalid: bad field); bits 3-7 reserved |
| id | ASCII | 1 | 0-9 = bulletin, A-Z = announcement (BLNn / BLNx) |
| group | ASCII | glen | Group name, 0-5 characters |
| text | ASCII | rest, 1-67 | As for messages |
Example: VK4ABC, BLN1, “Net tonight 1930 on 147.000”: 40 06 31 00 00 12 9F 5D 8E 00 31 followed by the text.
:BLN1 :Net tonight 1930 on 147.000 (38 bytes); the line is 65 bytes.On the channel
Store-and-forward through a Room Server (Tier A, optional)
A station or gateway MAY also post a bulletin to a MeshCore Room Server, so stations that were away receive it when they next sync. The room server’s limits shape what it can be used for (F§3.4):
BLN1: <text> (§9.7 syntax, without the MPRS <SRC> prefix, since the room records the poster) is the recommended content.Durable storage would need firmware changes (Annex C).
| Field | Type | Size | Notes |
|---|---|---|---|
| header | 9 / 13 | ||
| q | u8 | 1 | bits 0-6 query type; bit 7 = directed (a target follows) |
| target | base-40 callsign | 6 if directed |
| Type | Meaning | APRS analogue | Reply |
|---|---|---|---|
| 0x00 | Stations, send your position | ?APRS? |
Position frame |
| 0x01 | Position of the target | ?APRSP (directed) |
Position frame |
| 0x02 | Gateways, identify | ?IGATE? |
Status frame (code 8) from each gateway: role and counts |
| 0x03 | Send your neighbour report | none | Neighbour report |
| 0x04 | Send your telemetry definitions | none | Telemetry kinds 1-4 |
| 0x05 | Send your status | ?APRSS |
Status frame |
| 0x06-0x7F | Reserved |
The APRS analogues are for orientation only. ?APRS? and ?IGATE? come from the findings (F§4.2, F§4.6); ?APRSP and ?APRSS are UNVERIFIED (not yet in the research notes).
Examples:
40 07 12 00 23 3A DD DD 8E 81 00 00 12 9F 5D 8E. N = 16, 37 bytes on air. Compare :VK4ABC :?APRSP (17 bytes).?IGATE?: 40 07 13 00 23 3A DD DD 8E 02. N = 10 (1 block), 21 bytes on air.Rules
? queries are not gated (F§4.2).?IGATE? itself (F§4.6).A compact 5-analog + 8-bit form is the mandatory form, mapping 1:1 onto APRS T#. Raw CayenneLPP is an optional second kind.
Why the compact form:
igate.js tlmValues).Why LPP stays as an option: it is what MeshCore sensors already produce (F§3.2), but it can only reach APRS through a mapping, and §7.3 requires that mapping to come from the station.
Definitions come from the station. The meaning and scaling of a station’s channels (PARM, UNIT, EQNS, BITS) MUST come from the station: either its definition frames (kinds 1-4) or a profile ID (kind 5). A gateway MUST NOT invent definitions or use its owner’s local mapping for a station’s MeshPRS frames, because two gateways would then publish different definitions for one station. Kelly’s per-owner mapping (igate.js tlm*) applies only to profile-0 telemetry, polled with an encrypted request, which is not a MeshPRS frame.
Profile-0 polled telemetry exception (decision 0006). §13 forbids gating REQ/RESPONSE contents. Kelly’s profile-0 telemetry is exactly that: a response to the gateway’s own encrypted GET_TELEMETRY_DATA or GET_STATUS poll (F§3.2, F§3.3). The one exception is:
After the header, a kind byte selects the body:
| kind | Body | Notes |
|---|---|---|
| 0x00 | values | Below |
| 0x01 | PARM text |
ASCII after PARM.: up to 13 comma-separated names. Per-field length limits as in Kelly igate.js (A_LEN 7,7,6,6,5; B_LEN 6,5,4,4,4,3,3,3, from APRS 1.0.1). UNVERIFIED against APRS12c (not yet in the findings) |
| 0x02 | UNIT text |
ASCII after UNIT. |
| 0x03 | EQNS text |
ASCII after EQNS.: 15 comma-separated coefficients |
| 0x04 | BITS text |
ASCII after BITS.: bbbbbbbb,Title |
| 0x05 | profile ID | u16 LE: a registered telemetry profile that fixes PARM, UNIT, EQNS and BITS (and, for LPP, which reading goes in which slot). 1.0 has no registry (decision 0025), so only kinds 1-4 define channels |
| 0x10 | CayenneLPP | Bytes in MeshCore’s telemetry-reply format, without the 4-byte sender timestamp (F§3.2). Gated only when the station has announced a profile ID that maps it |
Until a profile registry exists, a gateway MUST NOT gate kinds 0x05 and 0x10 (not gated, decision 0017).
| others | reserved | |
Values body (kind 0):
| Field | Type | Size | Notes |
|---|---|---|---|
| mask | u8 | 1 | bits 0-4: A1-A5 present; bit 7: digital byte present; bits 5-6 reserved |
| A1..A5 | u8 each | 1 per bit set in 0-4 | Raw 0-255, in order |
| bits | u8 | 1 if bit 7 | B1 = bit 7 (MSB) … B8 = bit 0 |
Example: VK4ABC-3, A1-A5 = 156, 34, 0, 200, 99, bits 10000000: 40 03 2A 04 9B C4 93 5D 8E 00 9F 9C 22 00 C8 63 80.
T#042,156,034,000,200,099,10000000 (34 bytes); the line is 63 bytes.Definition example (kind 1): PARM text “Battery,Noise,TX air,RSSI,SNR,LowBat”.
:VK4ABC-3 :PARM.Battery,Noise,TX air,RSSI,SNR,LowBat (52 bytes).T# sequence is the header seq as three decimal digits (000-255). Every gateway therefore produces the same line, so APRS-IS’s duplicate filter merges copies (§10.3).:SRC :PARM.…, with the source padded to 9 characters (F§4.5).After the header comes a 16-bit presence mask (u16 LE), then each present field in bit order:
| Bit | Field | Type | Size | Units | APRS field (F§4.5) |
|---|---|---|---|---|---|
| 0 | position | i24 lat, i24 lon | 6 | As §5.1 | Positional weather !…/_ |
| 1 | wind direction | u8 | 1 | × 360/256° | ddd |
| 2 | wind speed | u8 | 1 | × 0.5 m/s | /sss (mph) |
| 3 | gust | u8 | 1 | × 0.5 m/s | gGGG (mph) |
| 4 | temperature | i16 | 2 | × 0.1 °C | tTTT (°F) |
| 5 | humidity | u8 | 1 | % (1-100) | hHH (00 = 100 %) |
| 6 | pressure | u16 | 2 | × 0.1 hPa | bBBBBB (tenths of hPa) |
| 7 | rain, last hour | u16 | 2 | × 0.1 mm | rRRR (hundredths of an inch) |
| 8 | rain, last 24 h | u16 | 2 | × 0.1 mm | pPPP |
| 9 | rain since midnight | u16 | 2 | × 0.1 mm | PPPP |
| 10 | luminosity | u16 | 2 | W/m² | Lnnn / lnnn |
| 11-15 | reserved |
Example: VK4ABC-13 at −27.6150, 152.7600 (illustrative), wind 90° at 3.0 m/s gusting 5.5, 24.5 °C, 65 %, 1013.2 hPa, no rain in the last hour.
00: 40 04 05 B7 1F D7 53 5D 8E FF 00 AF B9 D8 1C A1
10: 6C 40 06 0B F5 00 41 94 27 00 00
!2736.90S/15245.60E_090/007g012t076r000h65b10132 (48 bytes); the line is 78 bytes.Conversion
.../...; any other missing field is left out.no position).
_MMDDHHMM…) would need a receive-time stamp, and its format is UNVERIFIED (not in the findings).bad field./_.| Field | Type | Size | Notes |
|---|---|---|---|
| header | 9 / 13 | Source = the object’s owner | |
| oflags | u8 | 1 | bit 0 live (1) / killed (0); bit 1 item (1) / object (0); bit 2 TS present; bit 3 reserved; bits 4-7 name length 1-9 (0 and 10-15 invalid: bad field; an item of 1-2 characters likewise) |
| name | ASCII | 1-9 | Printable. Objects 1-9 characters, items 3-9 |
| lat, lon | i24, i24 | 6 | As §5.1 |
| symbol | 2 × ASCII | 2 | e.g. /r repeater, /o EOC |
| ts | u32 LE | 4 if TS | The owner’s Unix time when it last set the object, in s |
| comment | ASCII | rest, 0-43 | e.g. frequency and offset for a repeater |
Example: VK4ABC owns the live object “IPSRPT”, symbol /r, comment “147.000MHz -060” (illustrative).
00: 40 05 30 00 00 12 9F 5D 8E 61 49 50 53 52 50 54
10: AF B9 D8 1C A1 6C 2F 72 31 34 37 2E 30 30 30 4D
20: 48 7A 20 2D 30 36 30
;IPSRPT *291030z2736.90S/15245.60Er147.000MHz -060 (52 bytes); the line is 79 bytes.Conversion
DDHHMMz UTC, truncated to the minute. A sender clock is the same for every gateway, so the line comes out identical (§10.3).)NAME!pos (live) or )NAME_pos (killed). APRS12c marks items “not recommended”, so owners SHOULD use objects.Refresh and kill
“Who can hear me” data for coverage maps. It has no APRS equivalent.
| Field | Type | Size | Notes |
|---|---|---|---|
| header | 9 / 13 | ||
| nflags | u8 | 1 | bits 0-4: entry count 0-31; bits 5-6: prefix size − 1 (1-3 bytes; value 3 reserved); bit 7: direction (0 = I heard them, 1 = they heard me) |
| entries | count × (prefix + 2) | Each entry: prefix (key prefix = path hash of the neighbour), SNR (i8, dB × 4, MeshCore’s own encoding), age (u8, minutes since heard; 255 = 255 or more) |
Example: VK4ABC heard four neighbours, with 2-byte prefixes (illustrative):
| Prefix | SNR | Age |
|---|---|---|
| 9B80 | +6.25 dB | 1 min |
| FBAE | −3.5 dB | 4 min |
| A698 | −10.5 dB | 12 min |
| 582A | +2.0 dB | 30 min |
00: 40 08 32 00 00 12 9F 5D 8E 24 9B 80 19 01 FB AE
10: F2 04 A6 98 D6 0C 58 2A 08 1E
Data sources (F§3.3)
Direction 0 (I heard them):
GET_NEIGHBOURS list, which needs a login and contains repeaters only.Direction 1 (they heard me): DISCOVER_RESP, which carries the SNR at which each repeater heard the station’s NODE_DISCOVER_REQ. Discovery is zero-hop and limited to 4 answers per 2 min, so a station MUST NOT send discovery requests just to fill reports faster than that.
Rules
not gated). They MAY publish them elsewhere, such as a map or MQTT. This is final for 1.0 (decision 0025).| Field | Type | Size | Notes |
|---|---|---|---|
| header | 9 / 13 | ||
| code | u8 | 1 | bits 0-3 status code (below); bits 4-6 reserved; bit 7 = position follows |
| lat, lon | i24, i24 | 6 if bit 7 | As §5.1 |
| text | ASCII | rest, 0-62 | Status text (APRS limit 62, F§4.5) |
Status codes
Examples:
40 09 33 00 00 12 9F 5D 8E 08 followed by the text.
>Net control tonight (20 bytes).40 09 34 00 00 12 9F 5D 8E 87 35 EF D8 14 D1 6C followed by “Vehicle rollover”.
Conversion
>text. A position in a code 8 frame is not rendered (decision 0017)./> (the frame carries none; decision 0017), whose comment starts with the code’s name in capitals, for example !2728.08S/15301.41E>EMERGENCY Vehicle rollover (46 bytes). Without a position they become >NAME text.
Plain status retry
Emergency handling
queue full while it holds any lower-priority frame (it drops that frame instead).data_type u16 LE, data_len u8, blob (F§1.4).4D FF on the wire. It is a development value in the unregistered FF00-FFFE range (F§1.6), chosen because it is not 0xFFFF (DATA_TYPE_DEV, which every experimenter uses by default). It is provisional: a registered value in 0100-FEFF replaces it once a working implementation can be shown (decision 0012).MAX_GROUP_DATA_LENGTH, F§1.4).On-air size of a GRP_DATA packet with an N-byte blob (F§1.4):
1 (header) + 1 (path_len) + path + 1 (channel hash) + 2 (MAC) + 16·⌈(N+3)/16⌉
| Blob N | AES blocks | On air from the sender (no path) |
|---|---|---|
| ≤ 13 | 1 | 21 |
| ≤ 29 | 2 | 37 |
| ≤ 45 | 3 | 53 |
| ≤ 61 | 4 | 69 |
Every frame starts with:
| Byte | Field | Notes |
|---|---|---|
| 0 | hdr | bits 7-6 version (0-3); bit 5 G; bit 4 K; bits 3-2 gating; bits 1-0 retry |
| 1 | subtype | Decision 0002 (§9.4) |
| 2 | seq | u8, per-originator sequence number (below) |
| 3-8 | source | base-40 callsign (§4.1) |
| 9-12 | key prefix | Only when K = 1: the first 4 bytes of the sender’s node key (§4.3) |
The header is 9 bytes, or 13 with K. MeshPRS defines no hop count: the MeshCore path length already gives one.
Version. The version is the top 2 bits of byte 0. This layout is version 1, frozen at the 1.0 Candidate. Version 0 was the Draft 0.1 experimental layout: it is byte-identical apart from the version bits, and a receiver MAY accept it as version 1 until 1.0 final. Stations MUST send version 1. Version 2 is spare. Version 3 is reserved as an escape: a version byte follows the header, to be defined by the version that first uses it. Why this choice:
G (gatewayed in from APRS-IS). G is set only by a gateway, on a frame it built from APRS-IS. It is the mesh-side loop marker: a gateway MUST NOT gate a G = 1 frame to APRS-IS (looped). Stations MUST send G = 0. A G = 1 frame MUST be byte-identical whichever gateway builds it: it carries no gateway identity and nothing derived from a gateway’s clock. Its seq and msgno come from the APRS sender and message ID (§6.5), so the mesh’s duplicate table drops a second gateway’s copy.
K (key prefix present). §4.3.
Gating (2 bits):
| Value | Meaning | APRS analogue |
|---|---|---|
| 0 | Normal: may be gated | |
| 1 | Do not gate. Gateways MUST NOT gate the frame anywhere (nogate) |
NOGATE |
| 2 | Test. Gateways MUST NOT pass the frame to live APRS-IS (test); a gateway MAY pass it to a test server (§11) |
|
| 3 | Reserved; receivers treat it as 1 |
The two flags share one 2-bit field instead of two bits because they are mutually exclusive outcomes of one decision (where the frame may go). “Test but may go live” and “do not gate but test” mean nothing, so a field rules out the contradictory combinations, and the fourth value is spare for a later rule.
Retry (2 bits). 0 on the first send of a frame, incremented on each retransmission of that same frame. Stations retry a message at most twice (§6.3) and a plain status once (§8.4), and gateways re-inject at most twice (§6.5), so value 3 is never sent in 1.0; receivers treat it like any other retry.
../design/reliability.md). Without the field, a retry would never get past the nodes that carried the first copy.seq. The sequence space belongs to the originator identity: the node key when K is set, otherwise the source callsign.
T# sequence (§7.3);Why 8 bits:
A gateway keeps, for each originator identity (§9.3), the last accepted seq and when it was accepted. For a new frame, let d = (seq − last) mod 256 (serial-number arithmetic, as in RFC 1982):
| d | Meaning | Action |
|---|---|---|
| 0 | The same frame, or one of its retries | Duplicate (§10.2) |
| 1-127 | Newer | Accept; last = seq |
| 128-255 | Older | If last was accepted less than 10 min ago: drop (replayed). Otherwise accept and resync (a wrap or reboot after a quiet spell) |
Reboot inside 10 min. A node that reboots within 10 minutes and restarts at a random seq looks “older” about half the time. To recover, a gateway that drops two consecutive replayed frames from one originator, where the second is newer than the first (1 ≤ d ≤ 127 between them), MUST resync to the second and accept it. A rebooted node therefore loses at most one frame. Frames with d = 0 whose body differs from the stored frame (ignoring retry) are treated as “older”.
Limits. Senders MUST NOT send more than 127 new frames per originator in any 10 minutes, so “newer” stays unambiguous. This replay check stops casual replays only (§14).
As fixed by decision 0002:
| Value | Service | Layout |
|---|---|---|
| 0x00 | Position | §5.1 |
| 0x01 | Message | §6.1 |
| 0x02 | Ack | §6.2 |
| 0x03 | Telemetry | §7.2 |
| 0x04 | Weather | §8.1 |
| 0x05 | Object / Item | §8.2 |
| 0x06 | Bulletin | §6.6 |
| 0x07 | Query | §6.7 |
| 0x08 | Neighbour report | §8.3 |
| 0x09 | Emergency / Status | §8.4 |
| 0x0A-0xDF | Reserved for future standard use | |
| 0xE0-0xFE | Experimental / private | |
| 0xFF | Reserved |
A receiver MUST drop the frame, counting the named reason, when any of these holds:
| Condition | Reason |
|---|---|
| Data type is not MeshPRS’s | (not counted: not ours) |
| N < 9, or < 13 with K, or shorter than the subtype’s fixed fields | short frame |
| Version not implemented | unknown version |
| Source or another callsign field invalid (§4.1) | bad callsign |
| Subtype 0xFF, or reserved 0x0A-0xDF not implemented | unknown subtype |
| A field outside its defined values (bulletin glen 6-7, object name length 0 or 10-15, neighbour prefix size 4, humidity 0 or > 100) | bad field |
Further rules:
experimental subtype).unknown flag).bad text) rather than transliterate a frame that breaks this. Comments MUST NOT contain | or ~, as in Kelly’s clean.For stock phone apps that can send and show channel text but not GRP_DATA. Frames are GRP_TXT messages on the MeshPRS channel. The app shows them as name: text, where name is the sender’s node name.
Syntax (the text after name: ):
MPRS[.KKKKKKKK] SP SRC SP INFO
MPRS marks a MeshPRS text frame. Text not starting with it is ordinary chat and MUST be ignored by gateways..KKKKKKKK is optional: the first 4 bytes of the sender’s node key in uppercase hex. It is the text form of the K flag.SRC is the source callsign (§4.2).INFO is an APRS info field (F§4.5) of one of these types:
!, =, /, @: positions, uncompressed or compressed;:: message, ack, rej, bulletin, telemetry definitions;; object, ) item, > status, T# telemetry, _ positionless weather.Any other type MUST be dropped (bad text), including ? queries, } third-party, ` and ' Mic-E, and $ NMEA.
name: prefix plus text MUST NOT exceed 160 bytes; the NUL terminator is not counted (BaseChatMesh.cpp:519-531, F§1.4). With a 31-byte node name and a 9-character SRC, that leaves 112 bytes for INFO, or 103 with the key token. That is enough for a 67-character message with a 5-character ID (84 bytes).text profile).
Every gateway MUST process each mesh-side frame in this order. The order is normative, because it decides which reason a frame is counted under, and the test vectors depend on that. It is Kelly’s profile-0 order with the frame steps added. Each drop has a named reason and is counted.
short frame, unknown version, bad callsign, unknown subtype, bad field, experimental subtype, unknown flag, bad text.looped;nogate;test, unless this gateway is connected to a test server;not gated / no position;text profile.untrusted, no key, key mismatch, unknown ack.replayed. Validity, for subtypes with a position: no fix, out of range, 0,0, stale, and replayed (older advert) for adverts.duplicate.interval.queue full.rate.expired).
The numbers are the network-wide figures of decision 0011.
duplicate), so all retries of one frame give at most one APRS-IS line. The check belongs where the APRS-IS line is sent, so a restarted or second front end on the same connection can’t send a frame twice (decision 0028). The 127-frames-per-10-minutes limit (§9.3.1) keeps the key unambiguous.APRS-IS removes duplicates only when source, destination and info match (F§4.6). So that several gateways hearing one flood collapse into one APRS-IS packet, every gateway MUST render a given frame to the same bytes. Hence:
⌊x⌋ is floor division, so that floating-point rounding cannot differ between implementations.The rendering rules (the script check is in Annex A):
| Item | Rule |
|---|---|
| Position format | Always uncompressed DDMM.mmH / DDDMM.mmH. Compressed positions are never emitted, for readability, ambiguity support and profile-0 compatibility |
| Latitude | From the raw i24 v: hm = ⌊(|v|·540000·2 + 2²³) / 2²⁴⌋ hundredths of a minute (half-up). Degrees = ⌊hm/6000⌋ (2 digits), minutes = hm mod 6000 written as MM.mm. The carry at 60.00′ happens by itself (−27.99999° gives 2800.00S). S if v < 0, else N |
| Longitude | The same with |v|·1080000·2, 3-digit degrees, W/E |
| Ambiguity | Then blank the trailing digits as §5.1 |
| Course | c raw byte: ⌊(c·720 + 256) / 512⌋; 0 becomes 360; 3 digits |
| Speed | km/h s to knots: ⌊(s·2000 + 1852) / 3704⌋, 3 digits |
| Altitude | m a to feet: ⌊(|a|·20000 + 3048) / 6096⌋ with the sign of a; written /A= and 6 characters (- plus 5 digits when negative). The i16 maximum, 32767 m, gives 107503 ft, which fits. A negative value below −99999 ft is clamped to -99999 |
| Order after the symbol code | CSE extension, then /A=, then the comment |
| Text | Frames are printable ASCII already (§9.6; frames that aren’t are dropped, never mapped). The gateway trims leading and trailing spaces, truncates so that the whole text after the symbol code (extension, altitude, comment) is at most 43 characters, then trims trailing spaces again. Message text: at most 67 characters; bulletins likewise; status at most 62 characters |
| Wind speed and gust | Raw u (0.5 m/s units) to mph: ⌊(u·3600000 + 1609344) / 3218688⌋, 3 digits |
| Wind direction | As course. UNVERIFIED: whether APRS weather writes north as 360 or 000 |
| Temperature | Raw t (0.1 °C) to °F: ⌊(2·(9t + 1600) + 50) / 100⌋, 3 characters (-05 style when negative), clamped to −99 to 999 |
| Rain | Raw r (0.1 mm) to hundredths of an inch: ⌊(200r + 254) / 508⌋, 3 digits, clamped to 999 |
| Humidity | 100 written 00, otherwise 2 digits |
| Pressure | Tenths of a hPa as is, 5 digits (the u16 maximum, 65535, fits) |
| Luminosity | Below 1000 W/m²: L + 3 digits. 1000-1999: l + 3 digits of (value − 1000). 2000 and over: clamped to l999. UNVERIFIED: the L/l split (the findings name the fields only) |
| Weather field order | _ddd/sss (or .../...), g, t, r, p, P, h, b, L/l; absent fields left out |
| Timestamps | Objects per §8.2: the frame’s TS when plausible, otherwise receive time, truncated to the minute |
| Telemetry | T# + seq (3 digits) + the five analogs (3 digits each, empty when absent) + 8 bits (00000000 when the frame has no digital byte; decision 0017) |
A frame received either side of a minute boundary, when its timestamp comes from receive time, can still produce two lines; this is accepted.
A gateway SHOULD keep a record of every mesh-side frame it receives for at least 24 h, so that delivery, loss and latency can be measured (test plan §5, field-trial criteria P1, P2, P4 and P8). This doesn’t affect interoperability, so it is not a MUST.
sent or the drop reason;The full iGate design is in ../design/igate-design.md. In summary, a MeshPRS gateway:
pass -1, which cannot send (F§4.3);qAO;APZKPG (provisional, §11);user CALL-SS pass NNNNN vers NAME VER filter … on port 14580. Use aunz.aprs2.net, falling back to rotate.aprs2.net (F§4.3).SRC>TOCALL,qAO,IGATECALL:info. qAO is the receive-only construct and explicitly covers translated non-APRS sources (F§4.2).
?IGATE? replies) go as IGATECALL>TOCALL,TCPIP*: without qAO, as Kelly’s statusPacket does.APZKPG, from the unregistered experimental APZ range and currently unallocated (F§4.4).
APM2A? for mesh2aprs.vers string, not in the tocall.qAO is MeshPRS output, and MUST NOT be injected into the mesh (§6.5).g/ with those callsigns. It MUST NOT be a range filter feeding positions to the mesh.Tier A frames travel on one dedicated hashtag channel, so the key is public and derivable by anyone:
#meshprs (decision 0012). #aprs was not chosen because it may already carry human chat.sha256("#meshprs") = 650fa1fd5d2c0bc01449a964a130ea1c.sha256(key) = 0xBA. The Public channel’s is 0x11. The #aprs alternative would be key 910a190e741c01e20043ef8e74db9400, hash 0xE7.0xFF in CMD_SEND_CHANNEL_DATA (F§2.3).path.hash.mode 1 (2-byte hashes), multi.acks 1, flood.advert.interval 83 (F§5.4).Informative where it describes the law; the regulator’s text governs. See F§5. Decision 0003 settles it for 1.0: nothing from the mesh goes to amateur RF.
The rules below are normative (decision 0003). They come from the regulatory research’s recommendations R1-R8 (F§5.5).
NOGATE, RFONLY and TCPXX (R7).roundPos) are available.blocked). A blocklist SHOULD be able to override a binding.MeshPRS 1.0 requires the subtypes position (0x00), message (0x01), ack (0x02), telemetry (0x03), object/item (0x05) and status (0x09); weather (0x04), bulletin (0x06) and neighbour report (0x08) are optional, and query (0x07) must be parsed but need not be answered (decision 0007). The profiles:
| Profile | MUST implement |
|---|---|
| Station | §9 header and validation; sending position (§5) with smart beaconing (§5.5); receiving messages and acks if it sets MSG |
| iGate | §9.5 validation; §4.3 trust; §10 pipeline; §11; conversion of every subtype it gates; never gating 0x07, 0x08 or compact text |
| Messaging gateway | iGate, plus §6.5 APRS-IS → mesh |
Conformance is proven by the shared vectors in test-vectors/, following the kelly-page/bridge/igate_vectors.json pattern. The examples in this specification were checked by script (Annex A). The file names are fixed here so other documents can copy them:
| File | Contents |
|---|---|
frames.json |
Encode and decode: field values ↔ blob hex, for every subtype, including the invalid frames of §9.5 |
gateway.json |
Mesh → APRS-IS: a frame (or advert) plus gateway state → the exact APRS-IS line, or a drop reason |
inject.json |
APRS-IS → mesh: an APRS-IS line plus gateway state → the exact G = 1 blob and its routing, or a drop reason |
text.json |
Compact text profile: GRP_TXT text → parsed fields, or a drop reason |
These are the only drop-reason names. Implementations and vectors MUST use these exact strings.
| Reason | Where | Meaning |
|---|---|---|
short frame |
§9.5 | Blob shorter than the header or the subtype’s fixed fields |
unknown version |
§9.5 | Header version not implemented |
bad callsign |
§4.1, §4.2, §9.5 | Invalid callsign field, or not a valid APRS-IS amateur callsign |
unknown subtype |
§9.5 | Subtype 0xFF, or a reserved subtype not implemented |
bad field |
§9.5 | A field outside its defined values |
experimental subtype |
§9.5 | Subtype 0xE0-0xFE |
unknown flag |
§9.5 | A reserved flag set in a subtype’s flags or mask byte |
bad text |
§6.5, §9.6, §9.7 | Text outside printable ASCII, a forbidden character, over-long text, or a disallowed APRS type |
looped |
§9.3, §6.5 | G = 1 frame offered to APRS-IS, or a MeshPRS-output line offered to the mesh |
nogate |
§9.3 | Gating field 1 or 3 |
test |
§9.3 | Gating field 2 on a gateway with no test server |
not gated |
§6.7, §7.2, §8.3 | Query, neighbour report, or telemetry kind 0x05 / 0x10 |
no position |
§8.1 | Weather without a position |
text profile |
§9.7 | Compact text frame (not gated in 1.0) |
untrusted |
§4.3 | Source callsign not bound |
no key |
§4.3 | No key prefix |
key mismatch |
§4.3 | Key prefix does not match the binding |
unknown ack |
§6.2 | Ack matching no message record |
replayed |
§9.3.1 | Older seq within 10 min |
replayed (older advert) |
profile 0 | Advert timestamp older than the last |
no fix |
§5.3 | Profile 0 validity check |
out of range |
§5.3 | Profile 0 validity check |
0,0 |
§5.3 | Profile 0 validity check |
stale |
§5.3 | Profile 0 validity check |
duplicate |
§10.2, §6.5 | Same (identity, seq) within 10 min, or an extra APRS retry |
interval |
§10.1 | Per-station interval (per station and service) |
queue full |
§10.1 | Queue bound reached |
rate |
§10.1 | Global rate limit |
expired |
§10.1 | Not sent within 15 s of receipt |
addressee not heard |
§6.5 | APRS → mesh: addressee has sent no MeshPRS frame heard here in the last hour |
sender on mesh |
§6.5 | APRS → mesh: the APRS sender was heard on the mesh in the last hour |
addressee on IS |
§6.5 | APRS → mesh: the addressee was heard via APRS-IS in the last hour |
path excluded |
§6.5 | APRS → mesh: TCPXX, NOGATE or RFONLY in the path |
airtime |
§6.5 | APRS → mesh: over the gateway’s airtime budget |
blocked |
§14 | Blocklist |
no path |
§6.5, §12.2 | APRS → mesh: no known direct path (never flooded) |
id clash |
§6.5 | Derived msgno already live for that addressee |
All figures were verified by script (Annex A). On air is from the sender with no path; add 2 bytes per SEQ hop.
The compact text columns use the node name VK4ABC Mobile (13 characters) on every row, with the row’s own SRC. They are informative only, since compact text is not gated in 1.0 (§9.7).
| Subtype (example) | Blob N | On air | APRS info | APRS-IS line | Compact text | Text on air |
|---|---|---|---|---|---|---|
| 0x00 Position, K (required example) | 28 | 37 | 26 | 53 | 53 | 69 |
| 0x00 Position, no K | 24 | 37 | 26 | 53 | ||
| 0x00 Position, K + alt + course/speed | 26 | 37 | 36 | 65 | 65 | 85 |
| 0x01 Message, 20 characters | 36 | 53 | 33 | 60 | 60 | 85 |
| 0x01 Message, 67 characters | 83 | 101 | 80 | 107 | ||
| 0x02 Ack | 13 | 21 | 15 | 43 | 43 | 53 |
| 0x01 Message injected from APRS-IS (G = 1) | 38 | 53 | ||||
| 0x02 Ack of the injected message | 13 | 21 | 18 | 45 | ||
| 0x03 Telemetry values | 17 | 37 | 34 | 63 | 63 | 85 |
| 0x03 Telemetry PARM | 46 | 69 | 52 | 81 | ||
| 0x04 Weather with position | 27 | 37 | 48 | 78 | 78 | 101 |
| 0x05 Object | 39 | 53 | 52 | 79 | 79 | 101 |
| 0x06 Bulletin | 38 | 53 | 38 | 65 | 65 | 85 |
| 0x07 Query, directed | 16 | 37 | 17 | 45 | ||
0x07 Query, ?IGATE? |
10 | 21 | 7 | 35 | ||
| 0x08 Neighbour report, 4 entries | 26 | 37 | ||||
| 0x09 Status | 29 | 37 | 20 | 47 | 47 | 69 |
| 0x09 Emergency with position | 32 | 53 | 46 | 73 |
Text on air is 5 + 16·⌈(5 + L)/16⌉ for L bytes of name: text (4-byte timestamp + 1-byte type + text; the NUL is not counted, F§1.4).
Every question in OPEN-QUESTIONS.md is answered. The records:
| Record | Decides |
|---|---|
| 0001 | Tier A carrier: GRP_DATA on a dedicated channel, GRP_TXT compact text as fallback |
| 0002 | Subtype table |
| 0003 | Regulatory: nothing from the mesh to amateur RF in 1.0 |
| 0004 | K (key prefix) required for gating |
| 0005 | Trust lists: owner’s list plus club-signed list, per gateway |
| 0006 | Profile-0 polled-telemetry exception |
| 0007 | Scope of 1.0 (required and optional subtypes) |
| 0011 | Airtime budget and rate limits |
| 0012 | Trial identifiers: #meshprs, data type 0xFF4D, tocall APZKPG |
| 0013 | Field-trial pass criteria |
| 0014 | Name |
| 0017 | Rendering gaps found while writing the vectors |
| 0018 | Interval per service |
| 0020 | 1.0 Candidate, header version 1, publication, licence and review |
| 0021 | Wire format: big-endian callsigns, 24-bit positions, 8-bit telemetry |
| 0022 | End-to-end acks; G = 1 senders shown as unverified |
| 0023 | Receive time authoritative; compact text MPRS, mesh-only |
| 0024 | Emergency local alert, 120 s minimum beacon interval, bulletin schedule |
| 0025 | Gateway roles and directions; deferrals |
| 0026 | No signed frames in 1.0 |
Records 0008-0010, 0015, 0016 and 0019 are about the reference implementation (Kelly Page), not the standard.
Left for later versions. Each one is an extension: the reserved values they would use stay reserved in 1.0.
Every hex dump, byte count, on-air size, APRS line length, channel key and hash in this specification was produced by one throwaway script, and every frame was decoded back (round trip). The script was run 2026-10-29 with CPython 3.14. The examples are the first entries of test-vectors/frames.json (§15). When the header moved to version 1, every example was checked again against those vectors on 2026-10-30, by both reference codecs (meshprs.js and meshprs.py). The script is not kept in the repository.
The script also checks:
AB1CD = 0x9FDD51;Caveats:
02 F9 52 3D and the neighbour prefixes are illustrative, not real nodes.| Frame field | APRS field | Conversion |
|---|---|---|
| source (header) | TNC2 source | Text as encoded; validated (§4.2) |
| (none) | destination | Tocall APZKPG (§11) |
| (none) | path | qAO,IGATECALL |
| Position lat/lon | DDMM.mmH / DDDMM.mmH |
Integer rule of §10.3 (same output as Kelly fmtLat / fmtLon), with ambiguity blanking |
| Position symbol | Table and code characters | Unchanged |
| pflags MSG | DTI ! or = |
MSG set → = |
| course, speed | ccc/sss |
§10.3: course 0 → 360; km/h → knots |
| alt | /A=nnnnnn |
§10.3: m → feet, 6 characters |
| comment | Comment | §10.3: extension + altitude + comment ≤ 43 |
| Message addressee, msgno, text | :ADDRESSEE:text{ID |
Addressee padded to 9; ID = decimal msgno; no {ID when msgno is 0 |
| Ack to, msgno, kind | :TO :ackID / rejID |
|
| Telemetry values | T#sss,a1,…,a5,bbbbbbbb |
sss = header seq; a = 3 digits; empty slot for a missing analog |
| Telemetry kinds 1-4 | :SRC :PARM. / UNIT. / EQNS. / BITS. |
Text unchanged |
| Weather fields | _ddd/sssgGGGtTTTrRRRpPPPPPPhHHbBBBBB |
Units per §8.1 |
| Object name, live, position, symbol, ts, comment | ;NAME_____*DDHHMMz… / _ |
Name padded to 9; time per §8.2 (TS when within ±1 h of receive time, otherwise receive time), UTC, truncated to the minute |
| Item | )NAME!… / )NAME_… |
|
| Bulletin id, group, text | :BLNn[GROUP]:text |
Name field padded to 9 |
| Status code 8, text | >text |
≤ 62 |
| Status codes 0-7 and 9 | Position comment or status prefixed with the code name | §8.4 |
| Query, neighbour report | Never gated |
Optional and informative. Stock companions already send binary GRP_DATA (Tier A), so Tier B covers only what the app layer cannot do: