Published document · Markdown source · part of the specifications index. Experimental drafts can change section numbers; archived revisions are linked from the specifications index.

Contents
  1. 1. Purpose and design goals
  2. 2. Protocol architecture
  3. 3. PHY terminology and common conventions
  4. 4. Native JG waveform family
  5. 5. Initial acquisition
  6. 6. Universal PHY header
  7. 7. Payload FEC profiles
  8. 8. Payload frame coding pipeline
  9. 9. Receiver acquisition, timing, and frequency recovery
  10. 10. JG25 bearer envelope (wire version 1)
  11. 11. Packet type registry
  12. 12. Addressing
  13. 13. Type-specific fixed fields
  14. 14. Extensions
  15. 15. Payload rules and capacities
  16. 16. Link adaptation (local policy; not on the air)
  17. 17. Channel access
  18. 18. Station actions
  19. 19. Presence and capabilities
  20. 20. End-to-end durable objects
  21. 21. Mail
  22. 22. Files
  23. 23. Priority
  24. 24. AX.25 and KISS coexistence
  25. 24A. Application relay
  26. 25. Items deliberately not frozen
  27. 26. Required real-radio validation before wire freeze
  28. Appendix A — Current acquisition sequences
  29. Appendix B — Wire-format summary, as transmitted
  30. Appendix C — The v0.2 link-layer design (not transmitted)
  31. Appendix D — Golden byte vectors
  32. Appendix E — Why the status detail is an extension (rationale, not normative)

JG.25 Native Digital Link Specification #

Document status: Experimental Draft v0.9 — describes the transmitted form exactly
Date: 2026-10-09
Scope: Amateur-radio digital link and service protocol for the OpenJSQ/JG waveform family: the waveform, the PHY header and body coding, and the bearer envelope every frame carries.
Interoperability status: Not frozen for third-party interoperability, but EXACT for this release: every number, sequence, byte offset and bit position below is what OpenJSQ transmits, checked against the implementation (modem/jg25, transports/jg25_native, engine/src/jg25_service_session.cpp) in the conformance review of 6 September 2026 (docs/SPEC_CONFORMANCE_REVIEW.md). A third party can decode a JG25 transmission from this document alone. The envelope (Sections 10–15) is frozen by golden byte vectors in transports/jg25_native/tests/test_jg25_envelope.cpp; seven of them are reproduced in Appendix D.

Changes in v0.9 (OpenJSQ 1.0.7, 2026-10-09): call-n names. No wire change. Section 12 now says that an AX.25-style name such as w4ijg-7 is an ordinary JG25 address, which a station may use on every bearer (JSQ_PUBLIC_PROTOCOL_SPEC.md Section 4.1); OpenJSQ sends a hyphen in a JG25 address or relay hop only in that canonical -1..-15 form.

Changes in v0.9 (OpenJSQ 1.0.7, 2026-10-09): the software field. One optional extension: 0x05, the software field (the answering station's program and version, 1–32 printable ASCII), on CAPABILITY_RESPONSE only (Sections 14.1 and 19.1). A decoder built before v0.9 skips it by construction (Section 14). Sent only by a host that sets one.

Changes in v0.8 (OpenJSQ 1.0.6, 2026-10-05). The capability record (JSQ_PUBLIC_PROTOCOL_SPEC.md Section 81): two extension types, 0x03 (the six record bytes, on CAPABILITY_RESPONSE and on the PRESENCE that announces a change) and 0x04 (the record's two-byte change counter, on every other PRESENCE), Sections 14.1 and 19.1. A decoder built before v0.8 — a deployed 1.0.5 relay station — skips both by construction (Section 14) and reads every frame exactly as before; the CAPABILITY_RESPONSE flags word and the PRESENCE flags still go out unchanged. Sent only by a station that sends the record. Nothing else on the air changed.

Changes in v0.7. One addition, and it is transmitted: extension type 0x02, LOCAL_ONLY (Sections 14.1 and 24A.1, golden vectors in Appendix D). Length zero — the mark is the type octet itself — registered on CHAT_LIVE, CHAT_VERIFIED_DATA, CHAT_ACK, DELIVERY_RECEIPT, ACTION_REQUEST and ACTION_RESPONSE, and unknown (so skipped) on every other packet type. It is the origin's statement that no relay should repeat this frame; a relay MUST NOT forward one, except a RELAY_DATA whose route names that relay, where the operator asked for the hop by name. The wire version stays 1, the flags octet stays zero, no existing field changes meaning, and a decoder built before v0.7 skips the two octets by construction (Section 14) and reads the frame exactly as it did under v0.6 — which is what makes this an addition rather than a break. Section 14.2 gains the one rule two registered extensions need and one did not: they appear in ascending type order. Capability bit 10 is RESERVED for "understands LOCAL_ONLY" and is NOT set by this revision. Nothing else on the air changed: the waveform, the PHY, the CRCs, the interleavers, the FEC, extension type 0x01 and every packet type are byte-for-byte what v0.6 described.

Why the extension area and not the flags octet. The flags octet of Section 10 is MUST be zero, and the decoder enforces it: frame[3] != 0 is bad_flags and the WHOLE FRAME is refused. A marker carried there would make every marked frame undecodable to every station built before this revision — precisely the failure mode v0.4 avoided for the status detail (Appendix E), and for the same reason. The extension area is where this document has always said additions go, and a TLV nobody understands is skipped by construction.

Changes in v0.6. One addition, and it is transmitted: an ACTION_REQUEST whose action is signal_report MAY be addressed to a GROUP (Sections 11 and 18.2). It is the one action a group may be asked; the codec refuses every other action addressed to a group on encode and on decode (bad_fields), and ACTION_RESPONSE stays addressed to one callsign. A member answers once, after a spreading delay drawn from its own callsign and the request, and nobody acknowledges the answer. The wire version stays 1 and no field changes meaning. A decoder built before v0.6 refuses the group destination as bad_address and drops the frame, so an older member simply does not answer — the same outcome as a member that has switched group signal reports off. A relay never forwards an inner frame addressed to a group (Section 24A). Nothing else on the air changed.

Changes in v0.5. One addition, and it is transmitted: PRESENCE presence_flags bit 4, "serves shared folder" (Section 19, golden vector in Appendix D). It carries the JSQ capability file.shared on the one frame this bearer repeats unbidden — a station that switches its shared folder on or off is re-learned by everyone who hears its next announcement, where before that fact travelled only on a CAPABILITY_RESPONSE that OpenJSQ asks for once per peer per run. The wire version stays 1, the four existing presence bits keep their meanings and their values, the frame does not change length, and the presence cadence is untouched.

This is an addition rather than a break for a reason particular to this field, and the reason is worth stating because it did NOT hold for the change v0.4 made. Nothing validates presence flags. The codec's semantic check (fields_valid(), transports/jg25_native/src/jg25_envelope.cpp) polices the CHAT_ACK and ACTION_RESPONSE status octets, the ACTION_REQUEST action number, the chat fragment counts and the relay hop counts; it has no rule for Jg25PresenceFields at all, so every value of the octet decodes. An unallocated presence bit is therefore genuinely ignorable: a station that has never heard of bit 4 decodes the frame, reads the four bits it knows, and is not asked about the fifth. The ACTION_RESPONSE status octet is the opposite case — fields_valid() rejects a value above error, so a widened status would have made the WHOLE FRAME undecodable to an older station, which is why that change went to an extension TLV instead (Appendix E). The two are not the same shape and are not solved the same way.

Nothing else on the air changed: the waveform, the PHY, the CRCs, the interleavers, the FEC, the ACTION_RESPONSE status registry, extension type 0x01 and every other packet type are byte-for-byte what v0.4 described.

Changes in v0.4. One addition, and it is transmitted: extension type 0x01, the ACTION_RESPONSE status detail (Sections 14 and 18, golden vectors in Appendix D). It is the first use of the extension mechanism Section 14 has always defined. The wire version stays 1, every existing field keeps its meaning and its value, and a decoder that ignores extensions reads every frame exactly as it did under v0.3 — which is what makes this an addition rather than a break. Section 25 item 13 is closed by it. Appendix E, which proposed the change in v0.3, is now the record of why this option was chosen over the other three. Nothing else on the air changed: the waveform, the PHY, the CRCs, the interleavers, the FEC and every other packet type are byte-for-byte what v0.3 described.

Important: The SNR thresholds and adaptation parameters of Section 16 are local receiver/controller policy and are not on the air; they remain provisional. Everything that IS on the air — tone plans, acquisition sequences, header and body coding, CRCs, interleavers, FEC puncturing, the envelope and every packet type — is stated as transmitted. Appendix C preserves the earlier link-layer design of v0.2 for the record and is not transmitted. Appendix E records why the ACTION_RESPONSE status detail took the form it did; the detail ITSELF is transmitted and normative, and is specified in Sections 14 and 18.

1. Purpose and design goals #

JG.25 is a native packet protocol designed for a family of voice-band 4-FSK waveforms rather than as an extension of AX.25. Its goals are:

Normative terms MUST, SHOULD, and MAY have their usual requirements-language meanings within this experimental draft.

2. Protocol architecture #

Application Services
  Chat / Presence / Mail / Files / Actions / Groups
        |
End-to-End Service Envelope
  stable Message ID / origin / final destination / receipts / custody
        |
JG.25 Link Layer
  sessions / next hop / selective ARQ / fragmentation / priority / bursts
        |
JG PHY Family
  JG1200 ... JG2400 / F0 ... F3
        |
FM voice-band radio channel

JG.25 is independent of AX.25. OpenJSQ MAY support AX.25/Bell-202 separately for APRS/KISS interoperability.

3. PHY terminology and common conventions #

3.1 Symbols and bit mapping #

All native JG payload modes use 4-FSK and carry two coded bits per symbol. Symbol-to-tone mapping uses Gray coding:

DibitSymbol indexTone
000lowest tone
011second tone
112third tone
103highest tone

The simulation baseline is continuous-phase 4-FSK (CPFSK): phase remains continuous while the selected instantaneous frequency changes at symbol boundaries. Gaussian shaping has not been adopted.

3.2 Sampling #

Reference PC audio I/O is 48 kHz mono PCM. A receiver MAY resample to a lower internal rate; the simulations commonly used 12 kHz internally. Wire behavior is defined by tone frequencies and symbol timing, not by a required ADC sample rate.

3.3 Byte and bit order #

Unless a field explicitly states otherwise:

4. Native JG waveform family #

The provisional family is:

Mode IDNameBaudRaw bit/sCurrent payload tones (Hz)Intended role
000JG12006001200500 / 1100 / 1700 / 2300extreme robustness
001JG14007001400725 / 1275 / 1825 / 2375weak link
010JG16008001600775 / 1325 / 1875 / 2425weak/normal
011JG18009001800475 / 1125 / 1775 / 2425normal
100JG200010002000525 / 1175 / 1825 / 2475good link
101JG220011002200475 / 1125 / 1775 / 2425strong link
110JG240012002400500 / 1200 / 1900 / 2600very strong link
111reserved———future mode

The names equal the raw bit rate. These tone plans are exactly what OpenJSQ transmits (modem/jg25/include/openfsq/jg25/jg25_profile.hpp, kModes). Mode 111 is reserved and never transmitted. Whether a plan is the best choice for a given radio path is a separate question that real-radio testing continues to answer; the numbers a receiver must implement are these.

Occupied bandwidth. The 99 % occupied bandwidth (47 CFR §97.3(a)(8)) and the audio edges of every mode at every FEC, computed from the rendered waveform, are in docs/OCCUPIED_BANDWIDTH.md and the generated core/include/openfsq/occupied_bandwidth_table.hpp. JG1200, JG1400 and JG1600 occupy 2472–2531 Hz; JG1800, JG2000, JG2200 and JG2400 occupy 2812–3174 Hz and so exceed the 2.8 kHz limit on a data emission below 29.7 MHz (§97.307(f)(3)) at every FEC. The waveform was designed for FM channels.

5. Initial acquisition #

JG uses a nested two-length acquisition structure followed by a common robust PHY header.

5.1 Acquisition tones and rate #

5.2 NORMAL acquisition profile #

Preamble sequence, base-4 symbol digits:

221002203312213231310103230210301122001311233030

Sync sequence:

00223310112132013023

5.3 ROBUST acquisition profile #

ROBUST begins with the exact same 48-symbol prefix and then adds a 48-symbol extension before the same sync word.

96-symbol preamble:

221002203312213231310103230210301122001311233030021031131323120323022321131300203323201201020011

Sync sequence:

00223310112132013023

Because the profiles are nested, the receiver can test for sync after the 48-symbol prefix and continue listening for the robust extension if sync does not appear.

5.4 Profile selection #

A sender SHOULD use NORMAL for ordinary links and ROBUST for weak links, important custody/mail transfers, acquisition recovery, or after repeated acquisition failures. The adaptation controller MAY select ROBUST whenever the negotiated/estimated link is in the lower JG modes or weak SNR region.

6. Universal PHY header #

The header is intentionally slower and more robust than any payload mode.

6.1 Header waveform #

6.2 Header information word #

The 16-bit information word is:

15           13 12      11 10                         0
+--------------+----------+----------------------------+
| PHY mode (3) | FEC (2)  | decoded frame length (11) |
+--------------+----------+----------------------------+

6.3 Header CRC-16 #

As transmitted (modem/jg25/src/jg25_header_decode.hpp, crc16_ccitt):

The resulting 32 bits — information word then CRC — are convolutionally encoded.

6.4 Header convolutional code #

As transmitted (modem/jg25/src/jg25_modem.cpp, conv_encode):

6.5 Block interleaver #

Both interleavers (8 rows for the header, 32 rows for a coded body) are the same block interleaver (block_interleave): with n bits and R rows, cols = ceil(n / R); the input is laid row-major (bit i at row i / cols, column i mod cols) and read out column by column, top row first, skipping positions beyond n. No padding bits are inserted; the output has exactly n bits.

7. Payload FEC profiles #

The FEC field is:

FEC IDNameNominal rateCurrent definition
00F01no body FEC
01F1~3/4K=7 (171,133), puncture pattern 111001 over mother-code output
10F2~2/3K=7 (171,133), puncture pattern 1110
11F31/2unpunctured K=7 mother code

Puncturing (puncture_bits) walks the mother-code output stream of Section 6.4 (A, B, A, B, …) and keeps bit i when pattern[i mod length] is 1: F1's pattern is 111001 (four of every six bits kept), F2's is 1110 (three of every four). F0 sends the information bits unencoded and uninterleaved. The punctured stream of a coded profile passes through the 32-row block interleaver of Section 6.5; the transmitted bit count for n information bits is n (F0), 2·(n+6) (F3), or the pattern's share of 2·(n+6) (F1, F2).

F0 carries no convolutional redundancy but still uses the normal JG.25 CRC-32C.

8. Payload frame coding pipeline #

The current transmitter pipeline is:

JG.25 frame header + extensions + service payload
        ↓
CRC-32C appended
        ↓
(optional future whitening — currently disabled/TBD)
        ↓
F0/F1/F2/F3 coding
        ↓
body interleaver for coded profiles
        ↓
Gray dibit mapping
        ↓
selected JG 4-FSK waveform

The receiver applies the inverse process and MUST accept a reliable frame only after CRC-32C validation.

CRC-32C (modem/jg25/src/jg25_body_decode.hpp, crc32c): the Castagnoli CRC — reflected polynomial 0x82F63B78 (normal form 0x1EDC6F41), initial value 0xFFFFFFFF, reflected input and output, final XOR 0xFFFFFFFF; check value of 123456789 is 0xE3069283. It is computed over the frame body (the envelope of Section 10, every byte) and appended most-significant byte first, so the body carried by the PHY is envelope || CRC-32C.

Length limits. The envelope is at most 384 bytes (kMaximumModemFrameBytes, shared with the other bearers); with its CRC the PHY body is at most 388 bytes, which the 11-bit header length field carries exactly. The header length field counts every body octet INCLUDING the four CRC octets.

Whitening is not transmitted. No scrambler is applied at any stage; the pipeline is exactly the six steps above.

9. Receiver acquisition, timing, and frequency recovery #

A practical receiver SHOULD use the following architecture:

continuous audio
  ↓
preamble correlation / multiple timing hypotheses
  ↓
sync confirmation
  ↓
coarse packet start + common frequency-offset estimate
  ↓
350-baud universal header decode
  ↓
mode / FEC / length known
  ↓
buffer payload frame
  ↓
block/non-data-aided sample-clock search
  ↓
ISI-aware soft 4-FSK likelihoods
  ↓
deinterleave
  ↓
soft FEC decode
  ↓
CRC-32C

Simulation showed that a naive per-symbol timing loop can chase ISI at high baud rates; block clock estimation is the preferred baseline.

10. JG25 bearer envelope (wire version 1) #

Every PHY frame body (before the CRC-32C of Section 8) is exactly one bearer envelope, defined in transports/jg25_native/include/openfsq/transport/jg25/jg25_envelope.hpp and frozen by golden byte vectors. There is no separate link-layer frame: addressing, packet type, identifiers, fragmentation metadata and relay provenance are all fields of this envelope, and the JSQ service state machines above it own every transaction (JSQ_TRANSPORT_SERVICE_SPEC.md). All multi-byte integers are big-endian.

Offset  Size  Field
0       1     magic, 0x4A ('J')
1       1     wire version, 1
2       1     packet type (Section 11)
3       1     flags — reserved, MUST be 0 in version 1; a nonzero
              value makes the frame undecodable (bad_flags)
4       1     header_length: offset of the service payload from byte 0
5       1     source_length, then that many bytes of source callsign
…       1     destination_length, then that many bytes of destination
              (a callsign, a group, or length 0 = allcall / unaddressed)
…       n     type-specific fixed fields (Section 13)
…       …     extensions, each { u8 type, u8 length, length bytes },
              filling exactly the space up to header_length (Section 14)
              — type 0x01 on an ACTION_RESPONSE is the status detail
header_length … service payload, to the end of the envelope

Rules a decoder MUST apply before acting on anything: header_length is at least 5 and at most the frame length; each address fits within the header area and is at most 31 bytes; the fixed fields fit; the extensions end exactly at header_length (a short or overrunning extension is bad_extension; a mismatch is bad_header_length); an unknown version is unsupported_version; an unknown type is unknown_type (reported with the type byte, never reinterpreted); a payload declared UTF-8 by Section 15 must validate. An encoder refuses (never truncates) any frame that would exceed 384 bytes or whose header_length would exceed 255.

11. Packet type registry #

Values are wire; append, never renumber.

TypeNameFixed fields (Section 13)Destination rulePayload
0x01CAPABILITY_REQUESTnonea callsign, or noneempty
0x02CAPABILITY_RESPONSEu16 capability_flags, u8 maximum_relay_hopsa callsignempty
0x03PRESENCEu8 presence_flagsnone (never addressed)UTF-8 status text, may be empty
0x04HEARD_REQUESTu8 maximum_entries (0 = responder's default)a callsignempty
0x05HEARD_RESPONSEnonea callsignheard entries (Section 19)
0x06ACTION_REQUESTu8 action, u32 action_ida callsign, or a group when action is signal_report (Section 18.2)UTF-8 argument, may be empty
0x07ACTION_RESPONSEu32 action_id, u8 statusa callsignresponse bytes (UTF-8 for the textual actions); MAY carry the status-detail extension 0x01 (Section 18.1)
0x10CHAT_LIVEu32 message_id, u8 fragment_index, u8 fragment_counta callsign, a group, or nonetext fragment (UTF-8 when fragment_count is 1)
0x11CHAT_VERIFIED_DATAu32 message_id, u8 fragment_index, u8 fragment_count, u16 total_bytes, u32 whole_message_crc32ca callsignfragment bytes
0x12CHAT_ACKu32 message_id, u8 status, u8 cumulative_fragmentsa callsignselective bitmap (may be empty)
0x20RELAY_DATAu8 hop_count, u8 hop_limit, u8 previous_relay_length + callsigna callsign (the next hop)one complete inner envelope
0x21DELIVERY_RECEIPTnonea callsigncommon JSQ receipt bytes
0x30FILE_ENVELOPEnonea callsignJG25 file message (Section 22)
0x31MAIL_ENVELOPEnonea callsign, or nonecommon JSQ mail bytes (Section 21)

Which types a relay forwards is in Section 24.

12. Addressing #

Addresses are text, not packed. A callsign is 1–31 bytes of lowercase letters, digits, - and /, containing at least one letter or digit and not beginning with - or / (valid_jg25_callsign). A group is @ or # followed by 1–30 letters (either case), digits, -, _ or . (valid_jg25_group_name). The source is always a callsign. The destination rule per type is in Section 11; length 0 means allcall or "not addressed". OpenJSQ writes callsigns in lowercase.

There is no SSID octet, no base-37 packing and no broadcast sentinel; the v0.2 compact address format is in Appendix C and is not transmitted.

An AX.25-style name such as w4ijg-7 is therefore an ordinary JG25 address: the -7 is text, carried and compared like any other byte of the callsign. Since OpenJSQ 1.0.7 a station may use that name on JG25 (and FSQ) as well as AX1200, so it is one station on every waveform. No wire change was needed.

13. Type-specific fixed fields #

Laid out immediately after the destination, in the order given, with the sizes shown; a decoder reads exactly these bytes before the extensions.

14. Extensions #

Each extension is { u8 type, u8 length, length bytes }; they occupy exactly the space between the fixed fields and header_length. A decoder MUST skip unknown types and MUST fail the frame on a truncated one (bad_extension). Skipping is by construction: the decoder walks the area by length, keeps every TLV it finds, and interprets only the types it knows.

14.1. Extension type registry #

Values are wire; append, never renumber. A type is registered FOR A PACKET TYPE: the same number on a different packet type is unknown there, and is skipped like any other unknown type.

TypePacket typeNameLengthSection
0x01ACTION_RESPONSE (0x07)status detail118.1
0x02CHAT_LIVE (0x10), CHAT_VERIFIED_DATA (0x11), CHAT_ACK (0x12), DELIVERY_RECEIPT (0x21), ACTION_REQUEST (0x06), ACTION_RESPONSE (0x07)LOCAL_ONLY024A.1
0x03CAPABILITY_RESPONSE (0x02), PRESENCE (0x03)capability record619.1 (v0.8)
0x04PRESENCE (0x03)capability record change counter219.1 (v0.8)
0x05CAPABILITY_RESPONSE (0x02)software (program and version)1–3219.1 (v0.9)

CHANGED IN v0.8: 0x03 and 0x04 registered. CHANGED IN v0.9 (OpenJSQ 1.0.7): 0x05 registered. Its length is not fixed: 1 to 32 octets of printable ASCII (0x20–0x7E), no space at either end. A value outside that rule is ignored (no field), not bad_extension.

Types 0x06–0xFF are unassigned. A decoder MUST skip any it receives, and this revision emits none of them.

LOCAL_ONLY is registered for the six packet types above and for no other. It is a PRESENCE OR ABSENCE and carries no value, so its length is zero: the mark is the type octet itself. On any packet type not in that list — PRESENCE, RELAY_DATA, a capability exchange, a file or mail envelope — type 0x02 is unknown, and a decoder MUST skip it exactly as Section 14 requires of any unknown type rather than reading a meaning into it. The six are the types an origin composes for one correspondent; the others are either already unrepeatable by Section 24A or are the relay machinery itself.

14.2. Rules for a registered type #

15. Payload rules and capacities #

PRESENCE, ACTION_REQUEST and ACTION_RESPONSE payloads, and a single-fragment CHAT_LIVE payload, MUST be valid UTF-8 (RFC 3629: no overlong forms, no surrogates, nothing above U+10FFFF); a multi-fragment live message is validated once reassembled. Other payloads are binary.

An extension is part of the header and so comes out of the payload: an ACTION_RESPONSE carrying the status detail of Section 18.1 has three fewer payload bytes than the same answer without one, and a sender that sizes a payload before deciding the detail MUST allow for them rather than discover the overflow at encode time.

The service payload one frame can carry is 384 − header_length bytes (jg25_payload_capacity); for RELAY_DATA the capacity is reduced by a further 31 bytes so any previous-relay callsign fits. For example, a CHAT_LIVE from w4ijg to k4abc has a 22-byte header and carries up to 362 payload bytes.

Nothing in this section is transmitted: a frame announces only the mode and FEC it was sent with (Section 6.2). The controller jointly selects:

PHY rate × FEC × frame payload size × burst airtime

The controller MUST optimize expected delivered goodput, not merely choose the highest nominal PHY rate.

16.1 Provisional PHY/FEC ladder #

These thresholds are simulation calibration values, not protocol constants:

Enter at/above estimated SNRState
below −7.4 dBJG1200-F3
−7.4 dBJG1200-F2
−6.5 dBJG1400-F2
−5.5 dBJG1600-F2
−4.5 dBJG1800-F2
−3.5 dBJG2000-F2
−2.5 dBJG2000-F1
−1.8 dBJG2200-F2
−1.2 dBJG2200-F1
−0.4 dBJG2400-F1
+4.4 dBJG2000-F0
+6.2 dBJG2200-F0
+6.9 dBJG2400-F0

The apparent transition from JG2400-F1 to JG2000-F0 is intentional: uncoded JG2000 can have greater delivered information rate than 3/4-coded JG2400 on a clean link.

16.2 Hysteresis #

Initial implementation guidance:

These rules are local implementation policy and do not alter the on-air frame format.

16.3 Frame payload size #

Provisional adaptation:

Predicted FERPayload target
>16%64 B
5.5–16%128 B
≤5.5%256 B
very clean F0, <0.8%512 B

256 B is the current default candidate for normal reliable DATA.

17. Channel access #

JG.25 MUST respect carrier activity and SHOULD avoid transmitting into a busy channel. Collision/random-backoff details have not yet been frozen. Repeated collisions SHOULD cause shorter bursts and randomized delay before retry.

18. Station actions #

ACTION_REQUEST carries one of these action values (JSQ semantics; the mapping to any other bearer happens in that bearer's adapter):

ValueActionArgument (UTF-8 payload)
0signal_reportempty
1qthempty
2qtcempty
3softwareempty
4timeempty
5heard_listempty
6wakeempty
7alertthe alert text
8mailbox_readthe entry name
9mailbox_listempty
10library_listempty (JSQ_PUBLIC_PROTOCOL_SPEC.md §5.5F)
11library_fetch<index> <fingerprint-hex8> (§5.5F)
12link_fasterempty
13link_slowerempty
14relay_requestsee the relay service
15repeatempty
16repeat_delayedrequested delay in whole seconds as decimal text, e.g. 30; the responder clamps it
17mailbox_writethe entry NAME up to the first LF, then the note text

Values 15–17 were appended after the registry was frozen; a decoder built before them refuses them as bad_fields. An unregistered value does not encode.

ACTION_RESPONSE status is one of: 0 ok, 1 not_found, 2 disabled, 3 refused, 4 unsupported, 5 busy, 6 error. These seven values are the whole registry and a frame whose status exceeds error is bad_fields; the finer outcome, where there is one, travels beside the status as the extension of Section 18.1. The response payload for the textual actions is the reply text; OpenJSQ's signal_report reply reads snr <±n.n> dB or snr <±n.n> dB, offset <±n.n> Hz, or no calibrated snr, offset <±n.n> Hz / heard, no calibrated measurement when it has no reading (engine/src/station_action_service.cpp); its heard_list reply is one line per station, <callsign>, <HH:MM>, <n> db (, no snr when unmeasured, , via <relay> when relayed), or none.

OpenJSQ asks a heard list with HEARD_REQUEST, which a relay does not carry, and answers a heard_list ACTION_REQUEST that arrives direct unsupported. Through a relay it is the other way round: a station with a route sends heard_list wrapped for the relay, and the station asked answers ok with the HEARD_RESPONSE entry encoding of Section 19 as the payload (not text), back through the relay, or with a refusal status when it does not answer heard lists. A signal_report that came through a relay is answered with the answerer's reading of the relay's frame: the last hop, which the asker names as such.

18.1. ACTION_RESPONSE status detail (extension 0x01) #

The seven-value status octet is coarser than the outcomes the services above it produce. The shared folder (JSQ_PUBLIC_PROTOCOL_SPEC.md §5.5F) tells a rate limit from a spent quota from a station that is merely occupied, and a stale listing reference from a file it could not read; status alone folds the first three onto busy and the last two onto error. A requester that cannot tell them apart retries a station that asked to be asked less, and throws away a good cached listing whenever the far side has a disk problem.

Extension 0x01 carries the finer outcome. It is ONE octet:

u8  0x01        extension type
u8  0x01        length, always 1
u8  detail      a value from the table below

The values are the JSQ station-action status numbers, unchanged, so that this bearer, the OFQ2 bearer and the service above them share one vocabulary:

DetailNameMUST ride statusMeaning
5invalid3 refusedThe request was malformed or unusable as sent.
6rate_limited5 busyAsked again inside the responder's per-station interval.
7quota_exceeded5 busyThe responder's budget for the period is spent.
8storage_failure6 errorThe responder could not read what it was asked for.
9too_large6 errorThe object will not fit what was offered.
11denied3 refusedRefused by the responder's access policy.
13stale_reference6 errorlibrary_fetch only: the index was in range and the fingerprint did not match — the file changed since the listing.
14awaiting_approval5 busylibrary_fetch only (OpenJSQ 1.0.7): the responder's operator has been asked to approve the download, and the file follows if they say yes. Not worth asking again; a station that does not know the value reads busy.

Values 0 ok, 2 not_found, 3 disabled, 4 unsupported, 10 busy and 12 internal_failure in the same vocabulary are deliberately NOT registered as details: a bare status already says exactly those — a status of busy with no detail means the responder is occupied, and one of error with no detail means an internal failure — so a detail restating one would be two fields saying one thing. Value 1 accepted is not registered either; see the end of this section. Every other value is unassigned.

denied (11) IS registered even though a bare refused also reads as denied, because refused folds TWO outcomes and only one of them can have the silent reading. Registering invalid alone would leave a bare refused ambiguous between "this station means denied" and "this station sent a detail I could not use", which is exactly the ambiguity this extension exists to remove.

Consistency, and which field wins. A station MUST set status to the value the detail's row names, so the two fields never disagree, and an encoder MUST refuse to build a frame in which they do. A station MUST NOT send the extension on a packet type other than ACTION_RESPONSE.

A decoder MUST use the detail only when it is registered above AND rides the status that row names. Otherwise the coarse status octet wins: the detail is ignored and the frame is read exactly as a frame carrying no detail at all. This is not an error and the frame is NOT discarded. The rule is asymmetric on purpose:

The two structural faults of Section 14.2 apply here and are NOT ignorable: a type-0x01 extension whose length is not 1, and a frame carrying two of them, are both bad_extension.

"No detail was sent" and "a detail was sent that I could not use" MUST produce the same answer, so that a requester's behaviour depends on what it can read and not on what it happened to receive. An implementation MAY still report the ignored TLV to a monitor, and OpenJSQ does.

Not registered: accepted. A granted library_list answers accepted at the service and ok on this wire. It stays folded: the fact that matters — the listing is coming, as a file — is carried by ok unharmed.

18.2. A signal report asked of a group #

An ACTION_REQUEST whose action is signal_report (0) MAY carry a group (Section 12) as its destination. It is the only action a group may be asked, and the codec enforces that on encode and on decode: any other action with a group destination is bad_fields. An unaddressed ACTION_REQUEST remains bad_address, and OpenJSQ refuses to ask allcall anything.

A station that decodes a group signal report:

The asking station accepts an answer to that action_id from any station, one per station, and stops collecting two minutes after the request left the air; silence from every member is reported as no answer. OpenJSQ asks a given group at most once every five minutes. A group request is never sent through a relay, and a relay never forwards an inner frame addressed to a group (Section 24A).

19. Presence and capabilities #

19.1. The capability record (v0.8) #

NEW IN v0.8. The rest of Section 19 is unchanged.

A station that sends the capability record (JSQ_PUBLIC_PROTOCOL_SPEC.md Section 81: version, service flags, the waveform worked on beside the relay hop field, change counter, six bytes) carries it on this bearer as follows:

A record that does not decode (JSQ spec Section 81.1) is ignored; the frame is otherwise read as usual. A CAPABILITY_RESPONSE without 0x03 (an older station) is read from its flags word as a record without a counter.

PRESENCE (0x03) is sent unaddressed with presence_flags and an optional status text. It carries the presence policy bits only, not the capability word:

BitNameMeaning
0mailbox acceptingthe mailbox is accepting deposits right now
1relay willingthe station will carry for others right now
2accepts filesINBOUND: a file offered TO this station is accepted right now
3accepts custodycustodial mail is accepted right now
4serves shared folderOUTBOUND: this station SERVES its shared folder right now — it answers library_list and fetches by index and fingerprint (Section 18). The JSQ capability file.shared. Added in v0.5; allocated permanently.

Bits 5–7 are unallocated and MUST be transmitted as zero in wire version

Bits 2 and 4 are opposite directions and MUST NOT be conflated. Bit 2 is what this station will RECEIVE; bit 4 is what it will SERVE. A station that accepts incoming file offers but shares no folder sets bit 2 alone and MUST NOT be shown as sharing files; one that serves a folder but declines incoming offers sets bit 4 alone.

Why bit 4 exists on PRESENCE and not only on CAPABILITY_RESPONSE. The file.shared fact is also carried by capability bit 9 below, and would seem to need no second carriage. But CAPABILITY_RESPONSE is an ANSWER — it is sent only when asked, and OpenJSQ asks once per peer per run (see below) — so a station that switches its shared folder on or off after that single exchange has no way to tell anyone. PRESENCE is unaddressed and repeats on its own schedule, which makes it the only carriage a CHANGE of this capability has on this bearer. The two are not redundant and differ in one further way: bit 9 is decided PER RECIPIENT against the sender's access policy, and bit 4, having no recipient to decide about, means only that the sender admits somebody — sharing is enabled, a folder is set, and the audience is not an empty allow list. A receiver that has both MUST prefer the addressed answer, which is the more specific claim.

PRESENCE flags are not validated, and this is normative. The codec's semantic check refuses a CHAT_ACK or ACTION_RESPONSE status outside its registry, an ACTION_REQUEST action number outside its registry, an inconsistent chat fragment count and a relay hop count above its own limit; it applies NO rule to presence_flags. Every value of the octet decodes. A station built against v0.4 or earlier therefore reads a v0.5 PRESENCE exactly as it read a v0.4 one: the frame decodes, the four bits it knows read as they always did, and bit 4 reaches a receiver that never asks about it. In the other direction a v0.5 station hearing a v0.4 PRESENCE sees bit 4 clear, which reads as "does not serve", and learns file.shared from that station the way it always did — a CAPABILITY_RESPONSE. Neither direction loses a frame, and neither misreads one.

CAPABILITY_REQUEST (0x01) asks; CAPABILITY_RESPONSE (0x02) answers with the capability_flags word (u16) as allocated by the OpenJSQ implementation, permanent once merged:

BitNameMeaning
0verified chatVerified (ARQ) chat is bound.
1station actionsThe station-action service is bound.
2filesThe reliable file service is bound.
3mail custodyCustodial mail is bound.
4delivery receiptsJSQ delivery receipts are generated.
5application relayThe station will carry for others (follows consent, live).
6heard listsHeard requests are answered.
7mailboxThe mailbox actions are served.
8shared libraryThe station UNDERSTANDS library_list / library_fetch; says nothing about whether anything is offered.
9shared files availableVOLATILE, per recipient: shared files are currently available to the station this response is addressed to, under the sender's access policy. Decided as the response is built; withdrawn by a later response without it. The JSQ capability file.shared maps to this bit, never to bit 8. Presence bit 4 carries the same capability unaddressed; see Section 19's presence table for how the two differ.
10understands LOCAL_ONLYRESERVED, and NOT SET by this revision. When allocated it will mean that the sender reads extension 0x02 (Sections 14.1 and 24A.1) and will not repeat a frame carrying it. Nothing depends on the advertisement: a station that has never heard of the extension skips it and behaves exactly as it did before, so the mark is safe to transmit to anybody. The bit exists so that a future sender may one day AVOID marking traffic to a peer that cannot read the mark. Bits 11–15 are unallocated and MUST be transmitted as zero.

OpenJSQ sends a CAPABILITY_REQUEST automatically once per peer per run, after the first directed message it sends that peer, once the channel has been quiet 1.5 s; it answers a request no sooner than 600 ms after receiving it and only after 300 ms of quiet (engine/src/jg25_service_session.cpp). These delays are local policy.

Once per peer per run is what makes presence bit 4 necessary rather than redundant, and it is deliberately unchanged by v0.5: a probe is a frame on the air that somebody has to originate, and bit 4 removes the reason to originate more of them. The capability word carries fifteen other facts a peer may need — verified chat, mailbox, custody, relay hops — and PRESENCE carries none of those, so the probe still answers questions presence cannot. A receiver that wants file.shared current now gets it from the announcements a station was already making.

Heard lists. HEARD_REQUEST names how many entries the asker wants (0 = the responder's default of 16; never more than 32). HEARD_RESPONSE's payload is a sequence of entries, each:

u8   relayed      1 when the latest sighting came through a relay, else 0
u8   length, then that many bytes of callsign
u16  minutes_ago  whole minutes since last heard; 0xFFFF = longer than
                  the field can say, or unknown
i8   snr_db       calibrated dB of the newest DIRECT hearing, rounded;
                  +127 = no measurement (never a substitute zero); a
                  relayed sighting always reports 127

Entries are sorted most recently heard first; the asker itself is omitted. A response that would not fit one frame is shortened until it does.

20. End-to-end durable objects #

Durable objects (mail, files, receipts) keep their identity in the common JSQ codecs, not in the envelope: the origin, final destination and stable message identity live inside the bytes those codecs define (JSQ_PUBLIC_PROTOCOL_SPEC.md Parts III, V and XVI), and a relay carries the inner frame unchanged (Section 24), so no relay can replace them.

21. Mail #

MAIL_ENVELOPE (0x31) carries one common JSQ Mail Custody v1 message exactly as encode_jsq_mail produces it — the byte layout of JSQ_PUBLIC_PROTOCOL_SPEC.md Part V, INCLUDING that codec's own trailing CRC-32C — as the payload, with the JG25 envelope adding only source and destination. The mail state machine (announce, query, enumerate, fetch, deliver, ack, deposit, deposit_ack, delivered_notice) and its custody semantics are those of Part V and JSQ_TRANSPORT_SERVICE_SPEC.md §15. DELIVERY_RECEIPT (0x21) likewise carries one common receipt as encode_jsq_receipt produces it (Part XVI, with its trailing CRC-32C).

22. Files #

FILE_ENVELOPE (0x30) carries one JSQ File ARQ v1 message serialised by transports/jg25_native/src/jsq_file_adapter.cpp: the same field layout as JSQ_PUBLIC_PROTOCOL_SPEC.md Section 33 — kind byte first (0 offer … 8 abort_reply), then the fields of Sections 22–30, big-endian — but WITHOUT the trailing common CRC-32C, because the PHY frame's own CRC-32C already covers it. A DATA message therefore costs 17 bytes before its block: kind (1), session_id (4), object_id (4), block_index (4), block crc32c (4). Block size is chosen so a block fits one frame's payload capacity (Section 15), at most 512 bytes. Whole-file integrity is the manifest's SHA-256, as on every bearer.

23. Priority #

The base header provides a one-bit priority indication. Services MAY also carry a richer PRIORITY extension. Channel scheduling MUST avoid starvation of normal traffic; exact priority queues are TBD.

24. AX.25 and KISS coexistence #

JG.25 is not AX.25 and does not fit through an ordinary radio's internal AX.25 KISS TNC unless that device implements JG natively.

OpenJSQ MAY expose separate backends:

A native modem API should expose at least mode, FEC, frame bytes, receive confidence/SNR, frequency offset, and decode status.

24A. Application relay #

RELAY_DATA (0x20) wraps one complete inner envelope. The origin transmits it to the first relay with hop_count 0, hop_limit its own relay-hop setting and an empty previous_relay. A relay that receives one and is willing forwards it only when the inner type is CHAT_LIVE, DELIVERY_RECEIPT, ACTION_REQUEST or ACTION_RESPONSE (every other type, including a nested RELAY_DATA, is dropped), when the inner destination is a callsign (an inner frame addressed to a group, or unaddressed, is dropped), when hop_count is below the smaller of hop_limit and its own limit, and when it has not forwarded the same payload before. It sends a new RELAY_DATA whose source is itself, whose destination is the INNER frame's destination, with hop_count + 1, the same hop_limit, previous_relay = the station it heard the frame from, and the inner bytes unchanged, after a random hold-off of 150–650 ms. So the origin, final destination and message identity survive every hop intact.

24A.1. LOCAL_ONLY — traffic the origin asked no relay to carry #

Extension type 0x02 (Section 14.1), length 0, is the origin's statement that this frame is for whoever hears it directly and for nobody further. A relay MUST NOT forward a frame carrying it. The frame is otherwise ordinary in every respect: it is addressed normally, it is decoded normally, and the station it names answers it normally. Only the repeating is forbidden.

The one exception is a RELAY_DATA (0x20) WHOSE ROUTE NAMES THE RELAY. LOCAL_ONLY is not registered on RELAY_DATA, so the mark that matters there is the one on the INNER envelope, and a relay that finds it MUST still forward the RELAY_DATA addressed to it. The reason is that the two statements are not in conflict: LOCAL_ONLY says "no station should repeat this on its own initiative", and a RELAY_DATA addressed to a named relay is the origin asking that relay, by name, to carry it. The operator's explicit route beats the standing preference, because the operator set both.

The inner bytes are forwarded unchanged (Section 24A), so LOCAL_ONLY survives the hop. That is deliberate: the frame the relay emits is again marked, so a SECOND relay overhearing it repeats nothing, and the destination still sees the mark the origin set.

A station that has never heard of type 0x02 skips it (Section 14) and repeats the frame as it always did. LOCAL_ONLY is therefore a request honoured by relays that understand it and not a guarantee about the channel; nothing about privacy or confidentiality follows from it, and §97.113(a)(4) is untouched — the frame is plain text either way. Capability bit 10 (Section 19) is RESERVED for "understands LOCAL_ONLY" and is not advertised by this revision; nothing depends on the advertisement, because an older station's behaviour is safe by construction.

25. Items deliberately not frozen #

The following MUST remain experimental until further testing:

26. Required real-radio validation before wire freeze #

The next validation stage SHOULD use at least the available FT-8800/DigiRig and VR-N76 path and measure:

Only after those tests should numeric tone plans and controller thresholds be declared stable.

Appendix A — Current acquisition sequences #

NORMAL 48-symbol preamble #

221002203312213231310103230210301122001311233030

ROBUST 96-symbol preamble #

221002203312213231310103230210301122001311233030021031131323120323022321131300203323201201020011

Shared 20-symbol sync #

00223310112132013023

Appendix B — Wire-format summary, as transmitted #

Acquisition (both profiles at 600 baud 4-FSK, tones 500/1100/1700/2300):
  NORMAL: 48-symbol preamble, then 20-symbol sync
  ROBUST: 96-symbol preamble (the NORMAL 48 plus 48), then the same sync

Universal PHY header (350 baud 4-FSK, tones 950/1350/1750/2150):
  16 info bits = mode(3) | FEC(2) | body_length(11, octets incl. CRC-32C)
  + CRC-16/CCITT-FALSE (0x1021, init 0xFFFF)  -> 32 bits
  K=7 (171,133) rate 1/2, 6 zero tail bits    -> 76 coded bits
  8-row block interleaver, Gray dibits       -> 38 symbols

Body (the selected mode's tones and baud):
  envelope (Section 10) || CRC-32C (Castagnoli, over the envelope)
  F0: bits as they are
  F1/F2/F3: K=7 (171,133) + 6 tail bits, punctured 111001 / 1110 / none,
            32-row block interleaver
  Gray dibits; an odd final bit is paired with 0
  envelope <= 384 bytes, so body <= 388 bytes

Envelope (all big-endian):
  4A 01 <type> 00 <header_length>
  <source_length> <source> <destination_length> <destination>
  <type-specific fields> <extensions> <payload>
  extension = <type> <length> <length bytes>; the only type version 1
  defines is 01 on an ACTION_RESPONSE, one octet, the status detail

Carrier phase continuous throughout; symbol timing restarts at each
segment boundary; no whitening; no inter-frame markers; one envelope per
PHY frame.

Draft v0.2 of this document specified a link layer that the implementation never adopted: a 5-octet base header (control octet with version/class/E/A/P bits, 16-bit Context ID, 16-bit Sequence), compact TLV extensions with a 4-bit type and 4-bit length, a 5-octet base-37 compact station address with an SSID nibble, OPEN/DATA/ACK/CONTROL/ CUSTODY frame classes, a 32-frame selective-repeat window with compact ACK frames and 32-bit bitmaps, mode-rate inter-frame resync markers and multi-frame bursts under one PTT assertion, and durable-object, custody-operation and fragment layouts marked TBD. None of it is on the air. The envelope of Sections 10–15 replaced all of it: addressing is text, reliability is the JSQ service ARQ (CHAT_VERIFIED_DATA / CHAT_ACK, and the file service's own selective repeat), and every frame stands alone with full acquisition. The design text is preserved below for the record; a future revision that adopts any of it must do so under a new wire version.

10.1 Fixed base header #

Every JG.25 link frame begins with a 5-octet base header:

Octet 0   Control
Octets 1–2 Context ID
Octets 3–4 Sequence

10.2 Control octet #

7      6 5          3 2 1 0
+--------+------------+-+-+-+
| ver(2) | class (3)  |E|A|P|
+--------+------------+-+-+-+

10.3 Frame classes #

classNamePurpose
000BEACONpresence/discovery
001OPENestablish link/session context
010DATAreliable session data
011DATAGRAMconnectionless message
100ACKselective acknowledgement
101CONTROLlink control/management
110CUSTODYcustody/store-and-forward control
111EXTENDEDfuture/extended class

10.4 Context ID #

The 16-bit Context ID identifies an established adjacent-link session. It is local to the peer/session and is not globally unique. Context ID zero SHOULD be reserved for connectionless/control usage.

OPEN negotiation MUST prevent a stale pre-restart context from being mistaken for a current one. The exact session nonce/epoch field is still TBD.

10.5 Sequence #

The 16-bit Sequence field is the link-frame sequence number within a reliable context. Modular arithmetic MUST be used. The initial selective-repeat receive window is 32 frames.

C.11. Extensions #

If E=1, the base header is followed by one octet containing the total extension-area length, then compact TLVs.

TLV header:

7          4 3          0
+------------+------------+
| type (4)   | length (4) |
+------------+------------+

length is the value length in octets, 0–15. A future escape type MAY support longer values.

Current provisional type registry:

TypeNameNotes
0x0SOURCEcompact station address
0x1DESTINATIONcompact station address
0x2SERVICEapplication service ID
0x3SACKselective ACK data when not using compact ACK body
0x4FRAGMENTobject/block fragmentation
0x5MESSAGE_IDstable end-to-end ID
0x6PRIORITYservice/object priority
0x7CAPABILITIESstation/modem/service capabilities
0x8ROUTErelay/routing information
0x9TIMESTAMPoptional timestamp
0xASESSION_NONCEproposed restart/stale-context protection
0xBBURST_INFOproposed burst metadata
0xC–0xEreservedfuture standard use
0xFESCAPEextended/private format

Types 0xA and 0xB are architectural placeholders and are not yet wire-frozen.

C.12. Compact station addressing #

Normal amateur station addresses use five octets with no hash collisions for six-character normalized callsigns.

12.1 Normal format #

Normalize the base callsign to six characters using the alphabet:

space A B ... Z 0 1 ... 9

Values are:

Encode six base-37 digits into a 32-bit unsigned integer:

value = d0·37^5 + d1·37^4 + ... + d5

Append one format/SSID octet:

high nibble = address format
low nibble  = SSID 0–15

Initial format 0x0 is the normal six-character base-37 address. Format 0xF is reserved to indicate an extended address representation carried by an extension. All five octets 0xFF are reserved for broadcast.

C.13. Session establishment #

An OPEN frame establishes a Context ID and carries, directly or by extension:

Once the context is established, normal DATA frames SHOULD omit source/destination/service information already known from the context. This is a major JG.25 efficiency mechanism.

C.14. Selective-repeat ARQ #

Reliable sessions use selective repeat.

14.1 Compact ACK frame #

The current efficient ACK model is:

5-byte JG.25 base header
4-byte receive bitmap
4-byte CRC-32C

The ACK frame's Sequence field is the SACK base sequence; bitmap bit i refers to base + i modulo 65536.

14.2 Fast turnaround ACK #

After a known burst transaction, the receiver SHOULD use a short reverse synchronization marker and compact ACK rather than paying full initial acquisition/header overhead again. The current simulation baseline used a short mode-rate reverse sync; the exact final ACK marker sequence is TBD.

C.15. Multi-frame bursts #

JG.25 SHOULD support multiple independent frames under one PTT assertion.

full acquisition + PHY header
frame N
short resync
frame N+1
short resync
frame N+2
...
PTT off
fast SACK

Every frame retains its own sequence, FEC result, and CRC-32C. Loss of one frame MUST NOT invalidate later frames in the same burst.

15.1 Provisional inter-frame resync lengths #

The marker uses the same tones and baud rate as the current payload mode.

ModeMarker symbolsAirtime
JG12003253.3 ms
JG14003245.7 ms
JG16002025.0 ms
JG18001617.8 ms
JG20001616.0 ms
JG22001210.9 ms
JG24001210.0 ms

Current optimized marker candidates include:

12 symbols: 232110313020
16 symbols: 3123013120310202

The 20- and 32-symbol sequences MUST be frozen after real-radio sequence testing.

15.2 Burst airtime policy #

Burst length is an airtime budget, not a fixed frame count.

Initial controller targets:

Channel stateTarget
quiet~8 s
normal shared channel~6 s
busy/collision-prone~3 s
absolute initial cap10 s

At 128-byte payloads and a ~6 s cap, simulations produced approximately 4 frames/burst at JG1200–1600, 6 at JG1800–2000, and 8 at JG2200–2400.

Appendix D — Golden byte vectors #

From transports/jg25_native/tests/test_jg25_envelope.cpp, which freezes one vector per packet type, two more for the status detail of Section 18.1, one more for the presence shared-folder bit of Section 19, and three more for LOCAL_ONLY (Section 24A.1) — the marked and unmarked forms of one CHAT_LIVE, and the ACTION_RESPONSE that pins the order of two registered extensions; the envelope bytes only (the PHY CRC-32C follows on the air).

CAPABILITY_REQUEST from w4ijg, unaddressed:

4a 01 01 00 0c  05 77 34 69 6a 67  00
│  │  │  │  │   │  w  4  i  j  g   └ destination_length 0
│  │  │  │  │   └ source_length 5
│  │  │  │  └ header_length 12
│  │  │  └ flags
│  │  └ type 0x01
│  └ version 1
└ magic 'J'

CAPABILITY_RESPONSE from w4ijg to k4abc, flags = verified chat | files | mail custody | delivery receipts | heard lists = 0x005D, maximum_relay_hops 3:

4a 01 02 00 14  05 77 34 69 6a 67  05 6b 34 61 62 63  00 5d 03

PRESENCE from w4ijg, flags = mailbox accepting | relay willing = 0x03, status "monitoring 2m":

4a 01 03 00 0d  05 77 34 69 6a 67  00  03  6d 6f 6e 69 74 6f 72 69 6e 67 20 32 6d

The same PRESENCE from the same station once its shared folder is being served (v0.5): flags = mailbox accepting | relay willing | serves shared folder = 0x13. ONE OCTET DIFFERS and nothing else moves — header_length is 0x0d in both, because presence_flags is a fixed field and a new bit in it costs no length. That is what makes bit 4 an addition rather than a break, and it is the whole difference between this change and the v0.4 one, which had to buy its space in the extension area:

4a 01 03 00 0d  05 77 34 69 6a 67  00  13  6d 6f 6e 69 74 6f 72 69 6e 67 20 32 6d
│  │  │  │  │   │                  │   │   └ status text "monitoring 2m"
│  │  │  │  │   │                  │   └ presence_flags 0x13
│  │  │  │  │   │                  │     = mailbox accepting (bit 0)
│  │  │  │  │   │                  │     | relay willing (bit 1)
│  │  │  │  │   │                  │     | serves shared folder (bit 4)
│  │  │  │  │   │                  └ destination_length 0 — never addressed
│  │  │  │  │   └ source 'w4ijg', length 5
│  │  │  │  └ header_length 13, unchanged from the vector above
│  │  │  └ flags 0, reserved
│  │  └ type 0x03 PRESENCE
│  └ version 1
└ magic 'J'

HEARD_RESPONSE from k4abc to w4ijg with two entries — n0call heard 12 minutes ago at −7 dB directly, and w1aw heard through a relay with no age and no reading:

4a 01 05 00 11  05 6b 34 61 62 63  05 77 34 69 6a 67
00 06 6e 30 63 61 6c 6c  00 0c  f9
01 04 77 31 61 77        ff ff  7f

ACTION_RESPONSE from k4abc to w4ijg, action_id 0x01020304, status busy with the status detail rate_limited (Section 18.1), no payload. The extension is the last three octets of the header and header_length counts them: 0x19 = 25, three more than the same answer without a detail:

4a 01 07 00 19  05 6b 34 61 62 63  05 77 34 69 6a 67  01 02 03 04  05  01 01 06
│  │  │  │  │   │                  │                  │            │   │  │  └ detail 6 rate_limited
│  │  │  │  │   │                  │                  │            │   │  └ length 1
│  │  │  │  │   │                  │                  │            │   └ extension type 0x01
│  │  │  │  │   │                  │                  │            └ status 5 busy
│  │  │  │  │   │                  │                  └ action_id
│  │  │  │  │   │                  └ destination w4ijg
│  │  │  │  │   └ source k4abc
│  │  │  │  └ header_length 25
│  │  │  └ flags
│  │  └ type 0x07
│  └ version 1
└ magic 'J'

ACTION_RESPONSE from k4abc to w4ijg, action_id 0x01020304, status error with the status detail stale_reference, payload changed:

4a 01 07 00 19  05 6b 34 61 62 63  05 77 34 69 6a 67  01 02 03 04  06  01 01 0d
63 68 61 6e 67 65 64

A station that does not implement Section 18.1 reads both of these as ACTION_RESPONSEs with status busy and error, skips the three unknown-to-it octets, and finds the payload where header_length says it is — which is the whole point of putting the detail here.

CHAT_LIVE from w4ijg to k4abc, message_id 0x01020304, fragment 0 of 1, payload hello, WITHOUT the local-only mark (v0.6 and v0.7 alike). header_length is 0x17 = 23:

4a 01 10 00 17  05 77 34 69 6a 67  05 6b 34 61 62 63  01 02 03 04 00 01
68 65 6c 6c 6f

The same CHAT_LIVE carrying LOCAL_ONLY (v0.7). TWO OCTETS ARE ADDED and nothing else moves; header_length counts them, 0x19 = 25:

4a 01 10 00 19  05 77 34 69 6a 67  05 6b 34 61 62 63  01 02 03 04 00 01  02 00
│  │  │  │  │   │                  │                  │           │  │   │  └ length 0
│  │  │  │  │   │                  │                  │           │  │   └ extension type 0x02 LOCAL_ONLY
│  │  │  │  │   │                  │                  │           │  └ fragment_count 1
│  │  │  │  │   │                  │                  │           └ fragment_index 0
│  │  │  │  │   │                  │                  └ message_id 0x01020304
│  │  │  │  │   │                  └ destination k4abc
│  │  │  │  │   └ source w4ijg
│  │  │  │  └ header_length 25, two more than the unmarked vector above
│  │  │  └ flags 0 — reserved, and STILL zero; the mark is not here
│  │  └ type 0x10 CHAT_LIVE
│  └ version 1
└ magic 'J'
68 65 6c 6c 6f   payload "hello", unchanged and in the same place

A station that does not implement Section 24A.1 walks the extension area by length, finds one TLV it does not know, skips it, and reads hello where header_length says it is. It then repeats the frame if it is a relay — which is why the mark is a request and not a guarantee.

ACTION_RESPONSE carrying BOTH registered extensions, which is what pins their order (Section 14.2): from k4abc to w4ijg, action_id 0x01020304, status busy with detail rate_limited, and LOCAL_ONLY, no payload. header_length is 0x1b = 27 — the 25 of the status-detail vector above plus two:

4a 01 07 00 1b  05 6b 34 61 62 63  05 77 34 69 6a 67  01 02 03 04  05  01 01 06  02 00
                                                                       └ 0x01 ──┘  └ 0x02

0x01 precedes 0x02: ascending type order, always, whatever order the sender filled them in.

Appendix E — Why the status detail is an extension (rationale, not normative) #

The status detail IS transmitted. It is specified in Sections 14 and 18.1 and frozen by the golden vectors of Appendix D; nothing in this appendix adds to or qualifies that. What is recorded here is the problem it solves and the three options that were rejected, so that the choice can be understood later rather than rediscovered — and so that a reader who wonders why the seven-value status octet was not simply widened has the answer in the document rather than in a commit message.

E.1. The problem #

ACTION_RESPONSE carries status as one octet with seven registered values: 0 ok, 1 not_found, 2 disabled, 3 refused, 4 unsupported, 5 busy, 6 error. The shared-folder service behind library_list and library_fetch (JSQ_PUBLIC_PROTOCOL_SPEC.md §5.5F) produces a richer set of outcomes than that, and before v0.4 several of them collapsed:

Service outcomev0.3 status sentWhat the requester saw
busy (a concurrency slot in use)busybusy
rate_limited (asking too often)busybusy
quota_exceeded (the day's budget is spent)busybusy
too_large (the file will not fit the offer)errorinternal_failure
stale_reference (the fingerprint no longer matches)errorinternal_failure
storage_failure (the file could not be read)errorinternal_failure
internal_failureerrorinternal_failure
denied / invalidrefuseddenied

AX1200 (OFQ2) and FSQ both keep these apart, so this was a JG25-only narrowing, and the two harms it caused were concrete:

Both are closed by Section 18.1. Section 25 item 13 records what remains, which is local policy: an answer that carries no detail is still read the old way, because on that frame the two genuinely cannot be told apart.

E.2. Rejected — append to the existing status registry #

Register values 7..13 on the same octet:

7  rate_limited      10 stale_reference
8  quota_exceeded    11 storage_failure
9  too_large         12 denied      13 invalid

Why not. It is the smallest change to make and the largest to deploy. fields_valid() rejects an ACTION_RESPONSE whose status exceeds error as bad_fields, so a station built before the widening discards the whole frame. An answer carrying a new status would not read as an unknown status to such a station; it would read as no answer at all, and the requester would wait out its full ten-minute window — strictly worse than the folding it replaced. Making it safe would have required a wire version bump, a capability bit, and a rule that a station MUST NOT send a status above error to a peer that has not advertised it: three mechanisms to carry one octet of information.

The counter-argument was available and was considered: JG25 has never been released, there are no stations in the field, and nothing would have discarded anything. It was still rejected. A format's rules are worth more than one deployment's convenience — the reading a decoder gives an unknown value is a property of the format, and one that reads "unknown value" as "no frame" is a bad property whether or not anyone is currently on the air to be hurt by it.

E.3. Chosen — a status-detail extension (TLV) #

Section 14 already defined the extension mechanism and reserved no types: {u8 type, u8 length, length bytes}, with a decoder skipping unknown types by construction. Extension 0x01, one octet, carries the finer outcome; the status octet keeps its current value, so a station that ignores the extension area reads the frame exactly as it did before, and a later revision may register more outcomes on the same type without another change here.

Its cost, which Section 18.1 pays explicitly. Two to three header octets on the answers that carry it, and — the real price — two fields that say related things, which obliges the document to say which one wins. It does: the coarse status octet wins, the decoder implements that rather than leaving it to its caller, and a disagreement costs the detail rather than the frame.

What it does NOT need, and why. No capability bit, no negotiated opt-in, no "MUST NOT send to a peer that has not advertised support". The mechanism is backward compatible by construction, so there is nothing to negotiate; the extension is part of version 1 rather than something bolted on beside it.

E.4. Rejected — a reason token in the response payload #

Make the first line of a non-ok payload a lowercase reason token (rate_limited, stale_reference, …), with human text after an LF.

Why not. It puts a machine-readable field inside a human-readable one, which every protocol that has done it has come to regret, and it spends payload bytes on a bearer where a frame carries 384 minus the header. Its one genuine advantage — an old station renders the token to its operator as a word rather than losing it — is worth less than a field with a length, a registry and a decoder that can refuse it.

E.5. Rejected — do nothing on the wire #

Keep seven values, keep the requester-side heuristics, document the folding.

Why not. Zero spec churn and zero deployment risk, and it was a defensible answer while the extension mechanism had never been exercised. Against it: harm 2 above is permanent under it, the heuristics are guesses that can decline a retry that would have worked, and the extension area sat unused in every frame this bearer has ever sent. The mechanism was put in the envelope for exactly this, and a mechanism that is never used the first time it is needed does not get used the second time either.