Start with the misses
The older targets remain useful history. Cell 163 near 735.1 MHz and cell 467 near 742.6 MHz were visible to the scanner but weak and intermittent under the lab's conditions. FalconEye still did not acquire them on the September 16 recheck. That result does not invalidate the receiver, and it does not by itself prove an environmental root cause. It shows why a visible spectral peak and even a scanner detection were never sufficient acceptance criteria.
The first fixes were too satisfying
The initial debug run found two explanations that looked almost perfect. FALCON's coarse carrier-frequency estimate sat around 4.5–5.5 kHz where CellSearch reported roughly 200–300 Hz, and old synchronization code had a fixed tracking threshold of 1.2 while live HackRF peaks during losses clustered near 1.0. Clearing a CP-CFO term reduced the reported offset. Lowering the threshold to 0.9 produced long, uninterrupted tracking runs.
We later pulled both changes out of the active patch. The CP correction order had been read incorrectly, so the supposed CFO double-count was not established. The lower threshold kept tracking alive by admitting peaks we had not shown were unambiguous. It improved the log, not the proof. The historical notes retain the experiments because the wrong turns matter, but they are no longer presented as root causes or production fixes.
Several simpler theories did survive direct testing in the useful sense: they were ruled out. Within FALCON, requested aggregate gain matched the aggregate readback. Captures through raw libhackrf and SoapySDR put an intentionally offset FM station on the correct side, so IQ inversion was not the culprit. The HackRF Soapy module's 0.00 MHz master-clock report turned out to be an unimplemented USRP-oriented API, not evidence that the radio clock was stopped.
The confirmed bug was below the decoder
The receive contract was loose enough to poison everything above it. A short or failed Soapy read could reach synchronization as if the requested buffer were complete. Acquisition was bounded mainly by frame counts, which quietly assumed that samples arrived at approximately real-time speed; a stream making slow forward progress could keep one attempt alive far beyond the intended budget. When a discontinuity did occur, timing, CFO, and PBCH combining state were not treated as one contaminated unit.
That layer is now much harder to lie to:
- Short reads are accumulated only inside one bounded transaction. Timeout, overflow, corruption, disconnect, zero-length, abrupt-end, and oversized returns fail the whole request. Partial buffers are never accepted.
- Cell search and MIB decoding share one monotonic wall-clock deadline, ten seconds by default. MIB does not get a fresh clock because cell search used most of the first one.
- A receive failure resets synchronization, timing, CFO, and PBCH combining state instead of carrying history across a sample gap.
- Soapy now exposes monotonic counters for samples, short reads, timeouts, overflows, corruptions, disconnects, discontinuities, and reconnects.
- FalconEye requires two agreeing CRC-valid full-band MIB observations with valid SFN progression before it starts DCI work.
Those are host-testable controls. Gate 3 reached eleven focused tests; Gate 4 brought the suite to 13/13; Gate 5 brought it to 17/17; and the first Gate 6 recovery tests now bring the focused reliability suite to 19/19. The test surface includes slow-progress deadlines, every injected receive failure, reset behavior, MIB consistency and SFN wraparound, exact replay reads, truncated input, negative fixtures for noise, no signal, and clipping, continuous-decimator timing, synthetic sync/CFO recovery, repeated cell-identity checks, PBCH stage capture, positive fixture consistency, PBCH reset behavior, RS-CFO estimator coverage, recovery state-machine coverage, and stale-worker generation tagging. A repository-managed patch pins the old srsLTE dependency instead of leaving a working build trapped in one developer's build directory.
Make the failure replayable
Live RF is an awful sole source of truth. A decoder change, the weather, an antenna bump, and a different cell scheduler can all move at once. FalconAcquire now gives the live and replay paths the same callback shape and writes requested versus actual sample rate and frequency, aggregate gain, first-sample position, discontinuity information, clipping fraction, completion state, and a SHA-256 for the capture.
The fixture infrastructure now records enough metadata to make a future failure useful: exact replay reads, truncated input, discontinuity handling, clipping checks, negative fixtures for noise and no signal, and a manifest verifier. At this stage in the story, the important change is not that the radio has won. It is that the next radio result can be pinned to bytes instead of remembered as a console scrollback.
FalconEye's radio no longer changes underneath it
At the first publication checkpoint, the next structural problem was the acquisition-to-decode handoff. FALCON retimed the HackRF three times: 1.92 Msps for cell search, stop; 1.92 Msps again for MIB, stop; then 7.68 Msps for a 5 MHz cell or 15.36 Msps for a 10 MHz cell before full-band processing. The receiver had to rediscover continuity precisely where it needed to preserve timing.
Gate 3 is now merged into master around one hardware stream. FalconEye opens the HackRF once at 15.36 Msps and starts RX once. It derives 1.92 Msps for cell search and MIB by dividing by eight, derives 7.68 Msps for 5 MHz full-band by dividing by two, and passes 15.36 Msps directly into the 10 MHz path. There is no post-acquisition sample-rate change, stop, or second start left in EyeCore.
The acquisition and MIB state machines remain independent, so the design resets decimator history between them even though the hardware stream does not. A real receive error also resets the filter before the next use. Full-band filter state persists across callback boundaries, and clipping accounting now covers the passthrough path as well as the decimated path. FalconCaptureProbe's acquisition startup was repaired in the same final review wave; at that checkpoint, the larger Eye/GUI recovery state machine still belonged to Gate 6.
From the filter to the full path
The filter measurements did not move: ratio-eight impulse delay at nine output samples, ratio-two at ten, passband edges near -0.03 dB and 0.01 dB, and stopband rejection near -55 dB and -69 dB. Whole-buffer and chunked runs produce 400 identical outputs. The completed subframe-boundary test then injected ten periodic markers into production-sized 1 ms chunks and recovered all ten at the correct offsets through both divide-by-eight and divide-by-two paths.
The final whole-branch review was more valuable than the task count. It found three critical contract errors: the acquisition wrapper mistook an exact positive sample count for failure, the 5 MHz wrapper returned zero instead of its output count, and FalconCaptureProbe had not actually been fixed by the shared work. All three were corrected before merge. Gate 3 closed at 11/11; the two Gate 4 tests bring the current focused suite to 13/13 on master.
The first live read was still too late
The merged path reached real hardware and failed immediately for a different reason. The 15.36 Msps stream started before srsLTE finished one-time cell-search setup. That setup took about 777 ms on the attached HackRF Pro — enough time to accumulate roughly 24 MB of unread 8-bit IQ and overflow the driver before the first acquisition read.
The fix does not stop or retune the radio. It drains through the same bounded receive path after cell-search and MIB setup, in 1 ms chunks, until read latency approaches real time. After that change, FalconEye stopped aborting on its first data-critical read and completed all three configured acquisition attempts at both target frequencies. The driver counters still recorded one overflow at 735.1 MHz and two at 742.5 MHz, so the claim is deliberately narrow: the setup backlog no longer aborts acquisition. Continuous loss-free reception is not yet proven.
Acquisition remained open for those two frequencies. FALCON gains of 8, 20, and 32 dB exhausted the retry budget without a detection, and the September 16 retry at 735 MHz failed again at 40 dB. The fresh LTE-Cell-Scanner sweep nevertheless found both cells. In one LTE-Tracker snapshot at the scanner's higher gain setting, cell 163 held health 90 while its four displayed port SNRs sat between roughly -12.6 and -15.7 dB; cell 155 was tracked alongside it with similarly negative values. That is consistent with a detectable but noise- or interference-limited carrier under the tracker's settings, not proof of why FALCON misses it. The tools used different per-stage gain settings and different acquisition algorithms, so the comparison is not controlled.
The immediate overflow abort is gone. The acquisition gap is now repeatable enough to compare receivers instead of guessing from crashes.
The narrower carrier opened the path
The same session later explained why an earlier 100-PRB carrier at 1955 MHz stopped after its first CRC-valid MIB. In this srsLTE build, reduced sampling maps 75 PRB to 15.36 Msps, which FALCON's continuous path supports. A 100-PRB carrier maps to 23.04 Msps, already above HackRF's supported 20 Msps maximum even before considering LTE's standard 30.72 Msps rate. The wider cell proved acquisition and PBCH; the narrower component carrier made full-band control-channel candidate processing possible.
A fresh AWS Band 4 scan then found PCI 201 at 2147.5 MHz, 75 resource blocks / 15 MHz, with two antenna ports. FalconEye found it at a 100% detect ratio and 17.84 peak-to-sidelobe ratio, then accepted two CRC-valid MIB decodes at system frame numbers 477 and 478. That matters more than entering PBCH: FalconEye requires two MIB observations with matching cell parameters and valid SFN progression before it starts DCI work.
It started DCI work. The run processed 3,715 real subframes, logged 125,323 candidate-decode attempts, and reported six active-RNTI candidates. The active-set table placed them in internal associated-format index buckets 0, 3, and 5; those numbers are FALCON indices, not literal LTE DCI format names. Both downlink and uplink subframe-collision counters stayed at zero. This is the first recorded live run in this project that reaches the full FALCON pipeline and produces candidate identifier and control-channel output from an unmodified LTE network. The run does not independently establish that each candidate or inferred allocation is correct.
The proof is no longer a peak or a tracking state. Two agreeing CRC-valid MIBs are hard evidence. The PDCCH/DCI and RNTI output is the next layer of evidence, still awaiting independent ground truth.
The fixture manifest now has its first positive entry: a 30,623,320-byte capture of PCI 201 at 2147.5 MHz, recorded at 1.92 Msps for acquisition and MIB work. The run had a 30-second deadline but stopped after 3,827,915 samples, roughly two seconds, once the MIB passed. Its machine-readable sidecar records a complete capture with zero discontinuities or clipping and the SHA-256 db4e19bf…a1a8d; the live capture console output and manifest prose record a CRC-valid 75-PRB MIB with two antenna ports and zero receive failures. The raw file remains local because the project does not yet have a remote fixture store. This is a registered positive capture, not yet a demonstrated replay, a full-band PDCCH fixture, or a reference-decoder comparison with known grants.
The later PCI 201 runs changed the live evidence substantially: acquisition, MIB, and full-band handoff have now all occurred through the HackRF Pro, and a positive acquisition fixture is registered. They do not retroactively close every Gate 3 criterion. A 30-minute Soapy-versus-direct-libhackrf comparison, reference-equivalent timing checks, and the original two-band target matrix are still absent.
Gate 5 is now an instrumented checkpoint
The Gate 5 implementation wave is merged at a497f1c, and the focused reliability suite now passes 17/17. The new work did not try to change the live receive path. It built the PBCH/MIB inspection machinery around the path we just proved live: a stage-export diagnostic module, a positive fixture consistency test for the registered PCI 201 capture, a PBCH combining reset test, and RS-CFO estimator coverage folded in from the Gate 4 parked list. The fixture manifest also verifies locally: one positive IQ file present, one SHA checked, zero skipped.
The useful part is what the diagnostic can now see. TestPbchStageCapture reproduces the two-stage handoff the radio uses in practice: coarse cell search establishes timing and CFO on the 1.92 Msps fixture, then MIB decode's own synchronizer tracks subframe zero and calls the new capture routine. That routine records the pieces Gate 5 originally asked for: CP, PRB count, FFT symbols, PBCH resource elements, channel estimates, equalized symbols, soft bits, antenna-port hypothesis, CRC result, payload, and SFN. On the registered capture it finds the expected two-port MIB path.
TestReferenceFixtureConsistency is deliberately narrower than its name sounds. It proves FALCON still decodes the known-good real IQ fixture as PCI 201, 75 PRB, and two ports. That is a real regression lock on the first positive capture. It is not an independent CellSearch comparison: the expected values come from the capture metadata and manifest prose created by FALCON's earlier successful run. The gate document now says that plainly, which is better than pretending a self-check is a reference decoder.
So Gate 5 is finalized as an implementation wave, not fully closed as an original pass criterion. Synthetic PBCH vectors for 1/2/4 ports and 5/10 MHz payloads do not exist yet. Positive and negative replay coverage still runs at the library callback layer, not through the FalconAcquire and FalconEye binaries end to end. Fixture-level SFN progression is not asserted on the real capture, even though TestMibConsistency covers the cadence logic. And the actual independent FALCON-versus-CellSearch check remains a documented manual procedure, not a completed result.
Gate 6 found a worse truth, then bounded it
Gate 6 is not closed, but it is no longer just a line in the plan. FalconEye now has its first real recovery behavior for the live-RF path. A receive discontinuity and a sustained tracking-loss run both stop new DCI work, bump the receiver generation, reset PBCH/MIB consistency and RNTI history, and require the same two-agreeing-CRC-valid-MIB check before PDCCH/DCI work is allowed again. Worker output is tagged with the generation it was submitted under, so stale pre-recovery work can be discarded instead of mutating the recovered state.
The narrow host proof is now in the suite: TestReceiverRecovery covers the recovery state machine, and TestSubframeWorkerGeneration exercises the stale-generation contract directly. That moves the focused suite from 17/17 to 19/19. The live proof is more sobering. A real run triggered recovery, discarded stale DCI output, and reported Recoveries: 1 with stale-generation DCI results discarded: 11; it also showed that recovery can fail to regain repeated MIB agreement promptly. The applied fix is a bounded failure mode: if a recovery-triggered MIB re-validation does not complete within 60 seconds, the receiver exits cleanly and observably instead of silently spinning forever.
That distinction matters. Gate 6 has recovery triggers, generation tagging, stale-result rejection, and a clean timeout. It does not yet satisfy the original hardware criterion of twenty interruptions recovering within ten seconds, and the root cause of the long ret == 0 stall remains open. It is better engineering than before because a stuck recovery is now visible and restartable. It is not endurance.
The GUI finally joined the radio path
The biggest practical change came after stepping back from the gate ledger. The real goal was never only a CLI demo; it was FALCON's visible workflow — listen, capture, decode MIB, and see who is on the tower — working from FalconGUI with the same HackRF Pro. The root cause was embarrassingly concrete: on_actionStart_triggered() never copied RF device args or gain out of the GUI into eyeArgs. So the GUI silently kept empty device args and AGC-only gain, while the CLI path had always exposed -a and -g.
That made the GUI the one path that could not apply the lessons the radio had already taught us. This lab has more than one SDR/USB device, so empty Soapy args can select the wrong thing or behave ambiguously. The HackRF Pro also did not behave well under AGC here; fixed gain was the reliable path, and the successful CLI work converged around the same carrier and gain values. Commit 0c23f8c adds the missing GUI controls: RF Args defaults to driver=hackrf, Gain defaults to a fixed 40 dB, and there is still an AGC toggle when that is wanted.
Then the GUI reached the live result. With Frequency 2147.5 MHz, RF Args driver=hackrf, and fixed Gain 40 dB, clicking Start in FalconGUI reached cell search, MIB decode, and full PDCCH/DCI tracking. The UE Activity table populated with live, changing RNTIs; the MCS Index and Total Throughput charts updated with live timestamps; and both Uplink and Downlink RB Allocation waterfalls filled with live resource-block data. Stop was clean: Destroyed Phy, no crash.
This is the first recorded point where the GUI reaches the same live chain the CLI had already proved: HackRF Pro, MIB acceptance, PDCCH/DCI tracking, and changing RNTIs on screen.
The caveat is still honest: GUI automation has not been built, and Gate 6's recovery counters and lock-health telemetry are still mostly CLI-visible. But the old GUI problem was not a mysterious RF failure. It was missing wiring between the controls the user needed and the receiver arguments the radio actually used.
The gate ledger
| Gate | What it proves | Current state |
|---|---|---|
| 0 | Reproducible pinned build and known library paths | Validation build and library-path checks pass locally |
| 1 | Exact contiguous receive or explicit failure | Host controls landed; 30-minute Soapy vs. direct-libhackrf run remains |
| 2 | Capture and deterministic replay evidence | Metadata, manifest, replay hardening, negatives, and first positive 1.92 Msps acquisition fixture landed; remote storage and full-band known-grant fixture remain |
| 3 | One continuous 15.36 Msps hardware stream | All five host tasks merged; live acquisition, MIB, and full-band handoff now demonstrated; long continuity and original target matrix remain |
| 4 | Measured synchronization and CFO correctness | All five host tasks merged; RS feedback is wired; RS-CFO estimator now has focused coverage; direct frame-boundary, drift, clipping, integer-CFO, and real-fixture/reference proof remain |
| 5 | Stage-by-stage PBCH/MIB agreement with the reference decoder | Implementation wave finalized; PBCH stage capture, positive fixture consistency, PBCH reset, and RS-CFO tests merged; 17/17 focused tests pass; independent CellSearch comparison, synthetic PBCH vectors, binary-level replay, fixture-level SFN progression, and four-frame positive combining remain |
| 6 | Shared Eye/GUI recovery without stale worker output | Partial: recovery triggers, generation tagging, stale-result discard, and a 60s recovery timeout are implemented; 19/19 focused tests pass; a real long recovery counterexample, GUI automation, and GUI telemetry remain open |
| 7 | Known-good DCI plus cold-start and endurance proof | First multi-second live DCI/RNTI candidate output recorded in CLI and GUI; known-grant fixture, cold-start matrix, skip-rate fix, and endurance remain |
What comes after the stream stays still
Gate 4 now has evidence instead of only a plan. Its merged diagnostics log CP, PSS, integer, and fused CFO separately with explicit signed-hertz semantics. A synthetic 50-RB cell with +2,500 Hz injected CFO is recovered at +2,367.6 Hz, an error of 132.4 Hz against a 500 Hz limit. The same harness recovers cell 163 / 50 RB and cell 467 / 25 RB, survives a 1,000-sample prepend, and recovers the full SSS-derived identity for cell 163. The prepend result proves acquisition survives that offset; it does not measure the recovered frame boundary.
The threshold result is an operating point, not a magic constant. The positive synthetic fixture survives deterministic AWGN through standard deviation 1.0 and stops decoding at 1.5; a noise-only fixture at 1.5 is rejected. Because that fixture contains long zero regions, repeats the same noise, and uses one seed per point, it is not a calibrated sensitivity threshold. A second test requires eight agreeing PSS/SSS confirmations. With a second synthetic cell 10 dB weaker, cell 163 remains the winner; at 10 dB stronger, the interferer legitimately wins as cell 200 instead of producing a garbage identity. After the first Gate 6 recovery tests, all 19/19 focused tests pass on master.
Task 5 is merged too. Each completed worker can now return a cell-specific reference-signal residual-CFO estimate, and FalconEye feeds the subframe-5 result into srsLTE's bounded correction loop once per LTE frame. Review caught an important pipeline error before merge: the first version gated feedback on the receiver's current subframe index instead of the completed worker's own, even though threaded work can lag input. The successful live run entered this post-MIB path, but there is no isolated log or automated assertion proving what the RS correction contributed. It did not cause the first MIB; the feedback is only reachable after MIB acceptance.
The caveats are still as important as the green count. The synthetic fixtures still contain synchronization and reference signals, not PBCH. CP and integer CFO are structurally disabled in the current path, and fused CFO is loop state rather than an arithmetic sum of every diagnostic field. Sampling drift and clipping injection exist without measured tolerance; frame-boundary recovery is still not directly asserted. Gate 5 removed one parked RS-CFO coverage gap, but it did not magically turn the synthetic sync suite into a calibrated live-RF proof.
For Gate 7, the live DCI run is strong evidence but not known ground truth: a commercial scheduler does not disclose the correct RNTI/grant set in advance. The next useful reference result is still external: run FALCON and CellSearch against the same carrier, record the matching cell ID / PRB / port story, then keep moving toward a full-band known-grant fixture.
The run also exposed the next practical defect. FalconEye skipped 261 subframes, 6.56% of the attempted total, because no worker was available, and the receive counters recorded three overflows. The decoder still produced multi-second control-channel candidate output, but calling that loss-free or endurance-ready would be wrong. Gate 6 now has the first recovery machinery and stale-result controls, but its live counterexample keeps the gate open; Gate 7 still asks for known grants, 100-of-100 cold starts, bounded recoveries, and a one-hour run.
FALCON now reaches every stage of the live chain through a HackRF Pro from both the CLI and the GUI: cell search, synchronization, two agreeing MIBs, PDCCH/DCI candidate processing, and reported RNTI candidates. The remaining work is to validate the control-channel output independently, then make the result deterministic, loss-aware, and repeatable for an hour.
The next useful result is a full-band capture of the 2147.5 MHz carrier with enough metadata to replay its known-live control-channel behavior, compare it against a reference decoder, and measure the worker-pool loss instead of inferring around it. The local 1.92 Msps fixture is a real start, but it preserves acquisition and MIB evidence rather than the entire 75-PRB DCI path.
← Back to lab notes