Skip to content

cell M6: the conditional handover — a source lost before the command - #574

Merged
josephnef merged 2 commits into
masterfrom
feat/m6-cho
Nov 13, 2026
Merged

josephnef merged 2 commits into
masterfrom
feat/m6-cho

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

#566 item: a UE whose source fades before the handover command loses what it misses before the cross-channel DAPS visits begin.

The problem, measured first. A command reaches the UE only through its source's beacon. With cell 2 on another channel and DU1 killed before any command, UE1 was stranded: no switch, and no DL for the rest of the run (30–33 % of what UE2 got over the same run).

What this adds. A conditional handover, RAN_CHO=1, on F1 v18, HoIe v4 and HoCommand.h ChoCondition.

  • CU: the A3 or RAN_HO_FORCE prepares the move. The command's instant becomes the expiry (RAN_CHO_WINDOW_SF [300]), and the command carries the UE's condition: RAN_CHO_LOST_SF [4], RAN_CHO_MARGIN_DB [3] and RAN_CHO_TTT_MS [640]. ran.ho_done fires when the target hears the UE, at any time before the expiry.
  • Target DU: publishes the arriving UE's grant at once, not at an instant. Its silence clock starts at the expiry, so a UE that never came is released after it.
  • Source DU: relays the conditional fields in the IE. Its release logic is unchanged, keyed to the expiry.
  • UE: switches on source_lost (no serving beacon for N superframes) or target_better (its own filtered level of the target ahead by the margin for the hold time), logging cell.ue.cho_exec. Without either by the expiry it stays (cell.ue.cho_expired). Measurement gaps continue during a conditional preparation, and the DAPS visits run from the preparation.
  • Tests: cell_m6 covers the v4 IE round-trip and tamper check, ChoCondition (margin, hold, restart, loss threshold) and the F1 v18 round-trip.
  • Docs: new docs/cell-m6.md section "The conditional handover"; the "Not covered yet" list is updated.

Rig (testrig scripts/m6_cho.sh, 2 runs per case, cross-channel, all pass)

case move DL new PDUs vs UE2 longest DL silence uplinks lost
prepared, then the source killed source_lost, 4 sf after its last serving beacon 1.007, 1.008 248, 205 ms 1, 0
no preparation, the source killed none: stranded 0.304, 0.328 — —
prepared, no fault target_better, 8–9 sf after the preparation 1.006, 0.999 167, 205 ms 0, 0
prepared, margin never met lapsed; the target released, cell 1 kept the UE 1.008, 1.003 205 ms 1, 0

Between the kill and the move, the visits kept every uplink on the target and its answers carried the DL. The remaining 205–248 ms silence is the stream's own cadence.

Not settled here: the rig forces the preparation. No policy for the A3 preparation offset, or for re-preparing after a lapse, has been tuned.

ctest: 95/95 pass.

🤖 Generated with Claude Code

…566)

RAN_CHO=1 (F1 v18, HoIe v4, HoCommand.h ChoCondition): the CU prepares
the move early (the A3 or RAN_HO_FORCE), the target grants the UE from
the command, and the UE switches on its own condition: its serving
beacons absent RAN_CHO_LOST_SF superframes, or its own measurement of
the target ahead by RAN_CHO_MARGIN_DB for RAN_CHO_TTT_MS. None by the
expiry: the UE stays, the target releases. Gaps go on through a
conditional preparation; the DAPS visits run from it.

Rig (testrig m6_cho.sh, 2 runs per case, cross-channel): source killed
after the preparation, the UE moves 4 superframes after its last serving
beacon with its DL intact (1.007-1.008 of UE2's PDUs, 0-1 uplinks lost);
without it the UE is stranded (0.30-0.33).

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

Add conditional handover before source loss

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Prepare handovers early so a UE can switch after source loss or a sustained target advantage.
• Keep target grants, measurement gaps, and DAPS visits active during the preparation window.
• Extend handover protocols and tests; document cross-channel results and remaining
 preparation-policy work.
Diagram

graph TD
  CU["CU preparation"] --> SRC["Source DU"] --> UE["UE staging"] --> COND{"Switch condition?"}
  CU --> TGT["Target DU"]
  COND -->|met| MOVE["UE switch"] --> TGT --> DONE["CU completion"]
  COND -->|expires| LAPSE["Preparation lapses"] --> TGT
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. UE-initiated neighbor reattachment
  • ➕ Could recover even when no handover was prepared before source loss.
  • ➖ Requires a new discovery and session-recovery path.
  • ➖ Cannot preserve the same pre-staged grant and DAPS continuity.

Recommendation: Use the conditional preparation for continuity when the target is known: it builds on the existing handover and DAPS paths rather than adding a recovery protocol. UE-initiated reattachment is a separate fallback worth considering for losses that occur before preparation; A3 preparation and re-preparation policy remain untuned.

Files changed (6) +314 / -21

Enhancement (4) +187 / -17
main.cppStage conditional handovers across DU and UE paths +72/-9

Stage conditional handovers across DU and UE paths

• The target DU publishes arriving grants without waiting for a switch instant, while the source relays the condition fields. The UE evaluates beacon loss or measured target advantage, continues gaps during preparation, and switches or lets the command expire.

examples/cell/main.cpp

F1.hCarry conditional fields in F1 v18 handover commands +10/-2

Carry conditional fields in F1 v18 handover commands

• Increments the F1 version and extends handover command encoding and decoding with loss, margin, and hold-time parameters.

src/cell/F1.h

HoCommand.hDefine conditional commands, beacon IEs, and UE decision logic +75/-2

Define conditional commands, beacon IEs, and UE decision logic

• Adds conditional fields to handover commands and the authenticated v4 beacon IE. Introduces ChoCondition to evaluate serving-beacon loss and sustained target signal advantage.

src/cell/HoCommand.h

main.cppConfigure early preparation and recognize conditional arrivals +30/-4

Configure early preparation and recognize conditional arrivals

• Adds RAN_CHO and its expiry and switch-threshold settings, using the configured window as the command expiry. Concludes a conditional flight when the target reports the UE rather than waiting for the expiry instant.

tools/ran-controller/main.cpp

Tests (1) +48 / -2
cell_m6_selftest.cppTest conditional protocol fields and switch thresholds +48/-2

Test conditional protocol fields and switch thresholds

• Covers v4 IE round-trip and tamper rejection, F1 v18 command round-trip, and the margin, hold, restart, and beacon-loss decisions.

tests/cell_m6_selftest.cpp

Documentation (1) +79 / -2
cell-m6.mdDocument conditional handover behavior and rig results +79/-2

Document conditional handover behavior and rig results

• Explains preparation, UE switch conditions, expiry, and DAPS behavior. Records cross-channel results and identifies preparation policy as unresolved.

docs/cell-m6.md

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

qodo-free-for-open-source-projects Bot commented Nov 13, 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. Expired preparations can still move the UE ✓ Resolved
Description
run_ue evaluates ChoCondition::update() before checking effective_sf, and its expiry branch
runs only when the condition returns None. If source loss or the target-level hold first satisfies
the condition at or after expiry, the UE switches even though the preparation has lapsed.

2. The CU can confirm a UE that never moved ✓ Resolved
Description
main treats any live target measurement with the reused target_ue number as completion of a
conditional flight, without tying that measurement to the current preparation. If a previous
occupant’s measurement remains valid after its target assignment is freed, the CU can mark the new
flight done and move its downlink state before that UE switches.


Remediation recommended

3. A dead target can attract the UE ✓ Resolved
Description
run_ue feeds cached RSSI values from ue_meas.snapshot() into the new target-better hold timer
without requiring a recent target beacon. If the target stops transmitting after a favorable
measurement, its value remains valid for the measurement-staleness interval, so the hold can finish
and switch the UE to that target; a previously cached grant map can then clear the handover
fallback.

4. Delayed passes postpone source-loss moves ✓ Resolved
Description
run_ue records serv_bcn_sf as the current main-loop superframe when it notices a changed
reception counter, rather than the superframe in which the RX callback heard the beacon. If a
measurement gap or DAPS visit delays that pass across a superframe boundary, ChoCondition starts
its configured loss interval too late.

5. A departed UE gets a fresh downlink stream at its old cell ✓ Resolved
Description
For a conditional command, the source DU only releases the UE once sf >= eff_sf + ho_release_sf,
and eff_sf is now the expiry (RAN_CHO_WINDOW_SF, 300 superframes, about 30 s). The CU,
meanwhile, fires ran.ho_done as soon as the target hears the UE and keeps the source identity in
retired for only 6 s. If the UE moves early (the rig's target_better case moves 8–9 superframes
after the preparation), the source keeps reporting it in rep.ues for about 25 s after retired
lapses. During that time the CU's DL loop sees no stream, no retirement and no flight for `{source,
ue}. It creates a new ran.dl_stream` and sends PDUs to the source DU for a UE that has already
left. The A3 also restarts for the old identity, and the source keeps the departed UE's slot and
keeps airing its handover IE for the whole window.

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

…a fresh target level only; commit on a beacon of the new cell; the source releases a silent conditional UE; the CU needs a post-command target sample

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

Copy link
Copy Markdown
Collaborator Author

All five findings were valid. Fixed in fb286c5d:

  1. Expiry first. ChoCondition::update now returns Expired at or past the expiry before it looks at either condition. The selftest covers a source silent 10 superframes at the expiry superframe, and an ungranted UE past it: both are Expired.
  2. A post-command sample. A flight concludes only on a target measurement sampled after the command was issued: HoFlight::at against report time minus age_ms.
  3. A dead target. The target's level feeds the hold only while fresh, within twice the gap revisit period and never under a second; the selftest covers a pass without a fresh level restarting the hold. Separately, any switch now commits on the target's grant only after a beacon of the new serving cell has been heard since the switch, so a cached map is not enough.
  4. Source loss timed at RX. The serving beacon's superframe is taken from its own departure TSF in the RX path, not from when the loop notices it.
  5. The departed UE. A conditional source releases the UE once it has been silent CELL_HO_RELEASE_SF superframes, not at the expiry, and stops airing the IE. On the rig it released 22 superframes after the switch, and the CU opened no second DL stream at cell 1. A UE released while still there is no longer granted, which now counts as source_lost.

Rig, on fb286c5d deployed to all three boxes:

  • m6_cho.sh --rounds 2: all 8 pass. A lost source is followed by a source_lost move 4 superframes after the last serving beacon, with the UE getting 1.002–1.015 of UE2's DL.
  • Regressions: m6_handover.sh (2 normal, 1 fault) and m6_daps_xch.sh (1 run per case) pass.

ctest: 95/95 pass.

@josephnef josephnef added the skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision) label Nov 13, 2026
@josephnef
josephnef merged commit 1fb6c1a into master Nov 13, 2026
30 of 31 checks passed
@josephnef
josephnef deleted the feat/m6-cho branch November 13, 2026 19:57
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