G/NAV Implementation Reference
Detailed implementation guide for G/NAV PRS uplink parsing in src/parser/diss/gnav_parser.py.
Encryption and Bit Sizes (#GMSSSICD-3717)
G/NAV words are encrypted before packetisation on the uplink:
| Stage | Size | Constant | Padding to byte boundary |
|---|---|---|---|
| Pre-encryption (useful payload) | 164 bits | _GNAV_PREENC_BITS |
+4 → 168 bits = 21 bytes |
| Post-encryption (uplink slot size) | 283 bits | G_NAV_WORD |
+5 → 288 bits = 36 bytes |
Always pad before bits_to_bytes():
_G_NAV_WORD_PAD = (-G_NAV_WORD) % 8 # 5 bits
_GNAV_PREENC_PAD = (-_GNAV_PREENC_BITS) % 8 # 4 bits
word_bytes = bits_to_bytes(word_bits + "0" * _G_NAV_WORD_PAD)
Bitfield descriptors in config/g_nav/g_nav_uplink.json cover the 164-bit pre-encryption payload (offsets go up to 164 bits).
Normal-Band: Field 2 (§4.2.4.2, extract_gnav_page)
G/NAV data is in Message sub-frame Field 2, packets 6-11. Three consecutive packet pairs each carry one G/NAV word slot:
| Slot | Packets | Slot index |
|---|---|---|
| 0 | 6 + 7 | 0 |
| 1 | 8 + 9 | 1 |
| 2 | 10 + 11 | 2 |
Layout constants:
_FIELD2_BYTE_OFFSET = 1784 // 8 # 223 — byte offset of Field 2 start in subframe
_GNAV_PACKET_BYTES = 264 // 8 # 33 — bytes per packet slot
_GNAV_SLOT_BYTES = 33 * 2 # 66 — bytes for one PRS(1/2) + PRS(2/2) pair
_SERVICE_DESCRIPTOR_BITS = 3
_GNAV_PREENC_BITS = 164
_GNAV_PREENC_PAD = 4
Extraction logic:
slot_idx = subframe_id % 3 # subframe_id from generate_subframe_id()
pkt1_start = _FIELD2_BYTE_OFFSET + slot_idx * _GNAV_SLOT_BYTES
slot_bits = bytes_to_bits(subframe[pkt1_start : pkt1_start + _GNAV_SLOT_BYTES])
# Skip dummy (110) or spare (111) slots
service_desc = slot_bits[:_SERVICE_DESCRIPTOR_BITS]
if service_desc in ("110", "111"):
return None, None
# Pre-encryption word starts after the 3-bit service descriptor
g_nav_word_bits = slot_bits[_SERVICE_DESCRIPTOR_BITS : _SERVICE_DESCRIPTOR_BITS + _GNAV_PREENC_BITS]
g_nav_int = bits_to_bytes(g_nav_word_bits + "0" * _GNAV_PREENC_PAD) # 21 bytes
page_info Bypass (Critical)
collect_lookup_info(MSG_SUBFRAME_BITS) only aggregates nodes with a lookupValue from config/frames/msg_subframe.json. Field 2 packets (6-11) have no lookupValue, so page_info["GNAV"] never exists.
In MsgSubfrParser.extract_nav_page(), the GNAV branch must appear before the page_info lookup block:
# MUST come before the page_info dict lookup
if req_nav_type == "GNAV":
parser = gnav_parser.GNavParser(self.isComparison)
return parser.extract_gnav_page(subframe, str(req_page_type), subframe_id, subframe_data, msg_filt)
# Only reached for FNAV / INAV / CNAV which have lookupValues:
page_data = page_info[req_nav_type] # would KeyError for GNAV
S-Band Navigation Batches: G/NAV-1 to G/NAV-5 (extract_gnav_page_S_band)
Reference: §4.2.4.2, Figure 18; #GMSSSICD-3640, #GMSSSICD-3752.
GNAV_OFFSET = 26480 # bits, data-relative (after HEADER)
G_NAV_BATCH = 1464 # bits per nav batch slot (BATCH_START + 5 × G_NAV_WORD + 1 spare)
# 9 slots total between GNAV_OFFSET and GNAV_ALMANAC_OFFSET
Slot selection and dummy detection:
slot_start = GNAV_OFFSET + (batch_number - 1) * G_NAV_BATCH
g_nav_batch = packet_bits[HEADER:][slot_start : slot_start + G_NAV_BATCH]
# Dummy detection (#GMSSSICD-3752, Figure 19):
# Slot 5 is a dummy when fewer than 5 nav batches are available.
# Detected by TOW = all-ones (bits 16-35) AND WN = all-ones (bits 36-47) within BATCH_START.
_TOW_START, _TOW_END = 16, 36
_WN_START, _WN_END = 36, 48
if g_nav_batch[_TOW_START:_TOW_END] == "1" * 20 and \
g_nav_batch[_WN_START:_WN_END] == "1" * 12:
return None, None
Word extraction for G/NAV-1..G/NAV-5 (one word per page type per batch):
page_num = int(req_page_type) # 1-5
word_start = BATCH_START + (page_num - 1) * G_NAV_WORD
g_nav_word_bits = g_nav_batch[word_start : word_start + G_NAV_WORD]
g_nav_int = bits_to_bytes(g_nav_word_bits + "0" * _G_NAV_WORD_PAD) # 36 bytes
S-Band Almanac: G/NAV-6 (extract_gnav_almanac_S_band)
Reference: #GMSSSICD-3749, Table 13.
GNAV_ALMANAC_OFFSET = 39656 # bits, data-relative
G_NAV_ALMANAC_BATCH = 2600 # bits (BATCH_START + 9 × G_NAV_WORD + 5 spare)
# 8 slots (4 SV groups × 2 batches)
First word in each slot:
slot_start = GNAV_ALMANAC_OFFSET + (batch_number - 1) * G_NAV_ALMANAC_BATCH
g_nav_alm_batch = packet_bits[HEADER:][slot_start : slot_start + G_NAV_ALMANAC_BATCH]
g_nav_alm_bits = g_nav_alm_batch[BATCH_START : BATCH_START + G_NAV_WORD]
g_nav_alm = bits_to_bytes(g_nav_alm_bits + "0" * _G_NAV_WORD_PAD) # 36 bytes
Only valid when req_page_type == "6" — return None, None otherwise.
S-Band PRS: G/NAV-9 (extract_gnav_prs_S_band)
Reference: #GMSSSICD-3644.
GNAV_PRS_OFFSET = 60456 # bits, data-relative
G_NAV_PRS_BATCH = 7408 # bits (BATCH_START + 26 × G_NAV_WORD + 2 spare)
# 20 slots (20 × 26 = 520 G/NAV-9 words total)
First word in each slot:
slot_start = GNAV_PRS_OFFSET + (batch_number - 1) * G_NAV_PRS_BATCH
g_nav_prs_batch = packet_bits[HEADER:][slot_start : slot_start + G_NAV_PRS_BATCH]
g_nav_prs_bits = g_nav_prs_batch[BATCH_START : BATCH_START + G_NAV_WORD]
g_nav_prs = bits_to_bytes(g_nav_prs_bits + "0" * _G_NAV_WORD_PAD) # 36 bytes
Only valid when req_page_type == "9" — return None, None otherwise.
DissFilter Loop Counts for G/NAV (_process_s_band_block)
| Page type(s) | Loop range | Reason |
|---|---|---|
G/NAV-1..5 (page_num 1-5) |
range(9) |
Up to 9 nav batch slots (#GMSSSICD-3640); dummy detection handles missing slot 5 |
| G/NAV-6 almanac | range(8) |
8 almanac batch slots (#GMSSSICD-3749, Table 13) |
| G/NAV-9 PRS | range(20) |
20 PRS batch slots (#GMSSSICD-3644) |
Comparison Mode
When isComparison=True, all four methods skip bit extraction entirely and pass the raw packet/subframe bytes directly to GnavUplink:
if self.isComparison:
gnav_uplink = GnavUplink(subframe, subframe_data, req_page_type, msg_filt, None, True)
return gnav_uplink.get_data_structure()
GnavUplink Integration
GnavUplink lives in src/parser/nav_utils/g_nav/g_nav_uplink.py. Constructor signature:
GnavUplink(
packet: bytes,
subframe_data: dict,
gnav_page_type: str, # "1".."5", "6", "9"
msg_filt: dict | None = None,
extra: Any = None,
isComparison: bool = False,
)
GNAV_MAPS (module-level dict keyed by page type string) is loaded from config/g_nav/g_nav_uplink.json.
Static helper methods used for headers:
GnavUplink.get_gnav_csv_header(req_page_type, msg_filt) # → list[str]
GnavUplink.get_field_bitsizes(req_page_type, msg_filt, tag_message) # → dict
Both raise ValueError("Bit descriptor not found") if req_page_type is not in GNAV_MAPS.
Common Pitfalls
| Pitfall | Correct approach |
|---|---|
bits_to_bytes() on 283-bit string |
Pad 5 zeros first: word_bits + "0" * 5 |
bits_to_bytes() on 164-bit string |
Pad 4 zeros first: word_bits + "0" * 4 |
page_info["GNAV"] KeyError |
Early-exit GNAV branch before page_info lookup in msg_subfr_parser.py |
_parse_packet() returning bare int |
Must return (self.fieldIndex, self.number_of_mismatch) tuple |
Wrong gnav_page_len in debug section |
Use G_NAV_WORD = 283, loop range(5) for 5 words/batch |
| S-band offsets applied to raw bytes | All S-band bit offsets are data-relative (after 184-bit HEADER): use packet_bits[HEADER:][offset...] |