JG.25 Native Digital Link Specification #
Document status: Experimental Draft v0.6 — FM and HF transmitted characteristics Date: 2026-09-14 Scope: Two physical variants of the JG25 family: FM voice-radio operation and HF SSB operation, together with the shared JG25 service envelope. The HF material describes the on-air technical characteristics; it does not publish source code or receiver algorithms.
Document organization:
- Part I — FM: existing FM specification, retaining its section numbers and shared envelope definitions.
- Part II — HF: current HF waveform, ASCII/JHAT format, framing, error protection and service mapping.
Interoperability status: Experimental; not frozen for third-party interoperability. The FM wire description was checked against the implementation in the September 6 conformance review. The HF description was checked against the current implementation on September 14. Shared envelope golden byte examples remain in Part I, Appendix D. HF uses that envelope but has its own physical framing and character-count length field.
Changes in v0.6. Adds the implemented HF variant as Part II, explicitly scopes the earlier physical specification to FM, and replaces the FM receiver-design recommendation with an implementation-neutral description. Existing FM section numbers and envelope values are retained. This is a documentation change; no waveform or wire version changes. The public-disclosure purpose and remaining RF operating obligations are described in HF.8; this document does not claim FCC approval.
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 FM air interface 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 FM air interface 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 on the FM air interface described by Part I — 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.
Part I — FM #
This part describes the FM physical format. Its envelope/service sections are also used by HF as explicitly referenced in Part II. FM mode identifiers, acquisition rates, body lengths and local adaptation policies must not be applied to HF unless Part II says so. Historical appendices remain identified as such; they are not additional transmitted formats.
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:
- 1.High delivered goodput through ordinary amateur FM voice radios.
- 2.Graceful operation from weak links through strong local links.
- 3.Fast interactive chat plus efficient files, mail, presence, actions, and relaying.
- 4.Selective-repeat reliability rather than stop-and-wait or go-back-N.
- 5.Independent adjacent-link reliability and end-to-end message semantics.
- 6.Safe custodial store-and-forward semantics for emergency communications.
- 7.Efficient multi-frame bursts with independent recovery of every frame.
- 8.A waveform and decoder practical on PCs, sound-card/DigiRig interfaces, and embedded TNC-class hardware such as a future custom Mobilinkd implementation.
- 9.Automatic adaptation based on delivered goodput rather than nominal raw bit rate.
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:
| Dibit | Symbol index | Tone |
|---|---|---|
00 | 0 | lowest tone |
01 | 1 | second tone |
11 | 2 | third tone |
10 | 3 | highest 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:
- Multi-byte integers are transmitted most-significant byte first.
- Within an octet, bit 7 is transmitted before bit 0 before FEC/interleaving.
- Symbol digits
0–3in sequence definitions refer to the Gray-mapped tone indices above. - A coded bit stream is mapped to symbols two bits at a time, first bit first; when the stream has an odd number of bits, the final lone bit is paired with a
0(modem/jg25/src/jg25_modem.cpp,bits_to_symbols). - Carrier phase is continuous across every segment boundary (preamble → sync → header → body); symbol timing restarts at each segment's own baud rate at each boundary (
append_cpfsk).
4. Native JG waveform family #
The provisional family is:
| Mode ID | Name | Baud | Raw bit/s | Current FM payload tones (Hz) | Intended role |
|---|---|---|---|---|---|
000 | JG1200 | 600 | 1200 | 500 / 1100 / 1700 / 2300 | extreme robustness |
001 | JG1400 | 700 | 1400 | 725 / 1275 / 1825 / 2375 | weak link |
010 | JG1600 | 800 | 1600 | 775 / 1325 / 1875 / 2425 | weak/normal |
011 | JG1800 | 900 | 1800 | 475 / 1125 / 1775 / 2425 | normal |
100 | JG2000 | 1000 | 2000 | 525 / 1175 / 1825 / 2475 | good link |
101 | JG2200 | 1100 | 2200 | 475 / 1125 / 1775 / 2425 | strong link |
110 | JG2400 | 1200 | 2400 | 500 / 1200 / 1900 / 2600 | very strong link |
111 | reserved | — | — | — | 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.
5. Initial acquisition #
JG uses a nested two-length acquisition structure followed by a common robust PHY header.
5.1 Acquisition tones and rate #
- Modulation: 4-FSK CPFSK
- Baud: 600
- Tones: 500 / 1100 / 1700 / 2300 Hz
5.2 NORMAL acquisition profile #
- Preamble: 48 symbols
- Sync: 20 symbols
- Preamble airtime: 80.0 ms
- Sync airtime: 33.33 ms
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 #
- Modulation: 4-FSK CPFSK
- Baud: 350
- Tones: 950 / 1350 / 1750 / 2150 Hz
- Uncoded information: 16 bits
- Header CRC: 16 bits
- FEC: K=7 rate-1/2 convolutional code
- Tail: six zero termination bits
- Encoded length: 76 coded bits
- Header interleaver: 8-row block bit interleaver (Section 6.5)
- 4-FSK symbols: 38
- Airtime: 108.57 ms
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) |
+--------------+----------+----------------------------+
- PHY mode: values in Section 4.
- FEC: values in Section 7.
- frame length: 0–2047 octets. It is the number of decoded JG.25 frame-body octets following the PHY header, including the JG.25 CRC-32C.
6.3 Header CRC-16 #
As transmitted (modem/jg25/src/jg25_header_decode.hpp, crc16_ccitt):
- CRC-16/CCITT-FALSE: polynomial
0x1021, initial value0xFFFF, no input or output reflection, no final XOR (the check value of the ASCII string123456789is0x29B1). - Computed over the two information octets (Section 6.2, most-significant octet first) and appended most-significant byte first.
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):
- Constraint length K=7, generators octal
(171, 133)=G0 = 0x79,G1 = 0x5B, mother rate 1/2. - The generator-171 output precedes generator-133 for each input bit. In delay notation, 171 uses the current input and delays 1, 2, 3, 6; 133 uses the current input and delays 2, 3, 5, 6. Each coded block starts in the all-zero state.
- Terminated by six zero input bits, so the stream holds 2·(n + 6) bits for n information bits: 76 for the header's 32.
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 ID | Name | Nominal rate | Current definition |
|---|---|---|---|
00 | F0 | 1 | no body FEC |
01 | F1 | ~3/4 | K=7 (171,133), puncture pattern 111001 over mother-code output |
10 | F2 | ~2/3 | K=7 (171,133), puncture pattern 1110 |
11 | F3 | 1/2 | unpunctured 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 #
Receiver architecture, acquisition, timing recovery, frequency estimation and error-correction implementation are local design choices. They do not add fields or change the transmitted format. A receiver must recover the stated frame structure and validate the header/body checksums before accepting the frame. Detailed receiver algorithms are outside this specification's scope.
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.
| Type | Name | Fixed fields (Section 13) | Destination rule | Payload |
|---|---|---|---|---|
0x01 | CAPABILITY_REQUEST | none | a callsign, or none | empty |
0x02 | CAPABILITY_RESPONSE | u16 capability_flags, u8 maximum_relay_hops | a callsign | empty |
0x03 | PRESENCE | u8 presence_flags | none (never addressed) | UTF-8 status text, may be empty |
0x04 | HEARD_REQUEST | u8 maximum_entries (0 = responder's default) | a callsign | empty |
0x05 | HEARD_RESPONSE | none | a callsign | heard entries (Section 19) |
0x06 | ACTION_REQUEST | u8 action, u32 action_id | a callsign | UTF-8 argument, may be empty |
0x07 | ACTION_RESPONSE | u32 action_id, u8 status | a callsign | response bytes (UTF-8 for the textual actions); MAY carry the status-detail extension 0x01 (Section 18.1) |
0x10 | CHAT_LIVE | u32 message_id, u8 fragment_index, u8 fragment_count | a callsign, a group, or none | text fragment (UTF-8 when fragment_count is 1) |
0x11 | CHAT_VERIFIED_DATA | u32 message_id, u8 fragment_index, u8 fragment_count, u16 total_bytes, u32 whole_message_crc32c | a callsign | fragment bytes |
0x12 | CHAT_ACK | u32 message_id, u8 status, u8 cumulative_fragments | a callsign | selective bitmap (may be empty) |
0x20 | RELAY_DATA | u8 hop_count, u8 hop_limit, u8 previous_relay_length + callsign | a callsign (the next hop) | one complete inner envelope |
0x21 | DELIVERY_RECEIPT | none | a callsign | common JSQ receipt bytes |
0x30 | FILE_ENVELOPE | none | a callsign | JG25 file message (Section 22) |
0x31 | MAIL_ENVELOPE | none | a callsign, or none | common 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.
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.
- CAPABILITY_RESPONSE:
capability_flagsu16 (Section 19),maximum_relay_hopsu8 (the hop limit this station will honour when relaying). - PRESENCE:
presence_flagsu8 (Section 19). - HEARD_REQUEST:
maximum_entriesu8; 0 asks for the responder's own bounded default. - ACTION_REQUEST:
actionu8 (Section 18),action_idu32 chosen by the requester and echoed in the response. - ACTION_RESPONSE:
action_idu32,statusu8 (Section 18); the finer outcome, when there is one, follows as extension0x01(Section 18.1) and never contradictsstatus. - CHAT_LIVE:
message_idu32,fragment_indexu8,fragment_countu8 (≥ 1, index < count). OpenJSQ sends at most 3 fragments per live message (kJg25MaximumLiveFragments). - CHAT_VERIFIED_DATA: as CHAT_LIVE plus
total_bytesu16 andwhole_message_crc32cu32 (the CRC-32C of Section 8 over the whole message), identical on every fragment; fragments are uniform except the last, and sizes derive fromtotal_bytesandfragment_count. - CHAT_ACK:
message_idu32,statusu8 — 0 progress, 1 complete, 2 cancelled, 3 refused — andcumulative_fragmentsu8, the count received contiguously from the start; the payload is a selective bitmap of fragments beyond that (bit 0 of byte 0 = fragmentcumulative_fragments), empty when the cumulative count alone tells the story. - RELAY_DATA:
hop_countu8,hop_limitu8 (hop_count ≤ hop_limit), thenprevious_relay_lengthu8 and that many bytes: the station this relay heard the inner frame from, or length 0 when it heard the origin directly. The inner frame — a complete envelope of Section 10 with the origin as its source and the final destination as its destination — is the payload. A relay never rewrites the inner frame.
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.
| Type | Packet type | Name | Length | Section |
|---|---|---|---|---|
0x01 | ACTION_RESPONSE (0x07) | status detail | 1 | 18.1 |
Types 0x02–0xFF are unassigned. A decoder MUST skip any it receives, and this revision emits none of them.
14.2. Rules for a registered type #
- The length is exactly as registered. A registered type carrying any other length is
bad_extension— a differently sized extension is not the extension this document defines, and a later revision needing more room registers another type. - A packet MUST NOT carry the same registered type twice; a frame that does is
bad_extension, because it cannot be trusted to have meant either copy. - Where a registered extension refines a fixed field, the ENCODER writes it and it appears FIRST among the extensions. This makes a frame's extension order a property of the format rather than of the sender, which is what lets Appendix D freeze one.
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.
16. Link adaptation (local policy; not on the air) #
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 SNR | State |
|---|---|
| below −7.4 dB | JG1200-F3 |
| −7.4 dB | JG1200-F2 |
| −6.5 dB | JG1400-F2 |
| −5.5 dB | JG1600-F2 |
| −4.5 dB | JG1800-F2 |
| −3.5 dB | JG2000-F2 |
| −2.5 dB | JG2000-F1 |
| −1.8 dB | JG2200-F2 |
| −1.2 dB | JG2200-F1 |
| −0.4 dB | JG2400-F1 |
| +4.4 dB | JG2000-F0 |
| +6.2 dB | JG2200-F0 |
| +6.9 dB | JG2400-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:
- EWMA SNR: approximately 50% prior estimate / 50% newest measurement.
- Apply about 0.2 dB normal conservatism.
- If the previous burst delivered <55% of frames, apply about 0.55 dB additional pessimism.
- If <25% delivered, apply about 1.1 dB additional pessimism.
- Downshift immediately when evidence becomes bad.
- Upgrade only after two consecutive recommendations, at least ~0.25 dB beyond the next threshold, and recent frame success ≥70%.
These rules are local implementation policy and do not alter the on-air frame format.
16.3 Frame payload size #
Provisional adaptation:
| Predicted FER | Payload 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):
| Value | Action | Argument (UTF-8 payload) |
|---|---|---|
| 0 | signal_report | empty |
| 1 | qth | empty |
| 2 | qtc | empty |
| 3 | software | empty |
| 4 | time | empty |
| 5 | heard_list | empty |
| 6 | wake | empty |
| 7 | alert | the alert text |
| 8 | mailbox_read | the entry name |
| 9 | mailbox_list | empty |
| 10 | library_list | empty (JSQ_PUBLIC_PROTOCOL_SPEC.md §5.5F) |
| 11 | library_fetch | <index> <fingerprint-hex8> (§5.5F) |
| 12 | link_faster | empty |
| 13 | link_slower | empty |
| 14 | relay_request | see the relay service |
| 15 | repeat | empty |
| 16 | repeat_delayed | requested delay in whole seconds as decimal text, e.g. 30; the responder clamps it |
| 17 | mailbox_write | the 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.
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:
| Detail | Name | MUST ride status | Meaning |
|---|---|---|---|
| 5 | invalid | 3 refused | The request was malformed or unusable as sent. |
| 6 | rate_limited | 5 busy | Asked again inside the responder's per-station interval. |
| 7 | quota_exceeded | 5 busy | The responder's budget for the period is spent. |
| 8 | storage_failure | 6 error | The responder could not read what it was asked for. |
| 9 | too_large | 6 error | The object will not fit what was offered. |
| 11 | denied | 3 refused | Refused by the responder's access policy. |
| 13 | stale_reference | 6 error | library_fetch only: the index was in range and the fingerprint did not match — the file changed since the listing. |
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:
statusis the field EVERY station reads, including one that skips the extension area entirely. If a detail could override it, two stations decoding the same frame would act differently, which is the one outcome a wire format cannot allow.- A disagreement or an unregistered value means the sender is faulty or is speaking a later revision. Neither is a reason to throw away an answer whose CRC passed and whose coarse status is perfectly usable — discarding it would leave the requester waiting out its whole window, which is worse than the folding this extension removes.
- An unregistered value therefore costs a later revision nothing: a station built to this document reads such an answer as its coarse status, exactly as it reads one from a station that sends no detail.
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.
19. Presence and capabilities #
PRESENCE (0x03) is sent unaddressed with presence_flags and an optional status text. It carries the presence policy bits only, not the capability word:
| Bit | Name | Meaning |
|---|---|---|
| 0 | mailbox accepting | the mailbox is accepting deposits right now |
| 1 | relay willing | the station will carry for others right now |
| 2 | accepts files | INBOUND: a file offered TO this station is accepted right now |
| 3 | accepts custody | custodial mail is accepted right now |
| 4 | serves shared folder | OUTBOUND: 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
- 1.A receiver MUST ignore a bit it does not recognise:
presence_flagsis not validated (see below), so an unknown bit costs nothing.
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:
| Bit | Name | Meaning |
|---|---|---|
| 0 | verified chat | Verified (ARQ) chat is bound. |
| 1 | station actions | The station-action service is bound. |
| 2 | files | The reliable file service is bound. |
| 3 | mail custody | Custodial mail is bound. |
| 4 | delivery receipts | JSQ delivery receipts are generated. |
| 5 | application relay | The station will carry for others (follows consent, live). |
| 6 | heard lists | Heard requests are answered. |
| 7 | mailbox | The mailbox actions are served. |
| 8 | shared library | The station UNDERSTANDS library_list / library_fetch; says nothing about whether anything is offered. |
| 9 | shared files available | VOLATILE, 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. |
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:
- AX.25/Bell-202 via conventional KISS for interoperability.
- JG.25 via PC audio/DigiRig.
- JG.25 via a future native modem API on devices such as custom Mobilinkd firmware.
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 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.
25. Items deliberately not frozen #
The following MUST remain experimental until further testing:
- 1.Whether the payload tone frequencies of Section 4 are the best choice on real radio paths (what is transmitted today is exact; a change would be a new mode value).
- 2.Inter-frame resync markers and multi-frame bursts: designed in v0.2 (Appendix C), not transmitted — every frame today carries the full acquisition and header.
- 3.~~Body interleaver serialization/padding rules~~ — fixed, Section 6.5.
- 4.Whitening: not transmitted (Section 8); a future revision that adds it must bump the wire version.
- 5.Session nonce/epoch: not transmitted (Appendix C).
- 6.BURST_INFO: not transmitted (Appendix C).
- 7.~~Numeric service IDs~~ — fixed, the packet type registry of Section 11.
- 8.~~Durable service-envelope byte format~~ — fixed, Sections 20–22.
- 9.~~Fragment/block byte format~~ — fixed, Sections 13 and 22.
- 10.Channel-access/backoff constants.
- 11.Real-radio SNR calibration and all adaptation thresholds (local policy, Section 16).
- 12.Whether short LDPC or another code should replace/augment the K=7 convolutional baseline (any change is a new FEC value or wire version).
- 13.~~The ACTION_RESPONSE status vocabulary~~ — fixed in v0.4, Section 18.1. The
statusoctet keeps its seven values and the finer outcome rides beside it in extension0x01, sorate_limited,quota_exceeded,too_large,stale_reference,storage_failure,deniedandinvalidnow cross this bearer intact. Appendix E records why the extension was chosen over widening the octet. What remains open is only the requester's behaviour toward an answer that carries NO detail: there the older readings stand — a fetcherroris taken as "the list may have changed", and abusyearns its automatic retry only while a recent listing makes the concurrency-slot reading plausible — and those are local policy, not wire. - 14.Presence bits 5–7, unallocated. Bit 4 (Section 19) is fixed in v0.5 and is the last allocation this document makes to the octet; the remaining three MUST be transmitted as zero until a revision names them. How long a receiver should keep believing a presence bit after the announcement that carried it is LOCAL POLICY and not on the air: presence has no lifetime field, and OpenJSQ gives a
file.sharedclaim three of its own announcement intervals so a single lost frame cannot retract a folder that was never withdrawn. A withdrawal is immediate in either case — an announcement without the bit is a statement that the folder is not served, not silence.
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:
- 1.Audio passband and level versus frequency.
- 2.Every JG mode's packet FER versus received level/SNR.
- 3.NORMAL and ROBUST acquisition false/miss rates.
- 4.350-baud header reliability.
- 5.Frequency-offset estimates and sample-clock estimates.
- 6.F0–F3 coding under real correlated errors.
- 7.64/128/256/512-byte frame goodput.
- 8.Inter-frame resync after intentionally corrupted frames.
- 9.Fast SACK turnaround.
- 10.Adaptive-controller behavior under changing RF conditions.
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.
Appendix C — The v0.2 link-layer design (not transmitted) #
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.
C.10. JG.25 link-layer frame #
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|
+--------+------------+-+-+-+
ver: link format version. Initial experimental value00.class: frame class.E: extension area present.A: ACK requested.P: priority/urgent indication.
10.3 Frame classes #
| class | Name | Purpose |
|---|---|---|
000 | BEACON | presence/discovery |
001 | OPEN | establish link/session context |
010 | DATA | reliable session data |
011 | DATAGRAM | connectionless message |
100 | ACK | selective acknowledgement |
101 | CONTROL | link control/management |
110 | CUSTODY | custody/store-and-forward control |
111 | EXTENDED | future/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:
| Type | Name | Notes |
|---|---|---|
0x0 | SOURCE | compact station address |
0x1 | DESTINATION | compact station address |
0x2 | SERVICE | application service ID |
0x3 | SACK | selective ACK data when not using compact ACK body |
0x4 | FRAGMENT | object/block fragmentation |
0x5 | MESSAGE_ID | stable end-to-end ID |
0x6 | PRIORITY | service/object priority |
0x7 | CAPABILITIES | station/modem/service capabilities |
0x8 | ROUTE | relay/routing information |
0x9 | TIMESTAMP | optional timestamp |
0xA | SESSION_NONCE | proposed restart/stale-context protection |
0xB | BURST_INFO | proposed burst metadata |
0xC–0xE | reserved | future standard use |
0xF | ESCAPE | extended/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:
- space = 0
- A–Z = 1–26
- 0–9 = 27–36
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:
- source station
- destination station
- service
- capabilities as needed
- session nonce/epoch
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.
- Initial receive window: 32 frames.
- Receiver ACKs a base sequence and a 32-bit bitmap.
- A set bit indicates the corresponding frame is held correctly.
- Sender retransmits only missing frames.
- Go-back-N is not used.
- ACK information MAY be piggybacked on outbound DATA.
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.
| Mode | Marker symbols | Airtime |
|---|---|---|
| JG1200 | 32 | 53.3 ms |
| JG1400 | 32 | 45.7 ms |
| JG1600 | 20 | 25.0 ms |
| JG1800 | 16 | 17.8 ms |
| JG2000 | 16 | 16.0 ms |
| JG2200 | 12 | 10.9 ms |
| JG2400 | 12 | 10.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 state | Target |
|---|---|
| quiet | ~8 s |
| normal shared channel | ~6 s |
| busy/collision-prone | ~3 s |
| absolute initial cap | 10 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, and one more for the presence shared-folder bit of Section 19; 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.
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 outcome | v0.3 status sent | What the requester saw |
|---|---|---|
| busy (a concurrency slot in use) | busy | busy |
| rate_limited (asking too often) | busy | busy |
| quota_exceeded (the day's budget is spent) | busy | busy |
| too_large (the file will not fit the offer) | error | internal_failure |
| stale_reference (the fingerprint no longer matches) | error | internal_failure |
| storage_failure (the file could not be read) | error | internal_failure |
| internal_failure | error | internal_failure |
| denied / invalid | refused | denied |
AX1200 (OFQ2) and FSQ both keep these apart, so this was a JG25-only narrowing, and the two harms it caused were concrete:
- 1.A retry that answered the wrong question. The requester gives a BUSY fetch one automatic retry two seconds later, because a busy shortly after a listing is usually the far side's listing transfer still holding its only concurrency slot. On JG25 that same retry also went to a station saying "you are asking too often" or "your quota for today is spent" — airtime spent to be told the same no, by a station that had just asked to be asked less.
- 2.A stale listing could not be told from a failure.
stale_referenceand a genuinestorage_failureboth arrived aserror, so the requester either discarded a good cached listing on every failure or kept a wrong one after a real change. It did the former, which is the safe reading and the wrong one about half the time.
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.
Part II — HF #
HF.1 Scope and relationship to FM #
JG25-HF carries the same OpenJSQ service envelope as JG25-FM through an HF single-sideband audio channel. It supports chat, acknowledgments, presence, station actions, files, mail and application relaying. It is a distinct physical format, not an FM mode at a reduced rate. An FM-only decoder cannot decode its HF acquisition/header/body merely by selecting different tones.
This part specifies the currently transmitted HF technical characteristics. It does not specify receiver software, detection algorithms, equalization, frequency estimation or proprietary implementation techniques. No source-code publication is needed to read this description.
The envelope and service definitions in Part I, Sections 10–15, 18–24A, and the envelope byte examples in Appendix D apply to HF too. Part II replaces Part I's physical waveform, acquisition, header-length interpretation and body-coding pipeline. FM receiver policies, FM adaptation tables and the untransmitted historical design in Appendix C do not define HF operation.
HF.2 Emission and waveform #
The waveform is continuous-phase, four-tone frequency-shift keying, carried as audio through a suppressed-carrier SSB transmitter. The intended data emission type is J2D. The waveform's nominal audio center is 1500 Hz. USB places the tones above the suppressed carrier frequency; LSB places them below it. Transmitter and receiver must use corresponding sidebands.
The reference waveform uses 48 kHz mono audio. Tone frequencies and symbol rates below define the signal; they do not require a particular sound card. Each symbol carries two coded bits. Tone index 0 is the lowest audio tone and index 3 is the highest. Dibits map as follows: 00 → 0, 01 → 1, 11 → 2, 10 → 3, with the first bit transmitted first. If a coded bit stream ends with a lone bit, one zero completes its final dibit.
Carrier phase is continuous across symbols and across the preamble, sync, header and body boundaries within one frame. A frequency change follows a half-cosine transition over the first quarter of the new symbol, then holds the target frequency for the remainder. There is no added amplitude ramp in the PHY waveform at frame start or finish. PTT lead/tail and transmitter filtering are separate from the encoded frame.
| HF mode field | Name | Body baud | Raw coded bit/s | Body audio tones, indexes 0–3 (Hz) |
|---|---|---|---|---|
000 | H600 | 300 | 600 | 1050 / 1350 / 1650 / 1950 |
001 | H900 | 450 | 900 | 825 / 1275 / 1725 / 2175 |
010 | H1200 | 600 | 1200 | 600 / 1200 / 1800 / 2400 |
011–111 | reserved | — | — | not transmitted |
Names indicate the uncoded two-bit-per-symbol rate, not application throughput. JHAT encoding, checksums, FEC, frame overhead and acknowledgments reduce delivered application throughput. All three modes use the same robust 300-baud acquisition and header; the selected body rate begins after the header.
HF.3 Frame structure and acquisition #
Each frame contains, in order: a 96-symbol robust preamble, a 20-symbol sync word, a 38-symbol protected header, and the selected-mode body. There is no inserted silence between these segments. Each segment starts its own symbol timing at the boundary while carrier phase remains continuous.
The preamble, sync and header all use 300 baud and tones 1050 / 1350 / 1650 / 1950 Hz. The HF transmitter does not send the FM 48-symbol NORMAL acquisition option or the FM 350-baud header.
Preamble, expressed as tone-index digits:
221002203312213231310103230210301122001311233030021031131323120323022321131300203323201201020011
Sync, expressed as tone-index digits:
00223310112132013023
The fixed preamble/sync/header portion occupies approximately 513.33 ms. Every frame carries a complete acquisition and header, including frames containing acknowledgments or presence announcements.
HF.4 Protected header #
The header begins with a 16-bit information word, most-significant byte and bit first, followed by a 16-bit CRC:
| Information-word bits | Width | Meaning |
|---|---|---|
| 15–13 | 3 | HF mode from HF.2 |
| 12–11 | 2 | Body FEC: 00 F0, 01 F1, 10 F2, 11 F3 |
| 10–0 | 11 | Number of JHAT ASCII characters in the body text |
The length is a character count, not a binary-envelope byte count. It includes the JH1D: prefix, Base64 characters including = padding, and the terminating CR and LF. It excludes the body CRC, FEC termination bits, redundancy and final dibit fill bit. The current PHY accepts 7–519 characters; normal service frames must additionally contain a valid nonempty envelope.
Header error detection is CRC-16/CCITT-FALSE: polynomial 0x1021, initial value 0xFFFF, no reflection and no final XOR. It covers the two information bytes and is appended most-significant byte first. The resulting 32 bits are protected by the rate-1/2 convolutional code described in HF.6, with six zero termination bits, producing 76 coded bits. An eight-row block interleaver reorders these bits before Gray mapping into 38 symbols. Header protection is always present, including when the body uses F0.
HF.5 ASCII/JHAT body and error detection #
The data character code is seven-bit ASCII (International Alphabet No. 5). JG25-HF uses the following JHAT/1 text representation to carry one binary JG25 service envelope:
JH1D: followed by canonical RFC 4648 Base64 of the envelope, followed by CR and LF.
The Base64 alphabet is A–Z, a–z, 0–9, +, /, with normal = padding. There are no embedded line breaks, spaces or optional decorations. CR and LF are ASCII values 13 and 10. The prefix identifies this data-text format; it is not an encryption marker.
An envelope may contain at most 384 bytes, including its addressing, type-specific fields, extensions and service payload. Its JHAT representation contains seven framing characters plus four Base64 characters per group of up to three envelope bytes, for a maximum of 519 characters. Application content therefore has less than 384 bytes of capacity after envelope overhead.
Each JHAT character is serialized as seven bits, bit 6 first through bit 0. No eighth data bit, per-character start/stop bits, parity bit or character separator is inserted. Frame synchronization and length replace asynchronous character framing.
A CRC-32C follows the serialized text. It is calculated over all JHAT characters, one zero-extended ASCII byte per character, including the prefix, Base64 padding and CR/LF. It is not calculated over the decoded binary envelope or over packed seven-bit bytes. Parameters are Castagnoli polynomial 0x1EDC6F41 (reflected form 0x82F63B78), initial value 0xFFFFFFFF, reflected processing and final XOR 0xFFFFFFFF. The resulting 32-bit value is appended bit 31 first through bit 0, then protected along with the text according to HF.6. This convention describes the implementation as shipped.
No additional FM PHY CRC is appended to the envelope before Base64 encoding. Checksums already belonging to a service payload retain their own meanings. No whitening, scrambling, encryption or secret code is added by this PHY. Base64 and FEC are transport/error-control transformations, not secrecy.
HF.6 Body error protection #
| FEC field | Profile | Nominal information/code rate | Transmitted protection |
|---|---|---|---|
00 | F0 | 1 | Text bits plus CRC; no convolutional code or body interleaver |
01 | F1 | 3/4 | Convolutional mother code, repeating keep mask 111001 |
10 | F2 | 2/3 | Convolutional mother code, repeating keep mask 1110 |
11 | F3 | 1/2 | Full convolutional mother code |
The convolutional code has constraint length 7, generators octal 171 and 133, and mother rate 1/2. The generator-171 output precedes generator-133 for each input bit. In delay notation, 171 uses the current input and delays 1, 2, 3, 6; 133 uses the current input and delays 2, 3, 5, 6. Each coded header or body starts in the all-zero state and ends with six zero input bits. These are transmitted-code characteristics, not a receiver algorithm.
For F1/F2, a 1 in the repeating mask means an output bit is sent and a 0 means it is omitted. The mask starts at the first mother-code output bit of each body and continues through the termination outputs. F3 omits no mother-code output bits. Nominal rates exclude finite-frame termination, checksums and other overhead.
Coded bodies use a 32-row block interleaver after puncturing; the header uses eight rows without puncturing. Bits fill rows left to right using the smallest number of columns that fits the block, then are transmitted column by column, top to bottom. Unfilled positions are skipped; no interleaver padding is transmitted. The resulting bits are mapped to dibits as in HF.2. F0 bypasses this body interleaving and has no convolutional termination bits.
Each body contains seven times its JHAT character count plus 32 information bits before FEC. Body airtime depends on that count, the FEC profile and the selected baud rate. A maximum 384-byte H600/F3 frame occupies approximately 12.75 seconds of PHY audio, excluding PTT lead/tail and channel-access delays.
HF.7 Service envelope, acknowledgments and identification #
After text/CRC validation and canonical Base64 decoding, the result is exactly one JG25 bearer envelope of wire version 1. Its magic byte 0x4A, type registry, flags, callsign addressing, extensions and service payload formats are the shared definitions in Part I. The HF layer does not introduce a second chat, file, mail, action or relay packet registry. An acknowledgment is an ordinary HF frame carrying the corresponding shared acknowledgment envelope.
The channel is half duplex. Frames contain their own mode and FEC selections; these are not a separately negotiated fixed rate for the entire conversation. HF uses longer local response/retry timing than FM to accommodate its airtime. Those timers, carrier detection, scheduling and receiver processing do not change the text code or frame layout specified here.
Source callsigns are carried in the shared envelope. The current HF session also schedules a PRESENCE envelope for identification, using a ten-minute interval and a 60-second idle-gap heuristic for the end of a communication. These are implementation behaviors, not a legal allowance to delay an ID. Holding automatic traffic also holds these scheduled transmissions. No separate JH1I text tag is currently transmitted; presence and identification frames use JH1D: like other data. Operators remain responsible for the identification requirements applicable to their transmissions.
HF.8 Public disclosure and operating limits #
This description is intended to support public documentation of the technique under 47 CFR 97.309(a)(3)–(4). That rule recognizes ASCII and permits techniques whose technical characteristics are publicly documented. It does not specify a minimum document length or require publication of source code. This document must actually be made publicly accessible to serve that purpose; keeping a copy only in a private repository is not public documentation. This is not a claim of FCC approval or a determination that every station configuration complies with Part 97.
For the intended U.S. 80/40/20 m HF data operation, apply the authorized 2.8 kHz bandwidth limit in the relevant segments and the operator's license privileges. Higher baud does not remove that bandwidth requirement. The 60 m band additionally has its own allocation/channel, power and primary-user protection requirements. See 97.307 and 97.303(h).
The width between the four nominal tones, an audio-filter setting, a decoder success rate or a simulated 99% occupied-bandwidth result does not establish the bandwidth of the actual RF emission. Use the bandwidth definition in 97.3(a)(8) and assess the real transmitter, including drive, filtering, keying effects and unwanted emissions. Our SSB screening results show limited H1200 margin and widening under simulated overdrive; they are not RF qualification. Identification obligations under 97.119 remain separate.
HF.9 Documentation baseline #
This HF part was checked against the branch implementation on September 14, 2026: the transmitted PHY, JHAT formatter, modem wrapper and HF service-session mapping. It documents the existing signal without changing the waveform or wire version. The older separately named HF design document is not required to interpret this part. Receiver internals and experimental decoding studies are deliberately outside its scope.