# Gnav Implementation

> Use when implementing or debugging G/NAV PRS uplink parsing — normal-band Field 2 extraction, S-band batch/almanac/PRS extraction, byte alignment, dummy batch detection, page_info bypass, or service descriptor handling for G/NAV in the eLTS project.

- Skill: `iabuelruzz/gnav-implementation` (Agent Skill)
- Install (CLI): `npx skillmds@latest add iabuelruzz/gnav-implementation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/iabuelruzz/gnav-implementation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: project-internal
- Author: IAbuElRuzz (https://skillmd.com/u/iabuelruzz)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/iabuelruzz/gnav-implementation

---


# 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()`**:
```python
_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:

```python
_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:

```python
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:

```python
# 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.

```python
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:

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

```python
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.

```python
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:

```python
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.

```python
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:

```python
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`:

```python
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:

```python
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:

```python
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...]` |

