Repository navigation
cell M6: UE measurement gaps — the A3 on the UE's own measurements (inter-frequency trigger) - #533
Conversation
…ents, the inter-frequency trigger src/cell/MeasGap.h: - the NL IE: the neighbour list as offsets on the serving grid, (phase_n - phase_s) mod P on the shared timebase; - the MR IE: the UE's filtered beacon RSSI/SNR per cell; - both carry a CRC and the cell key's tag; - gap_window(): retune + beacon + retune, clear of the serving beacon and the UE's own DL/UL slots, or none. F1 v7: Neighbours (CU -> DU, every round) and Report.ue_meas. DU: airs the NL IE (from the CU's list, on its own grid) and relays its UEs' MR reports with ages. UE: L3-filters every cell beacon it hears and parses the NL IE. Every CELL_UE_GAP_SF it measures an other-channel neighbour in a gap if the window fits (cell.ue.gap / gap_skip), and reports in the MR IE every CELL_UE_MR_MS. CU: for a UE that reports, the A3 compares serving and neighbour beacons (metric ue_rssi). Rig (testrig m6_measgap.sh, 5 runs, ch149 / ch157 at phase 60000, DEFAULT A3 thresholds, nothing forced): all pass. - gaps 53.9 ms median, the neighbour heard at -46 dBm vs a serving -56 dBm; - the A3 fired on UE reports, and the inter-frequency handover executed; - the gaps cost no uplink. The TSF-continuity assertion is now exact: the switch is at the network instant eff_target * P - ((phase_t - phase_s) mod P) plus the poll latency (21 ms median). ctest 92/92. docs/cell-m6.md. Refs #260 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PR Summary by QodoTrigger inter-frequency handovers from UE measurement gaps
AI Description
Diagram
High-Level Assessment
Files changed (6)
|
Code Review by Qodo
1.
|
UE gaps: - planned with a conservative retune bound for both retunes (CELL_UE_GAP_RETUNE_US floor, raised by any slower retune measured); - no gap across bands; - skipped on a late wake-up; - listening cut short when the outbound retune overruns; - a failed return is retried, then the beacon stops and the normal retune path recovers (cell.ue.gap_return_fail); - neighbours that never fit are parked; - each measurable neighbour is revisited every CELL_UE_GAP_SF; - TSF steps wait 4 superframes after a gap: the only uplink losses seen with gaps followed one with a step; the mechanism is not established. Freshness, IE v2: - NL and MR carry a tagged generation; UEs refuse an older NL, and DUs an MR not newer or far from their superframe; - MR entries carry their sample age, added to the report age. - A synced DU airs an empty NL too. UEs drop a list not heard for 3 s, and clear it on a handover. Provenance: a beacon is measured only if it carries that cell's valid NL IE. The residual risk (a replayed genuine beacon) is stated. CU: per neighbour, one comparable pair (the UE's or the DUs'), with the A3 run on margins. F1: ue_meas cap kMaxUeMeas = 216, serving entry first, whole UEs only, truncation logged. Rig (m6_measgap.sh, 8 runs, default A3): all pass, 21 gaps with no uplink lost. The same-channel m6_handover regression passes. ctest 92/92. The docs state the stationary-UE limit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Qodo review addressed in 7581772:
Also from the rig: TSF steps wait 4 superframes after a gap. The only uplink losses seen with gaps followed one with a step; the mechanism is not established, and 8 runs with 21 gaps lost none with the hold. Rig: |
|
CI green. Qodo shows no bugs, rule violations or requirement gaps; the two remaining items are the high-level 'alternative approaches' (blind neighbour scanning; DU-reported uplink only), and Qodo's own recommendation is the phase-based, slot-safe gaps this PR implements. The limitation it names (layouts where no gap fits) is the CU phase planning that follows in the next PR. Merging with skip-qodo-gate. |
…sure each other (#534) M6 (#260, EPIC #254): the CU places a **joining** DU's TBTT on the shared PTP timebase so that its UEs and the running cells' UEs can measure each other in gaps (#533). Running cells keep their phases. ## What - **`src/cell/PhasePlan.h`** (new) — `plan_phase(joining, existing, params)`: - another channel: per candidate phase, the share of each cell's capacity positions that fit a `gap_window` for the other's beacon, both ways (pair score up to 2); - same channel: 2 for disjoint active spans, 1 if only the beacons are clear; - best total over a 100 µs grid, then the middle of the widest run of equal scores; - `phase_cell()` builds a cell from a Report's layout. - **F1 v8 `Type::Phase`** (`PhaseAssign{du, phase_us, seq}`), tagged, with the request's seq echoed back. - **DU** (`CELL_PHASE_AUTO=1`): - before starting its beacon, it reports `phase_us = -1` with its grid, every 500 ms; - it takes only a CU-addressed, tagged answer to its own run's requests (`cell.phase.assigned`); - otherwise, after `CELL_PHASE_WAIT_S`, it keeps `CELL_PHASE_US` (`cell.phase.default`); - `TsfDiscipline::set_phase()`. - **CU** (`tools/ran-controller`): - joining DUs are kept apart from the running ones and are in no neighbour list until they report a phase; - they are planned one at a time in id order, each waiting up to `RAN_PHASE_WAIT_S` for expected DUs not yet heard; - assignments are sticky; `ran.phase` logs the score per pair; - it plans with `RAN_GAP_RETUNE_US` / `RAN_GAP_GUARD_US`. - **UE gap bound defaults are now 26000 / 2000 µs**, down from 30000 / 3000. The 8852CE's retune measured 23.5–23.7 ms, so this is that plus about 10 %. At 30 + 3 ms, no phase is measurable both ways on the rig grid. - Selftests (cell_m6): - plan_phase on the rig grid: the both-ways window is [48500, 53900] and the plan picks 51200; - the 30/3 bound has no both-ways phase; - same channel gives disjoint spans; - a third cell gets a partial plan; - F1 Phase round-trip, forgery and truncation; - `set_phase`. - Docs: `docs/cell-m6.md` gains a "Phase planning" section. ## Rig (testrig `scripts/m6_phaseplan.sh`, 5/5 pass) Cell 1 is on ch149 at phase 0. Cell 2 is on ch157 with `CELL_PHASE_AUTO=1`. - **Phase:** DU2 was assigned **51200 µs, score 2/2, in every run**. - **Gaps:** both UEs heard the other cell in **568 of 570 gaps**, with no gap skipped and no uplink lost. - **Gap deaf time:** 54.1 ms median, down from 60.1 ms at 30/3. - **Regression:** `m6_measgap.sh` (inter-frequency handover on the UE's own measurements) passes 2/2 at the new bound. **Found on the way.** When the UEs were started a fixed 8 s after the DUs, the auto-phase DU was still waiting for its phase with its beacon on (the request then came after `StartBeacon`). The UE that locked early then saw one jump of about 1.3 ms in its beacon's timing. Its cumulative `TsfSync` fit took that as skew (hundreds to thousands of ppm), and its gaps missed (3 of 3 runs). This PR changes two things: - the DU now asks before its beacon starts; - the rig starts the UEs only once both DUs log `cell.du.start`. The source of the jump is not established. A UE-side reset of the fit on a detected discontinuity is listed as open in the docs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Part of M6 (#260), epic #254: UE measurement gaps, the inter-frequency handover trigger, on top of #532. With one radio per DU, no DU hears a UE on another channel, so the A3 needs the UE's own measurements.
src/cell/MeasGap.h:(phase_n − phase_s) mod P, so the UE knows when a neighbour's beacon airs from its serving clock alone.gap_window(): retune + beacon + retune, clear of the serving beacon and the UE's own DL/UL slots; otherwise the gap is skipped.Neighbours(CU → DU, every round) andReport.ue_meas.CELL_UE_GAP_SFit measures an other-channel neighbour in a gap, if one fits, and reports every second.ue_rssi).Rig (testrig
scripts/m6_measgap.sh, 5 runs; ch149 and ch157, cell 2 at phase 60000 µs; default A3 thresholds, nothing forced): all pass.eff_target·P − ((phase_t − phase_s) mod P)plus the UE's poll latency. All earlier rig sets re-verify under it.ctest 92/92. Limit: a gap goes only where it clears the UE's slots, so in this layout cell 2's UE cannot measure cell 1 (logged
gap_skip).🤖 Generated with Claude Code