Skip to content

cell M6: DAPS across channels — sequential uplink visits to the target's channel - #558

Merged
josephnef merged 2 commits into
masterfrom
feat/m6-daps-xch
Nov 10, 2026
Merged

josephnef merged 2 commits into
masterfrom
feat/m6-daps-xch

Conversation

@josephnef

@josephnef josephnef commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #260 (epic #254). Closes "DAPS across channels" for the uplink. The #557 crossing measured the gap: 12–14 uplinks lost from a source kill to the switch, because the target cannot hear the source identity on another channel.

What

  • UE visits (examples/cell, CELL_UE_DAPS_VISIT [1]):
    • between accepting an inter-frequency handover and its switch, the UE visits the target's channel once a superframe, in its idle time after its uplink slot;
    • it airs a copy of its uplink beacon at an instant clear of the target cell's active span (from the HoIe TSF delta), holds the channel 4 ms (send_packet only queues; no TX report on Kestrel), and retunes back before its serving beacon;
    • during the window the UE loop runs every 30 ms, so every superframe gets its visit.
  • Seq mapping (F1 v14, Handover.h ul_seq_for_copy): the DU forwards the copy with its reception instant (Uplink.net_us). The CU maps a copy heard by another cell's DU to the UE's last own uplink slot, from the source phase and the UE's position in the reports, so a visit dedups with the source's copy. The UE needs no global clock.
  • Step hold cleared at the switch: a hold set before the switch counts in the source's superframes. A visit just before the switch kept the uplink 40 ms off for 9 superframes, and the target's TA loop took that for delay (~12 s to settle).
  • Observability: cell.daps.fwd (DU) and ran.ul_copy (CU, raw vs mapped seq).
  • Selftests: the mapping (at the slot, timing advance, a visit 40 ms later, the next slot, phase 0) and the v14 round trip. ctest 95/95.

Review fixes (later commit): the whole send clear of the target's span; late or overrun visits skipped and logged; the CU maps every copy (incl. the serving DU's) from the identity's last-known slot; a visit holds steps only through its own superframe.

Rig (testrig scripts/m6_daps_xch.sh, 2 runs per case, all pass)

uplinks lost at the CU before the switch at the switch
source killed at the accept, visits 0, 0 1, 2
source killed, no visits 15, 15 2, 2
no fault, visits 0, 0 2, 1
  • Visits: 20–21 per handover, all sent and back, none skipped, 55.6–56.8 ms deaf each.
  • No cost to the source: without a fault, DU1 heard every uplink in the window, in slot 100 %.
  • Target and DL: the target in slot 99.9 %; DL DAPS lost no PDU.
  • The switch's own two superframes: the switch superframe, and the first uplink after the beacon restart on the target. The latter is the same effect seen after suspend gaps; its cause is not established.
  • Regression: inter-frequency measgap, same-channel DAPS, plain and fault handovers, and the three-cell crossing pass.

🤖 Generated with Claude Code

…t's channel

Uplink DAPS needs the target to hear the UE's source identity, which it
cannot on another channel: with the source killed mid-handover, every
uplink from the kill to the switch was lost.

- UE (CELL_UE_DAPS_VISIT [1]): between accepting an inter-frequency
  handover and its switch, the UE visits the target's channel once a
  superframe, in its idle time after its uplink slot. It airs a copy of its
  uplink beacon clear of the target's active span, holds 4 ms
  (send_packet only queues), and retunes back. The loop runs every 30 ms
  in that window.
- F1 v14: Uplink carries its reception instant (net_us). The CU maps a copy
  heard by another cell's DU to the UE's last own uplink slot
  (Handover.h ul_seq_for_copy) from the source's phase and the UE's
  position, so a visit dedups with the source's copy.
- The switch clears the TSF step hold. A hold set before it counts in the
  source's superframes: a visit just before the switch left the uplink
  40 ms off for 9 superframes, and the target's TA loop took ~12 s to
  settle.
- DUs log each forwarded other-identity copy (cell.daps.fwd); the CU logs
  each mapped copy (ran.ul_copy).

Rig (testrig m6_daps_xch.sh, 2 runs per case, all pass), source killed at
the accept:
- with visits, 0 uplinks lost before the switch (no visits: 13);
- 2 lost at the switch itself;
- without a fault, the visits cost the source nothing.
The measgap, same-channel DAPS, handover and three-cell crossing runs pass.

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 sequential uplink DAPS across channels

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Let UEs visit the target channel before an inter-frequency switch, preserving uplinks if the
 source fails.
• Timestamp forwarded uplinks and map target-heard copies to source slots for CU deduplication.
• Clear switch-time timing holds; add visit telemetry, mapping tests, and rig results.
Diagram

sequenceDiagram
    actor UE
    participant Source as Source DU
    participant Target as Target DU
    participant CU as RAN controller
    participant UL as UL dedup
    UE->>Source: Scheduled uplink
    Source->>CU: Forward uplink
    UE->>Target: Retune and copy
    Target->>CU: Forward timestamped copy
    CU->>CU: Map to source slot
    CU->>UL: Deliver mapped sequence
    UE->>Source: Retune before beacon
    Note over UE: Switch clears step hold
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. UE-supplied logical uplink sequence
  • ➕ Would not depend on a current source DU report for slot mapping.
  • ➖ Requires beacon and protocol changes and trusting UE-provided sequencing rather than network reception time.

Recommendation: Keep the network-time mapping: it builds on existing DU timing and preserves the UE beacon format. Review behavior when the source report or uplink position is unavailable, since the CU then retains the forwarded raw sequence.

Files changed (6) +251 / -13

Enhancement (4) +163 / -8
main.cppAdd UE target-channel visits and DU forwarding telemetry +120/-7

Add UE target-channel visits and DU forwarding telemetry

• The UE copies its uplink beacon during a guarded target-channel visit before each eligible switch superframe, then retunes to the source; the switch also clears the source-grid step hold. The DU includes reception time in forwarded uplinks, and UE and DU logs report visit and forwarding activity.

examples/cell/main.cpp

F1.hAdd reception time to F1 uplink messages +8/-1

Add reception time to F1 uplink messages

• Bumps F1 to version 14 and adds the network-time reception instant to UplinkFwd encoding and decoding so the CU can identify the originating uplink slot.

src/cell/F1.h

Handover.hMap delayed copies to their source uplink slot +15/-0

Map delayed copies to their source uplink slot

• Adds a helper that derives a copy's sequence from the latest source-cell uplink slot at its reception time. A small early-arrival allowance covers timing advance.

src/cell/Handover.h

main.cppDeduplicate target-heard uplinks against source slots +20/-0

Deduplicate target-heard uplinks against source slots

• For copies forwarded by a different DU, the CU uses the source report's phase and UE uplink position to replace the raw sequence before delivery. It logs raw and mapped sequences with the deduplication outcome.

tools/ran-controller/main.cpp

Tests (1) +17 / -1
cell_m6_selftest.cppTest copy sequence mapping and F1 timestamp round trip +17/-1

Test copy sequence mapping and F1 timestamp round trip

• Covers at-slot and early reception, a later target visit, the next slot, and a zero-phase grid. Extends the uplink serialization round trip with the new reception timestamp.

tests/cell_m6_selftest.cpp

Documentation (1) +71 / -4
cell-m6.mdDocument cross-channel uplink DAPS and rig outcomes +71/-4

Document cross-channel uplink DAPS and rig outcomes

• Explains visit timing, CU sequence mapping, switch-time hold clearing, and reported fault and regression results. Narrows the remaining cross-channel DAPS limitation to target downlink reception before the switch.

docs/cell-m6.md

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

qodo-free-for-open-source-projects Bot commented Nov 10, 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


Remediation recommended

1. Visits can interfere with target traffic ✓ Resolved
Description
in_target checks only the planned send instant, although send_packet queues the copy and the UE
holds the target channel for another 4 ms. When the selected instant is just before the target's
next beacon, the queued copy can air during that beacon or its active slots.

2. Late copies can corrupt uplink counts ✓ Resolved
Description
The controller remaps a foreign copy only while the source DU's latest report still lists that UE,
leaving the forwarded raw sequence unchanged otherwise. After the source releases the UE, a delayed
old-identity copy can still arrive during the target's forwarding window; its raw
reception-superframe sequence can differ from the source-slot sequence used by the shared
deduplication stream.

3. Visit copies can mismatch the source's own sequence number ✓ Resolved
Description
ul_seq_for_copy sets a copy's seq to floor((k*P + phase + ul_start)/P), the nominal slot
instant. The source DU's own copy, which the CU does not remap, keeps
ul_seq_from_network_time(net_us), the actual reception instant. If `(phase_us + ul_start_us) mod
P` falls within the timing advance or jitter of a network-superframe boundary, the two copies of one
uplink get seqs one apart. Example: with the slot 200 µs after a boundary and the uplink heard 400
µs early, the visit copy of uplink n takes the seq the source gives uplink n+1. The CU then neither
dedups the pair nor keeps the right one: the visit of n arrives first and the source's real n+1 is
discarded as a duplicate.

View review recommended (1)
4. A late or slow visit can miss the serving beacon ✓ Resolved
Description
The visit sleeps until go, retunes, and sends without checking for a late wake or outbound-retune
overrun; unlike run_gap, it also leaves gap_retune_us unchanged despite measuring out_us and
the return retune. If either delay consumes the planned margin, the copy can enter the target cell’s
active span and the radio can return after the serving-beacon window or into the next superframe.


Informational

5. Timing steps stay frozen for the whole handover window ✓ Resolved
Description
Each visit sets no_step_until_sf = S + 4, and visits run every superframe from the accept to the
switch. The UE's TSF/timing-advance step is therefore suppressed for the entire DAPS window, about
20 superframes or 2 s on the rig. A TA or phase correction the source DU commands in that window is
not applied until the switch clears the hold, so the uplink to the source can drift out of slot
during the crossing.

Grey Divider

Tip of the day
💡 Did you know, you can move or hide resolved findings to focus on what still needs action

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

…target, late/overrun visits skipped, every copy mapped, short step hold

- A visit instant is taken only if the whole send (queued through the
  4 ms hold, with guards) is clear of the target's active span, across the
  superframe boundary.
- A late wake skips the visit. An outbound retune that overran the instant
  skips the send and retunes back (cell.ue.daps_visit_skip). Both retunes
  feed gap_retune_us.
- The CU maps every forwarded uplink, the serving DU's own copies
  included, from the identity's last-known slot (kept 30 s from the latest
  report naming it). Copies of one uplink share a seq even next to a
  network-superframe boundary, and a late copy after the source released
  the UE still maps. Selftest for the boundary case.
- A visit holds TSF steps only through its own superframe (not four, which
  froze them for the whole window); held steps are counted.

Rig (m6_daps_xch.sh 2 runs per case, all pass): with visits, 0 uplinks lost
before the switch (15 without), no visit skipped. Regression: same-channel
DAPS, the handovers, the two-cell crossing with a source failure, measgap
and the three-cell crossing with one all pass.

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

Copy link
Copy Markdown
Collaborator Author

Review addressed in the latest commit:

  1. Visits interfering with target traffic: a visit instant is accepted only if the whole send, from queuing through the 4 ms hold with 1 ms guards, is clear of the target's active span, across the superframe boundary.
  2. Late copies: the CU keeps each identity's last-known slot (phase, period, uplink start) from the latest report naming it, for 30 s. A copy arriving after the source released the UE still maps; there is no fallback to the raw seq.
  3. Seq mismatch at a boundary: the CU now maps every forwarded uplink, the serving DU's own copies included, so both copies of one uplink share a seq. New selftest: the slot 200 µs after a boundary, a serving copy 400 µs early and a visit 30 ms later give one seq, where the raw seqs differ.
  4. Late or slow visits:
    • a late wake (> guard/2) skips the visit without retuning;
    • after the outbound retune, an instant already passed skips the send and retunes straight back;
    • both retunes feed gap_retune_us;
    • skips are logged (cell.ue.daps_visit_skip).
  5. Frozen steps: a visit holds TSF steps only through its own superframe. cell.ue.done counts held steps: 2–10 per run.

Rig on this code:

  • m6_daps_xch.sh, 2 runs per case, all pass: 0 uplinks lost before the switch with visits (15 without), no visit skipped, the source unaffected, the target in slot 99.9 %.
  • Regression, all pass: same-channel DAPS, plain and fault handovers, the two-cell crossing with a source failure, measgap, and the three-cell crossing with a source failure.

@josephnef

Copy link
Copy Markdown
Collaborator Author

Applying skip-qodo-gate: Qodo posted its review as a PR comment (5 findings), not a review with threads, so the gate sees no review. All 5 are fixed and re-verified on the rig (m6_daps_xch.sh 2 runs per case, plus same-channel DAPS, handovers, crossings with source failures, measgap); its high-level note recommends keeping the network-time mapping. All 28 builds pass.

@josephnef josephnef added the skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision) label Nov 10, 2026
@josephnef
josephnef merged commit c049a0a into master Nov 10, 2026
30 of 31 checks passed
@josephnef
josephnef deleted the feat/m6-daps-xch branch November 10, 2026 10:34
josephnef added a commit that referenced this pull request Nov 10, 2026
…it with its DL PDUs (#559)

Part of #260 (epic #254). Follows #558 (sequential uplink visits): the
downlink side across channels.

## Why
Before the switch, the UE takes DL only from its serving cell. With the
source dead, it had no DL from the kill to the switch (1.4–1.9 s),
although the target held every PDU since the command (DAPS DL).

## What (`examples/cell`)
- **The answer:** when the target DU hears a visit copy from a UE it
staged for an inter-frequency arrival, before the switch, it answers at
once with the queued DL PDUs no answer has offered yet (up to 8).
- The frame (`build_visit_dl_frame`, `cell.h`) is broadcast-addressed:
no ACK, no CW escalation.
- It is tagged with the target cell's key, and the body names the UE's
target identity.
- **The UE:** while still holding the target's channel, it takes the
PDUs into its DL stream (`parse_visit_dl_frame`; `cell.ue.visit_dl`).
- **After the switch:** the queue keeps everything (nothing ACKs an
answer), and the target sends not-yet-offered PDUs first, then the
offered ones. Without that ordering, the newest PDU waited behind ~5
frames the UE already had (0.53–0.63 s).
- **Sending:** answers go out from a dedicated thread. Sending from
inside the RX callback froze the DU on the rig (the RX loop recycles the
TX pages).
- **Only across channels:** the target's neighbour list must put the
source on another channel. On one channel the target hears every uplink,
and an answer would land in the source cell's schedule.
- **Selftests:** the visit DL frame (broadcast RA, target identity, key,
not a unicast DL frame, PDU sn under the tag). ctest 95/95.

## Rig (testrig `scripts/m6_daps_xch.sh`, 2 runs per case, all pass)
| longest DL silence at the UE | runs |
|---|---|
| source killed at the accept, visits answered | 205, 205 ms |
| source killed, no visits | 1395, 1498 ms |
| no fault, visits answered | 308, 205 ms |

- **Answers:** 19–21 per handover; DL DAPS lost no PDU.
- **Uplink:** unchanged (0 lost before the switch with visits).
- **What remains:** the switch's own pause (~2 superframes). That is
next.
- **Regression:** same-channel DL DAPS (no answers sent, silence
unchanged), the three-cell crossing with a source failure, plain and
fault handovers.

🤖 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