MeshPRS

MeshPRS: APRS ↔ MeshCore Interface Standard

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.


1. Introduction

1.1 Purpose

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

1.2 Scope

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.

1.3 Profile 0 (the advert profile)

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.


2. Terminology


3. Architecture, tiers and gateway roles

 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-

3.1 Tiers

3.2 Gateway roles

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


4. Identity and trust

4.1 Callsign encoding

Every callsign in a frame (source, addressee, ack target, query target) is a 6-byte base-40 callsign:

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.

Examples (verified by script): VK4ABC = 00 00 12 9F 5D 8E; VK4AZ-9 = 00 23 3A DD DD 8E.

4.2 Callsign rules

4.3 Binding a callsign to a node key

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:

  1. Key prefix in the frame. A station whose frames are to be gated to APRS-IS MUST set the header’s K flag (decision 0004); other stations SHOULD and include the first 4 bytes of its own node key (§9.3). The first 1-3 of those bytes equal the station’s path hash, so a gateway can cross-check against adverts and paths.
  2. Binding at the gateway. A gateway keeps bindings of callsign-SSID → full node key. Each gateway keeps its own bindings, from its owner’s list and from a club-signed list (decision 0005).
  3. Gate rule. A gateway MUST NOT gate a frame to APRS-IS unless the source callsign is bound to a node key and the frame’s key prefix matches that key (drop reasons 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).
  4. Not authentication. A 4-byte prefix is public: anyone who has heard a station’s advert can copy the prefix along with the callsign. The prefix catches misconfiguration and casual spoofing only. Real proof needs a signature. An Ed25519 signature is 64 bytes, which would roughly triple a position frame. 1.0 defines no signed frame: trust rests on the gateway’s bindings (decision 0026). A later signed frame would take a subtype from the reserved range.
  5. A gateway MAY also require that it has heard an advert from the bound key recently (for example within 24 hours) before gating that station’s frames.

5. Position (subtype 0x00)

5.1 Layout

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)

5.2 Worked example (required)

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”

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.

5.3 Mesh → APRS conversion

5.4 Adverts (profile 0) and frames together

5.5 Smart beaconing

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:

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.

5.6 APRS → mesh

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


6. Messaging

6.1 Message (subtype 0x01)

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

6.2 Ack (subtype 0x02)

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.

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.

6.3 IDs, retries and duplicates

6.4 Length and fragmentation

6.5 Messages across the gateway

Mesh → APRS-IS

APRS-IS → mesh (messaging gateway only)

6.6 Bulletin (subtype 0x06)

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.

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

Durable storage would need firmware changes (Annex C).

6.7 Query (subtype 0x07)

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:

Rules


7. Telemetry (subtype 0x03)

7.1 Choice of form

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:

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:

7.2 Layout

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.

Definition example (kind 1): PARM text “Battery,Noise,TX air,RSSI,SNR,LowBat”.

7.3 Rules


8. Weather, objects, neighbours and status

8.1 Weather (subtype 0x04)

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

Conversion

8.2 Object / Item (subtype 0x05)

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

Conversion

Refresh and kill

8.3 Neighbour report (subtype 0x08)

“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):

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

8.4 Emergency / Status (subtype 0x09)

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:

Conversion

Plain status retry

Emergency handling


9. Frame format, encodings and limits

9.1 Carrier

9.2 Size

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

9.3 Common header

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.

seq. The sequence space belongs to the originator identity: the node key when K is set, otherwise the source callsign.

Why 8 bits:

9.3.1 Sequence wrap, reboot and replay

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

9.4 Subtypes

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  

9.5 Receive validation, versioning and extension

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:

9.6 Text and limits

9.7 Compact text profile (GRP_TXT fallback)

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

10. Gateway behaviour

10.1 Pipeline

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.

  1. Decode and validate (§9.5): short frame, unknown version, bad callsign, unknown subtype, bad field, experimental subtype, unknown flag, bad text.
  2. Loop and gating checks (§9.3):
    • G = 1 gives looped;
    • gating 1 or 3 gives nogate;
    • gating 2 gives test, unless this gateway is connected to a test server;
    • subtypes 0x07 and 0x08, telemetry kinds 0x05 and 0x10 (§7.2), and weather without a position, give not gated / no position;
    • compact text frames give text profile.
  3. Trust (§4.3): untrusted, no key, key mismatch, unknown ack.
  4. Replay (§9.3.1): replayed. Validity, for subtypes with a position: no fix, out of range, 0,0, stale, and replayed (older advert) for adverts.
  5. Duplicate suppression (§10.2): duplicate.
  6. Per-station interval, kept per station and service: the key is (source, subtype), plus the telemetry kind for telemetry and the object name for objects (decision 0018). Kelly: 2 min, down to a 30 s floor when the station has moved more than 500 m. Messages, acks and emergencies are exempt: interval.
  7. Queue and jitter (a bounded queue with a random delay of 0-5 s, so gateways hearing one frame don’t all send at once): queue full.
  8. Global rate limit (Kelly: token bucket, 1 per 5 s, burst 3). A frame that would exceed it waits in the queue, but at most 10 s: rate.
  9. Maximum hold: a frame not sent within 15 s of its receipt MUST be dropped (expired).
    • The figure is jitter (5 s) plus the longest wait (10 s).
    • It keeps every gateway’s line for one frame inside APRS-IS’s 30 s duplicate window (F§4.6), so copies still merge.
    • Emergencies (§8.4) are held to the same limit.

The numbers are the network-wide figures of decision 0011.

10.2 Duplicates

10.3 Deterministic output (normative)

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:

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.

10.4 Diagnostic log

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.

10.5 Summary

The full iGate design is in ../design/igate-design.md. In summary, a MeshPRS gateway:


11. APRS-IS specifics


12. Mesh-side specifics

12.1 The MeshPRS channel

Tier A frames travel on one dedicated hashtag channel, so the key is public and derivable by anyone:

12.2 Routing

12.3 Radio settings and etiquette


13. Regulatory considerations

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


14. Security and privacy considerations


15. Conformance

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

15.1 Test vector files

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

15.2 Drop reasons

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

16. Open questions

16.1 Size summary

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

16.2 Decisions

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.

16.3 Future work

Left for later versions. Each one is an extension: the reserved values they would use stay reserved in 1.0.


Annex A: Verification of the examples

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:

Caveats:

Annex B: Field mapping tables

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  

Annex C: Tier B: future work needing MeshCore firmware changes

Optional and informative. Stock companions already send binary GRP_DATA (Tier A), so Tier B covers only what the app layer cannot do: