Skip to content

cell M6: inter-frequency handover (UE retunes at the switch) - #532

Merged
josephnef merged 2 commits into
masterfrom
feat/m6-interfreq
Nov 5, 2026
Merged

josephnef merged 2 commits into
masterfrom
feat/m6-interfreq

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Part of M6 (#260), epic #254: different-channel handover, on top of #530 and #531.

  • Command and IE: HoCommand.target_channel comes from the target DU's report, and HoIe.channel travels with it (IE v2, F1 v6).
  • UE: at the switch it shifts its clock by the TSF delta (no target beacon needed first), takes the target identity, and stops its uplink beacon → retunes (FastRetune) → restarts it on the new channel. It logs the retune time and the first serving beacon after it.
  • Deadline to return: with no target grant within CELL_HO_FALLBACK_SF, the UE retunes back to the source, which still holds it. This bounds the deaf time.
  • Trigger: with one radio per DU, no DU hears a UE across channels, so the A3 has no neighbour value to compare. UE measurement gaps are the next step. Until then, RAN_HO_FORCE=SRC:UE:TGT@SECONDS forces one handover through the normal execution path.

Rig (testrig scripts/m6_handover.sh --channel2 157: ch149 → ch157, 5 normal + 3 runs with the target killed after staging; all pass):

KPI min median max
FastRetune (8852CE, 11 retunes) 23.5 ms 23.6 ms 23.7 ms
first target beacon after the retune 37 ms 52 ms 101 ms
interruption (sf, first in-slot uplink at the target) 2 2 3
target crash: fallback / source uplink missed (sf) 10 / 10 10 / 10 10 / 10

The switch lands on the target's effective superframe in every run: clock continuity holds across the channel change. ctest 92/92. Details in docs/cell-m6.md.

🤖 Generated with Claude Code

…nd, the UE retunes at the switch

- HoCommand.target_channel (from the target DU's report) and HoIe.channel
  (IE v2); F1 v6 carries it.
- UE: at the switch, as on the same channel, it shifts the clock by the TSF
  delta (no target beacon needed first), takes the target identity and
  runs the target's map. In addition it stops its uplink beacon, retunes
  (FastRetune) and restarts the beacon on the new channel.
- Logged: cell.ue.retune (retune_us), and cell.ue.retune_first_beacon (the
  deaf time to the first serving beacon).
- Fallback (no target grant within CELL_HO_FALLBACK_SF) retunes back: the
  bounded deadline to return to the serving channel.
- CU: RAN_HO_FORCE=SRC:UE:TGT@SECONDS forces one handover through the
  normal execution path. No DU hears a UE across channels until UE
  measurement gaps exist, so the A3 has nothing to compare.

Rig (testrig m6_handover.sh --channel2 157: ch149 -> ch157, 5 normal + 3
target-crash): all pass.
- the switch lands on the target's effective sf in every run;
- FastRetune 23.5/23.6/23.7 ms (min/median/max);
- first target beacon 37/52/101 ms after the retune;
- interruption 2/2/3 sf;
- crash fallback 10 sf, with 10 sf of source uplink missed.
ctest 92/92. docs/cell-m6.md.

Refs #260

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Enable inter-frequency handover with UE retuning and fallback

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Carry the target DU’s channel through F1 and the beacon handover IE.
• Retune the UE at the switch and return to the source if no target grant arrives.
• Add a forced test trigger and document inter-frequency rig results.
Diagram

sequenceDiagram
    participant CU as RAN controller
    participant TDU as Target DU
    participant SDU as Source DU
    participant UE as UE scheduler
    participant R as UE radio
    TDU->>CU: Report target channel
    CU->>TDU: Stage handover command
    TDU-->>CU: Staged
    CU->>SDU: Command with channel
    SDU->>UE: Beacon HoIe v2
    UE->>UE: Shift TSF and switch identity
    UE->>R: Stop beacon, retune, restart
    alt Target grant arrives
        TDU-->>UE: Target beacon and grant
    else No grant by deadline
        UE->>R: Retune to source channel
        SDU-->>UE: Resume source service
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Implement UE measurement gaps before inter-frequency handover
  • ➕ Enables automatic neighbor measurements and an A3-based trigger across channels.
  • ➖ Adds scanning and reporting work beyond this PR; delays validation of the switch and fallback path.

Recommendation: Use the existing handover execution path with a forced trigger for this milestone: it isolates and tests channel switching on the current single-radio setup. Treat measurement gaps as required follow-up for automatic inter-frequency handover, not as something RAN_HO_FORCE replaces.

Files changed (6) +168 / -18

Enhancement (4) +106 / -12
main.cppRetune the UE at handover and fallback +49/-1

Retune the UE at handover and fallback

• The source DU includes a differing target channel in the beacon IE. At the switch, the UE stops its uplink beacon, retunes, and restarts it; if the target grants nothing by the deadline, it retunes to the saved source channel. New events measure retune duration and time to the first serving beacon.

examples/cell/main.cpp

F1.hCarry the target channel in F1 handover commands +4/-1

Carry the target channel in F1 handover commands

• Bumps F1 to version 6 and adds target-channel encoding and decoding to the handover command.

src/cell/F1.h

HoCommand.hAdd channel fields to handover commands and beacon IEs +15/-6

Add channel fields to handover commands and beacon IEs

• Adds target-channel fields and bumps the handover IE to version 2. Updates its encoded length and decode offsets while retaining CRC and tag validation.

src/cell/HoCommand.h

main.cppPopulate target channels and support forced handovers +38/-4

Populate target channels and support forced handovers

• Copies the target DU’s reported channel into each handover command. Adds the one-shot RAN_HO_FORCE trigger to exercise the normal execution path when cross-channel A3 measurements are unavailable.

tools/ran-controller/main.cpp

Tests (1) +5 / -4
cell_m6_selftest.cppVerify channel round-trips through handover messages +5/-4

Verify channel round-trips through handover messages

• Updates F1 and beacon-IE fixtures for the added channel byte, checks the decoded target channel, and preserves same-channel replay coverage.

tests/cell_m6_selftest.cpp

Documentation (1) +57 / -2
cell-m6.mdDocument inter-frequency switching and measured outcomes +57/-2

Document inter-frequency switching and measured outcomes

• Explains how the channel reaches the UE, how retuning and fallback work, and why testing currently needs a forced trigger. Records rig results and an unexplained source-beacon observation.

docs/cell-m6.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. A failed beacon stop can disrupt service ✓ Resolved
Description
The inter-frequency path ignores StopBeacon()'s result, marks beaconing false, and proceeds to
retune. If the stop fails while the hardware beacon is still active, the UE may carry its source
beacon across the channel change and subsequently attempt to start a second beacon instead of
recovering the active one.


Remediation recommended

2. A busy cell loses the forced handover ✓ Resolved
Description
RAN_HO_FORCE sets force_done and emits ran.ho_forced before the flight, target, capacity, and
execution checks create a command. If the source or target is busy, the target is unavailable or
full, or execution is disabled at the configured time, the request is consumed without a handover
and is never retried.

3. A stopped target can receive a command ✓ Resolved
Description
The forced target lookup accepts any retained DU report with net_sf > 0, without checking when
that target last reported. When the target stops reporting but remains in dus, its old capacity,
channel, and clock data can be used to construct a handover that it cannot currently stage.

4. A retune failure terminates the UE ✓ Resolved
Description
run_ue calls FastRetune() without handling an exception from channel configuration. On a radio
whose retune throws, control leaves the loop before it stops the receive thread and joins it, so the
UE cannot perform the promised fallback or orderly cleanup.

View review recommended (1)
5. Oversized cell IDs force the wrong cell ✓ Resolved
Description
RAN_HO_FORCE parses cell and UE identifiers as unsigned and narrows them to uint16_t without
checking their range. A value such as source 65537 becomes cell 1, so a mistyped setting can
select and hand over a different real UE instead of being rejected.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

- UE: the uplink beacon must stop before a retune (StopBeacon checked,
  retried; the retune waits for it, bounded by the fallback deadline).
  A FastRetune exception is caught (cell.ue.retune_fail): the channel
  stays, and the fallback resolves it.
- CU: RAN_HO_FORCE is consumed only once its command is in flight
  (pending while busy, full or stale). A handover target's report must be
  fresh (3 s). Ids are range-checked to uint16 and the time to finite
  0..86400 s.

Rig regression (m6_handover.sh --channel2 157, 2 normal + 1 crash): pass.
ctest 92/92.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@josephnef

Copy link
Copy Markdown
Collaborator Author

Qodo review addressed in 1f8114d:

  1. Beacon stop: StopBeacon() is checked and retried, and the retune and the beaconing flag wait until the stop succeeds (bounded by the fallback deadline).
  2. Forced handover: RAN_HO_FORCE is consumed, and ran.ho_forced emitted, only once its command is in flights. It stays pending while busy, full or stale.
  3. Stale target: the target's report must be fresh (3 s) before a command is built from it, forced or not.
  4. Retune exception: a FastRetune exception is caught and logged (cell.ue.retune_fail). The channel is left unchanged, the fallback resolves it, and the loop and its cleanup continue.
  5. Id ranges: ids are checked to 1..65535 and the time to a finite 0..86400 s.

Rig regression: 2 normal + 1 target-crash inter-frequency runs pass. ctest 92/92.

@josephnef

Copy link
Copy Markdown
Collaborator Author

All 5 Qodo findings resolved. The remaining item, 'Implement UE measurement gaps before inter-frequency handover', is Qodo's alternative-approach note. It is the next PR, already rig-tested on its branch: the A3 triggering this inter-frequency handover on UE-reported gap measurements. Merging under skip-qodo-gate.

@josephnef josephnef added the skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision) label Nov 5, 2026
@josephnef
josephnef merged commit 7c58fb1 into master Nov 5, 2026
27 of 28 checks passed
@josephnef
josephnef deleted the feat/m6-interfreq branch November 5, 2026 19:25
josephnef added a commit that referenced this pull request Nov 5, 2026
…nter-frequency trigger) (#533)

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`:**
- **NL IE:** the neighbour list as offsets on the serving grid,
`(phase_n − phase_s) mod P`, so the UE knows when a neighbour's beacon
airs from its serving clock alone.
  - **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; otherwise the gap is skipped.
- **F1 v7:** `Neighbours` (CU → DU, every round) and `Report.ue_meas`.
- **DU:** airs the NL IE and relays its UEs' MR reports.
- **UE:** filters every cell beacon it hears. Every `CELL_UE_GAP_SF` it
measures an other-channel neighbour in a gap, if one fits, and reports
every second.
- **CU:** the A3 on UE reports compares serving and neighbour beacons
(metric `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.

| | min | median | max |
|---|---|---|---|
| gap deaf time (14 gaps) | 53.7 ms | 53.9 ms | 56.6 ms |
| neighbour in gaps vs serving | −46 vs −55..−57 dBm | | |
| switch after the network instant | 21 ms | 21 ms | 28 ms |
| interruption (sf) | 3 | 3 | 3 |

- **Every run:** the A3 fired on UE-reported values, the inter-frequency
handover executed, and the gaps cost no uplink.
- **TSF continuity:** now asserted exactly. The switch is at the network
instant `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](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant