libgnss-recv-gps

Integration Testing and Bug Fixes

This document records the findings, issues, and solutions encountered during integration testing of the Orientable-AI-GNSS / libgnss-recv-l1ca receiver against the project’s GPS signal generators.

Two campaigns are documented:

  1. libgnss-tx-swift generator → receiver (initial bring-up).
  2. libgnss-gen-l1 generator → libgnss-recv-l1ca receiver (cross-repository loopback).

Campaign 1 — libgnss-tx-swift → receiver

Testing Setup

Findings & Issues

During integration testing, the target receiver successfully acquired satellites, tracked signals, and found bit synchronization. However, it repeatedly failed at the subframe decoding stage despite finding the correct preamble (10001011).

Two major bugs were identified:

  1. Incorrect Preamble Polarity Configuration (src/core/sdrinit.c) The GPS L1 C/A preamble array pre_l1ca was misconfigured as { 1, -1, -1, -1, 1, -1, 1, 1 }. In the internal logic where -1 represents a binary 1 and 1 represents a binary 0, this mapped to 01110100 instead of the correct GPS preamble 10001011. As a result, when the true preamble was received, the correlation was -8, leading the receiver to wrongly flag the signal as inverted (polarity = -1). This double inversion corrupted the bit stream before the parity check.

  2. Faulty Parity Check Implementation (src/core/coding/hamming32.c) The original paritycheck_l1ca implementation did not correctly implement the GPS IS-GPS-200 Hamming(32, 26) parity check algorithm. Specifically, it failed to properly un-invert the parity bits ($D_{25}$ to $D_{30}$) when the 30th bit of the previous word ($D_{30}^*$) was a 1.

Solutions

Note (superseded by Campaign 2): the original fix un-inverted with the mask 0x3FFFFFFF, which flips all 30 low bits (24 data bits and 6 parity bits). Campaign 2 showed this is incorrect for words whose predecessor ends in $D_{30}=1$; the mask was corrected to 0x3FFFFFC0 (data bits only). See Campaign 2, Issue 2.

Verification

Following the fixes, the integration test against the 90-second generated libgnss-tx-swift signal was successful. Orientable-AI-GNSS successfully decoded Subframe 4 and Subframe 5 with passing parity checks.


Campaign 2 — libgnss-gen-l1libgnss-recv-l1ca

Testing Setup

Findings & Issues

The receiver acquired all visible satellites with strong tracking (C/N0 ≈ 38–50 dB-Hz) and found bit synchronization, but never decoded a subframe: the preamble was rarely detected and the parity check never reached 10/10 words (best observed was 4/10).

Debugging from the receiver side (good acquisition but garbled nav bits) and then instrumenting the generator’s transmitted nav-bit stream revealed two independent bugs, one in each repository:

  1. Generator: navigation subframes were never populated (libgnss-gen-l1/src/mac/gps_lnav.c) generate_gps_lnav() ignored its ephemeris argument ((void)eph;) and never called gps_lnav_eph2sbf(), so link->sbf stayed all-zeros (from calloc). Every telemetry (TLM) word was transmitted as 0x00000000 — the preamble 10001011 simply did not exist in the signal. The handover words (HOW) appeared non-zero only because TOW is injected directly (sbfm_word |= tow << 13), which masked the problem in casual inspection. This bug went unnoticed because the receiver had previously only been integration-tested against libgnss-tx-swift (Campaign 1), not against libgnss-gen-l1.

  2. Receiver: parity check un-inverted the parity bits (src/core/coding/hamming32.c) decode_l1ca_word() used if (word & 0x40000000) word ^= 0x3FFFFFFF;. The mask 0x3FFFFFFF flips all 30 low bits — the 24 data bits and the 6 parity bits. Per IS-GPS-200 the parity bits ($D_{25}$–$D_{30}$) are transmitted un-inverted; only the 24 data bits are inverted by $D_{30}^$. As a result, every word whose predecessor ended in $D_{30}=1$ failed parity. The existing unit test (Hamming32Test.ParityChecksumMatches) only exercised the $D_{30}^=0$ case, so the bug was latent. (This is the mask that Campaign 1’s note above flagged as superseded.)

Solutions

Verification

Reproducing

# 1. Generate the signal (writes output/integration_test_l1ca.bin)
cd <libgnss-gen-l1>
./gen-l1-signals -c configure/integration_test_l1ca.json

# 2. Run the receiver on the generated file
cd <libgnss-recv-l1ca>
./build/orientable-libgnss-cli cfg/integration_test_gen_l1.yaml < /dev/null

# 3. Inspect decoded subframes
grep -E "ID=[1-5] tow" log/integration_test.log

Known minor artifact

A one-time double initialization call in the generator stretches the first ~200 ms of nav bits (the first 5 bits are emitted twice). The receiver recovers bit synchronization immediately afterwards and decodes subsequent subframes normally; it does not affect the loopback result.


Campaign 3 — Post-Improvement Validation (2026-07-20)

Context

Following the CU-SDR comparative architecture review (see docs/developer-guide/cu-sdr-comparison-review.md), three algorithm improvements were ported from the CU-SDR MATLAB reference into the C receiver:

  1. Fine frequency acquisition (fine_acquisition() in sdracq.c): 40 ms coherent integration with 25 Hz frequency bins and 20 nav-bit-edge hypotheses, reducing frequency handoff error from ±100 Hz to ±12.5 Hz.
  2. VSM C/N₀ estimator (cn0_vsm() in sdrtrk.c): Variance Summing Method replacing the undocumented heuristic formula, providing front-end-independent signal quality metrics.
  3. Lock detector + cycle-slip flags (lockdetect() in sdrtrk.c): carrier lock indicator using normalized |IP|/mag ratio, with Doppler-predicted vs. measured carrier phase comparison for cycle-slip detection. LLI flags propagated through sdrobs_t to RINEX output.

Testing Setup

Results

Metric Count
Satellites acquired 13
Bit syncs found 9
Preambles locked 2 (G03, G08)
Subframes decoded 7
Parity failures (transient) 36
Cycle slips detected 6

Decoded subframes

Satellite Subframe ID TOW (s) Week
G08 4 518424
G08 5 518430
G08 1 518436 2190
G08 2 518442 2190
G03 5 518430
G03 1 518436 2190
G03 3 518442 2190

TOW increments by 6 s per subframe — correct per IS-GPS-200. Week 2190 matches the generator’s epoch (2022-01-01). G08 decoded 4 consecutive subframes (4→5→1→2); G03 decoded 3 (5→1→3).

Feature Validation

Fine acquisition: All 13 acquisitions used the new fine_acquisition() stage. Frequency estimates at 25 Hz resolution (e.g., G01: −75 Hz, G10: +100 Hz, G16: +275 Hz). The fine stage runs after coarse FFT detection and before tracking handoff.

VSM C/N₀: Acquisition C/N₀ values reported in the 37–52 dB-Hz range, consistent with the generator’s 15 dB SNR setting and the expected processing gain. Values are stable across channels and physically plausible.

Lock detector: 6 cycle slips detected and logged (e.g., G07 cycle slip detected: dL_meas=0.1911 dL_pred=0.8282), confirming the feature triggers correctly during tracking transients. LLI flags propagated to sdrobs_t.

Parity Algorithm Cross-Check

A standalone C program (/tmp/parity_e2e.c) was written to verify end-to-end parity compatibility between the generator’s calc_hamming32_checksum_v0() and the receiver’s decode_l1ca_word(). The test:

  1. Generates a full 5-subframe LNAV frame using the generator’s checksum function
  2. Converts 30-bit words to a ±1 bit stream (simulating signal modulation)
  3. Applies polarity correction (simulating receiver’s findpreamble with corr=−8)
  4. Constructs 32-bit words and checks parity using the receiver’s decode function

Result: All 20 words across 2 subframes pass — the algorithms are provably compatible. The 36 transient parity failures in the live integration test are caused by the tracking loop needing ~8 seconds to converge (PLL/FLL pull-in from residual acquisition frequency), not by any algorithmic mismatch.

Reproducing

# 1. Generate the signal (writes output/integration_test_l1ca.bin)
cd <libgnss-gen-l1>
./gen-l1-signals -c configure/integration_test_l1ca.json

# 2. Run the receiver on the generated file
cd <libgnss-recv-l1ca>
./build/orientable-libgnss-cli cfg/integration_test_gen_l1.yaml < /dev/null

# 3. Inspect decoded subframes
grep "ID=" log/integration_test.log

# 4. Run unit tests (103 tests)
cd build && ./unit_tests

Automated closed-loop harness (2026-08-03)

The Campaign 2/3 loopback is automated by <libgnss-gen-l1>/tests/validation/closed_loop_receiver_test.py:

  1. Generates the 45 s scenario (configure/integration_test_l1ca.jsonoutput/integration_test_l1ca.bin, 234 MB of ci8 @ 2.6 MHz).
  2. Runs orientable-libgnss-cli cfg/integration_test_gen_l1.yaml on it.
  3. Asserts against log/integration_test.log:
    • ≥ 4 satellites acquired (flagacq = 1)
    • ≥ 1 bit synchronization
    • ≥ 2 valid subframe decodes (TOW on the 6 s subframe grid inside the scenario window). Isolated corrupt decodes — a tracking transient whose bits happen to pass parity — are reported as anomalies but do not fail the test, mirroring the transient parity statistics of Campaign 3.
    • Week 2190 recovered from at least one valid decode.
cd <libgnss-gen-l1>
python3 tests/validation/closed_loop_receiver_test.py                 # full run (~7 min)
python3 tests/validation/closed_loop_receiver_test.py --reuse-signal  # iterate on receiver side

Exit codes: 0 pass, 1 fail, 2 prerequisites missing (both build/gen-l1-signals and build/orientable-libgnss-cli must exist; the receiver repository is located as a sibling directory or via LIBGNSS_RECV_L1CA).

Reference run (2026-08-03): 13 acquisitions (C/N0 44–52 dB-Hz), 8 bit syncs, 6 valid subframes across G01/G03 with week 2190 and TOWs 518424–518442, 0 anomalous decodes.