Skip to content

cell M5: move a running cell to another channel (runtime channel separation) - #567

Merged
josephnef merged 1 commit into
masterfrom
feat/m5-runtime-channel
Nov 12, 2026
Merged

josephnef merged 1 commit into
masterfrom
feat/m5-runtime-channel

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Closes the last open item of #259 (M5), epic #254: the coordination planner's channel separation, until now only logged (ran.retune), is applied at runtime.

How (src/cell/ChannelSwitch.h, F1 v15 ChannelSwitch)

  • CU (RAN_RETUNE_APPLY=1): tells the proposed DU to serve on the spare channel from a network superframe RAN_CS_LEAD_SF (20) ahead (ran.cs_cmd), and resends until it is staged.
    • One move at a time, never across a handover, in either direction.
    • Done when the DU reports the new channel (ran.cs_done); timed out 50 sf after its instant.
    • A refused or timed-out move backs that DU off RAN_CS_BACKOFF_S (30 s).
  • DU: refuses too little lead, its own channel, a handover in progress, or a retune that would not fit the idle window (no_window).
    • A staged move is announced in its beacon (CsIe: id, cell, channel, first superframe on the new channel; cell-key tag, CRC), and the DU refuses handover commands meanwhile.
    • In the idle part of the superframe before the instant, after every slot, the DU stops its beacon, FastRetunes and restarts it. The TSF doesn't move, so the grid and slots keep their phase.
  • UE: takes the announcement once (CsReceiver: a newer id, instant ahead, so a replayed beacon can't move it).
    • It retunes with its cell in the same window and restarts its beacon at once.
    • If it doesn't hear its cell on the new channel within 10 superframes, it goes back.
  • Selftests (ctest cell_m5): IE round trip, wrong key/cell, tamper, replay and spent instants, the window, F1 message and ack.

Rig (testrig m5_retune.sh: two cells on 149, capacity 1, cell 2's DL frames land on UE1's uplink; the planner proposes 157 for DU1)

case (runs) result
apply (2) moved; DU retune 23.7 ms, UE 70–116 µs after its window; across the switch 0 / 1 uplinks and 0 DL frames lost; 147/147 uplinks in slot after; on-air cross-cell overlaps none after the move (20 and 1 before it)
fault: DU1's retune can't fit (2) refused at staging (no_window), never announced, the UE never moved; 3 commands in 90 s (backoff)
baseline, apply off (1) proposed, not made; 68 cross-cell overlaps on air

At this rig's levels the overlap costs UE1 only a few uplinks (1–3 of ~440 without the move, 0 with it), so the gain shown is the conflict removed, not a large delivery loss recovered. A first scenario (phase 3000) had a planner conflict with no frame overlap on air; the planner reasons on whole slots. The docs record both. The UE's fall-back path ran on the rig only before the staging check (it fell back every time and was heard on the old channel 1 sf later); a DU now refuses a move it can't make.

Regression. Coordination (m5_ran_coord), crossing3 and crossing pass on this branch. handover, measgap, three_ho, daps, daps_dl and daps_xch fail, identically on master (A/B run on the rig). The rig's RF levels changed between 11:20 and 16:00 today, with no code change in between. UE1 now hears cell 1 at −50 dBm (was −63) and cell 2 at −51 (was −53), so those suites' standing A3 margins (+10–13 dB) are gone and no handover is the correct outcome. The cause isn't established.

🤖 Generated with Claude Code

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

Copy link
Copy Markdown

PR Summary by Qodo

Apply coordinated channel moves to running cells

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Apply spare-channel proposals at runtime behind RAN_RETUNE_APPLY, moving one cell at a time.
• Coordinate authenticated announcements and idle-window retunes so DUs and UEs preserve slot
 timing.
• Add refusal, timeout, fallback, and backoff behavior, with selftests and rig results.
Diagram

sequenceDiagram
    participant Planner
    participant CU
    participant DU
    participant UE
    Planner->>CU: Spare-channel proposal
    CU->>DU: ChannelSwitch command
    DU-->>CU: Staged or refused
    DU-->>UE: Tagged beacon announcement
    Note over DU,UE: Retune in idle window
    DU-->>CU: New-channel report
    alt Cell not heard
        UE->>UE: Return to old channel
    end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Combine channel moves with schedule updates
  • ➕ Could reuse the existing scheduling command and acknowledgment path.
  • ➖ Couples radio retuning to slot reassignment and obscures the distinct staging and completion lifecycle.

Recommendation: Keep the dedicated ChannelSwitch command and beacon IE. A running-cell move needs its own admission checks, advance UE notice, and completion tracking; folding it into schedule updates would conflate separate operations. Review the timing and failure paths particularly closely.

Files changed (6) +602 / -20

Enhancement (4) +503 / -12
main.cppImplement DU and UE channel-switch behavior +213/-4

Implement DU and UE channel-switch behavior

• The DU validates and stages F1 commands, announces moves in its beacon, retunes within the idle window, and reports its live channel. The UE accepts fresh authenticated announcements, retunes without changing its grid phase, and returns to the old channel if its cell is not heard.

examples/cell/main.cpp

ChannelSwitch.hDefine channel-switch announcements and timing checks +171/-0

Define channel-switch announcements and timing checks

• Adds encoding and validation for the cell-key-tagged beacon IE, UE replay and expired-instant rejection, and an idle-window fit calculation.

src/cell/ChannelSwitch.h

F1.hAdd F1 v15 ChannelSwitch command and acknowledgment kind +46/-5

Add F1 v15 ChannelSwitch command and acknowledgment kind

• Introduces the authenticated CU-to-DU channel-switch message and extends F1 message and acknowledgment decoding to recognize it.

src/cell/F1.h

main.cppApply planner proposals through tracked channel moves +73/-3

Apply planner proposals through tracked channel moves

• Adds an opt-in CU path that commands one DU move at a time, resends until staged, fences moves against handovers, and confirms the new reported channel. Refused or timed-out moves place the DU in a configurable backoff.

tools/ran-controller/main.cpp

Tests (1) +37 / -0
cell_m5_selftest.cppTest channel-switch protocol primitives +37/-0

Test channel-switch protocol primitives

• Covers beacon IE round trips, key and cell mismatches, tampering, replay and expired instants, idle-window fit, and F1 command and acknowledgment serialization.

tests/cell_m5_selftest.cpp

Documentation (1) +62 / -8
cell-m5.mdDocument runtime channel moves and rig evidence +62/-8

Document runtime channel moves and rig evidence

• Explains command staging, beacon announcements, synchronized retuning, fallback, and backoff. Records apply, refusal, and disabled-mode rig results and revises the remaining coverage limits.

docs/cell-m5.md

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

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

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. A failed beacon restart looks like a move 🐞 Bug ☼ Reliability
Description
run_du updates du_ch after FastRetune but only logs a failed StartBeacon call. When the
retune succeeds and the restart fails, subsequent reports advertise the new channel, allowing the
controller to declare the move done while the DU is not beaconing there.
Code

examples/cell/main.cpp[R2995-2998]

+            if (tuned)
+              du_ch.store(cs->channel);
+            if (!dev->StartBeacon(live_bcn.data(), live_bcn.size(), c.tu))
+              emit("{\"ev\":\"cell.cs.restart_fail\"}");
Evidence
du_ch changes before the restart result is known, and the failed restart only emits a log. Reports
use du_ch, while the controller's completion test checks the reported channel and superframe, not
beacon health.

examples/cell/main.cpp[2983-3009]
examples/cell/main.cpp[1795-1804]
tools/ran-controller/main.cpp[1400-1406]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A successful retune followed by a failed beacon restart is reported to the controller as a completed channel move.
## Fix Focus Areas
- examples/cell/main.cpp[2983-3009]
- examples/cell/main.cpp[1795-1804]
- tools/ran-controller/main.cpp[1400-1410]
## Recommended Fix
Make beacon restart part of the switch success condition. Recover or report an explicit execution failure rather than letting the channel report alone certify completion.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. A failed UE retune disables its fallback 🐞 Bug ☼ Reliability
Description
cs_back treats an increased serving-beacon count as proof that the cell was heard on the new
channel, without checking whether FastRetune succeeded. If the UE's retune throws while
old-channel beacons are still arriving, those beacons clear its fallback state even though it
remains on the old channel.
Code

examples/cell/main.cpp[R3798-3801]

+      if (cs_back && !retune_to) {
+        if (serv_bcn_n.load() > cs_back->bcn_at && sfn >= cs_back->deadline_sf - 10) {
+          cs_back.reset(); /* the cell is heard on the new channel */
+        } else if (sfn > cs_back->deadline_sf) {
Evidence
cs_back records the beacon count before the attempted retune; the receive path increments it for
serving beacons. The retune path clears retune_to before calling FastRetune and leaves cur_ch
unchanged on failure, yet the fallback success test uses only the count.

examples/cell/main.cpp[3780-3784]
examples/cell/main.cpp[3450-3455]
examples/cell/main.cpp[3798-3808]
examples/cell/main.cpp[3952-3978]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Old-channel beacons can satisfy the channel-switch success test after a failed UE retune.
## Fix Focus Areas
- examples/cell/main.cpp[3780-3808]
- examples/cell/main.cpp[3952-3978]
## Recommended Fix
Clear switch fallback only after a successful retune and a serving beacon received on the new channel. Keep or explicitly resolve the pending switch when retuning fails.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. A failed DU retune has no outcome ack 🐞 Bug ☼ Reliability
Description
run_du clears cs_staged after its execution attempt even if StopBeacon fails or FastRetune
throws, and sends no execution-result acknowledgment. The controller has only the earlier Staged
ack and waits until its timeout, while UEs may already have acted on the announced switch.
Code

examples/cell/main.cpp[R3007-3009]

+        emit(l);
+        std::lock_guard<std::mutex> lk(f1_mu);
+        cs_staged.reset();
Evidence
Both a failed stop and a thrown retune leave tuned false. Nevertheless the staged state is reset,
whereas the controller otherwise waits for a target-channel report or a timeout 50 superframes after
the instant.

examples/cell/main.cpp[1971-1973]
examples/cell/main.cpp[2981-3009]
tools/ran-controller/main.cpp[1400-1410]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The DU discards a staged switch after a failed radio operation without telling the controller that execution failed.
## Fix Focus Areas
- examples/cell/main.cpp[2981-3009]
- tools/ran-controller/main.cpp[706-718]
- tools/ran-controller/main.cpp[1400-1414]
## Recommended Fix
Track the execution result and send an authenticated failure outcome that the controller handles immediately. Preserve enough state to make retries or recovery explicit rather than silently clearing the staged operation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. A restarted DU loses its staged move 🐞 Bug ☼ Reliability
Description
cs_flight->staged permanently stops command retransmission once the controller receives a Staged
ack. If the DU restarts before the switch instant, its process-local cs_staged disappears, but the
controller does not resend the command and waits for the move to time out.
Code

tools/ran-controller/main.cpp[R1411-1413]

+      } else if (!cs_flight->staged && net_now + 2 < cs_flight->cmd.effective_net_sf) {
+        cs_flight->cmd.seq = next_seq();
+        send(d, f1::encode_channel_switch(cs_flight->cmd, mac));
Evidence
The acknowledgment sets the controller's staged flag, which disables retransmission. The DU's staged
command is held only in local process state, so it does not survive a restart.

tools/ran-controller/main.cpp[706-716]
tools/ran-controller/main.cpp[1411-1414]
examples/cell/main.cpp[1376-1385]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A DU restart erases its staged switch while the controller continues to believe it is staged.
## Fix Focus Areas
- tools/ran-controller/main.cpp[706-718]
- tools/ran-controller/main.cpp[1398-1414]
- examples/cell/main.cpp[1376-1385]
## Recommended Fix
Detect a new DU instance or otherwise verify continued staging, then reissue the command while it still has enough lead; handle an expired instant without waiting for the full post-instant timeout.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (3)
5. A rapid controller restart reuses a move 🐞 Bug ≡ Correctness
Description
cs_last_id starts afresh and derives its first ID from a seconds-resolution version_base. If the
controller restarts within that second and issues another switch, the DU can treat the reused ID as
a resend, acknowledge it as staged, and ignore its new channel and instant.
Code

tools/ran-controller/main.cpp[R1443-1445]

+          CsFlight f;
+          cs_last_id = std::max(cs_last_id + 1, version_base);
+          f.cmd = {du, cs_last_id, adv.channel.at(du), net_now + cs_lead_sf, next_seq()};
Evidence
The startup base has one-second resolution and the new counter is process-local. On the DU, ID
equality alone selects the resend branch before channel, instant, and lead validation.

tools/ran-controller/main.cpp[315-319]
tools/ran-controller/main.cpp[379-380]
tools/ran-controller/main.cpp[1443-1448]
examples/cell/main.cpp[1948-1967]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Two controller instances started in one second can assign the same switch ID to different commands, which the DU mistakes for one command.
## Fix Focus Areas
- tools/ran-controller/main.cpp[315-319]
- tools/ran-controller/main.cpp[379-380]
- tools/ran-controller/main.cpp[1443-1445]
- examples/cell/main.cpp[1948-1967]
## Recommended Fix
Use an ID allocation scheme that remains monotone across controller restarts, and require an equal-ID resend to match the already-staged channel and effective instant before acknowledging it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. A cell can switch channel without telling its UEs 🐞 Bug ☼ Reliability
Description
run_du retunes a staged channel switch based on cs_staged and the superframe alone, without
verifying that a beacon carrying the channel-switch IE was successfully published. If sync is lost
after staging or beacon updates fail, the DU stops its beacon and calls FastRetune while
unannounced UEs remain on the old channel, where they lose the cell and have no cs_back fallback.
Code

examples/cell/main.cpp[R2976-2981]

+      if (cs && sf + 1 >= cs->eff_sf) {
+        const auto at = devourer::cell::cs_retune_at_us(grid_span_us, cs_retune_us, 1000, P);
+        const uint8_t from = du_ch.load();
+        bool tuned = false;
+        int64_t late_us = 0, retune_us = 0;
+        if (at && sf + 1 == cs->eff_sf) {
Evidence
The channel-switch IE is added only while synced && f1_on, and live_bcn changes only after
UpdateBeaconPayload succeeds. The retune block checks only cs_staged and the superframe, with no
record of successful IE publication or check for lost sync; meanwhile, a UE stages a move and arms
cs_back only after accepting the IE.

examples/cell/main.cpp[2528-2529]
examples/cell/main.cpp[2804-2817]
examples/cell/main.cpp[2853-2864]
examples/cell/main.cpp[2967-3010]
examples/cell/main.cpp[2860-2867]
examples/cell/main.cpp[2976-3009]
examples/cell/main.cpp[3453-3455]
examples/cell/main.cpp[3762-3771]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The DU can retune for a staged channel switch that was never successfully announced because sync was lost or beacon updates failed, leaving unannounced UEs on the old channel.
## Fix Focus Areas
- examples/cell/main.cpp[2807-2817]
- examples/cell/main.cpp[2860-2864]
- examples/cell/main.cpp[2976-3010]
## Recommended Fix
Add a counter to `CsStaged` for superframes in which a beacon carrying the current channel-switch IE was successfully published. Increment it only when `UpdateBeaconPayload` succeeds and the IE was included in `ho_ies`; constructing the IE alone must not count. At the scheduled switch instant, abandon the move if `synced` is false or the count is below a sufficient lead threshold (for example, three superframes): log `cell.cs.abandoned`, reset `cs_staged`, and leave the beacon running. Let the CU time out and back off as it already does.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. A UE can permanently ignore its cell's move ✓ Resolved
Description
In run_ue, cs_rx.accept(*cs_seen, sfn) runs before the !ho_pending check, so the move id is
recorded as taken even when cs_pending is not set because a handover is pending. The DU
re-announces the same id every beacon, and CsReceiver now rejects it as already seen. If the
handover is later dropped or cancelled, the UE stays on the old channel when the DU moves, with no
cs_back fallback armed.


Informational

8. A protocol comment promises an Applied ack that never comes 🐞 Bug ⚙ Maintainability
Description
The ChannelSwitchCmd comment in F1.h says the DU acks "Applied once it moved", but the DU only
ever sends Staged, Stale, Late or Invalid. The CU detects completion only through the reported
channel (ran.cs_done). Anyone building on the comment (a CU waiting for Applied, or a log
analyser) will wait for a message that never arrives.
Code

src/cell/F1.h[R621-624]

+/* A running cell's move to another channel (ChannelSwitch.h), CU -> that
+ * DU: from the network superframe `effective_net_sf` it serves on `channel`.
+ * `seq` orders the copies (a replayed or reordered one is Stale). The DU
+ * acks Staged (and announces it), or Late/Invalid; Applied once it moved. */
Evidence
The DU's ChannelSwitch handler sends a single ack at staging time. The retune block emits only a log
line and sends nothing over F1. The CU's done check reads d.rep.channel.

examples/cell/main.cpp[1938-1981]
examples/cell/main.cpp[2996-3010]
tools/ran-controller/main.cpp[1400-1415]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The F1.h comment says the DU acks Applied once a channel move is made, but no Applied ack is ever sent.
## Fix Focus Areas
- src/cell/F1.h[621-624]
## Recommended Fix
Change the comment to say completion is seen when the DU's Report shows the new channel at or after the instant. Alternatively, send `encode_ack({c.cell, AckKind::ChannelSwitch, id, Applied, net_sf})` after a successful retune in examples/cell/main.cpp.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can open the rule or relationship behind a finding in one click

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

…n-announced, DU and UEs retune together

The coordination planner's spare-channel proposals were only logged. With
RAN_RETUNE_APPLY=1 the CU now applies one: F1 v15 ChannelSwitch to that DU,
RAN_CS_LEAD_SF (20) ahead; the DU stages it (refusing too little lead, its own
channel, a handover in progress, or a retune that would not fit the idle
window), announces it in its beacon (src/cell/ChannelSwitch.h: CsIe, tagged
with the cell key, CRC; CsReceiver rejects replays and spent instants), and in
the idle part of the superframe before the instant it and each UE stop the
beacon, FastRetune and restart it - no TSF change, so the grid and slots keep
their phase. A UE not hearing its cell there within 10 superframes goes back.
The CU fences moves against handovers, times one out 50 sf after its instant
and backs a refusing DU off RAN_CS_BACKOFF_S (30 s).

Rig (testrig m5_retune.sh, cell 2's DL on UE1's uplink): the move done in both
apply runs, 0/1 uplinks and 0 DL frames lost across it, uplinks in slot from
the first superframe on 157, on-air cross-cell overlaps stop; a DU that can't
fit the retune refuses and is not asked every round.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@josephnef
josephnef force-pushed the feat/m5-runtime-channel branch from aeac6d2 to f34ef69 Compare November 12, 2026 23:21
@josephnef

Copy link
Copy Markdown
Collaborator Author

Review fixes pushed. All eight were real:

  1. Failed beacon restart looks like a move: the restart is retried 3×. If it still fails, the DU retunes back to the old channel and restarts there, and du_ch only changes on success. The outcome is reported (item 3).
  2. Failed UE retune disables its fallback: if the UE is still on the old channel, its fall-back record is dropped; it's already where it would go back to. Beacons from the old channel can't clear it any more.
  3. No outcome ack: at the instant the DU acks the outcome, Applied once it moved or Invalid if it didn't (why: unannounced / missed / stop_beacon / retune / restart_beacon). The CU backs off on Invalid.
  4. A restarted DU loses its staged move: the CU now resends until the instant, staged or not, so a restarted DU stages the move again.
  5. A rapid CU restart reuses an id: a staged id arriving with another channel or instant is refused (id_reused). The CU's next attempt carries a new id.
  6. A cell can move without telling its UEs: the DU moves only if a published beacon carried the IE in at least 3 superframes before the instant (aired). Otherwise it stays (unannounced), for example after losing sync, when the IE isn't aired.
  7. A UE can permanently ignore the move: with a handover pending, the move isn't taken or marked seen, so the next beacons offer it again.
  8. The comment promised an Applied ack: it's now true (item 3), and the comment says what the outcome ack carries.

Rig on the fixed build (m5_retune.sh, all 5 pass):

  • apply: moved, the IE aired in 18 superframes, Applied acked, 1/0 uplinks and 0 DL frames lost across the switch, 147/147 in slot after.
  • fault: refused at staging, never announced.
  • baseline: 80 on-air overlaps.

m5_ran_coord passes.

The M6 suites that need a handover margin fail on master and here alike since the rig's RF levels changed; a separate PR makes them placement-independent.

@josephnef

Copy link
Copy Markdown
Collaborator Author

Qodo's re-review marks #7 resolved. It lists #1–#6 and #8 with their original descriptions, but the pushed head (f34ef69) has each fix:

Adding skip-qodo-gate.

@josephnef josephnef added the skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision) label Nov 12, 2026
@josephnef
josephnef merged commit 7faeb67 into master Nov 12, 2026
30 of 31 checks passed
@josephnef
josephnef deleted the feat/m5-runtime-channel branch November 12, 2026 23:33
josephnef added a commit that referenced this pull request Nov 13, 2026
…LEVEL) (#568)

Part of #254 (M6 regression).

**Why.** On 2026-11-12, between 11:20 and 16:00, the rig's RF levels
changed with no code change in between. UE1 now hears cell 1 at −50 dBm
(−63 before) and cell 2 at −51 (−53); UE2's levels moved by 2–5 dB. The
cause is not established. The M6 suites that hand over on a standing
difference lost their margin (+10 to +13 dB): handover, measgap,
three_ho, daps, daps_dl and daps_xch failed, identically on master (A/B
on the rig). With those levels the A3 correctly has nothing to fire on.

**What.** `CELL_UE_MEAS_LEVEL="cell:dBm,..."` (`MeasLevelPin`,
`examples/cell/cell.h`). For each listed cell, the UE takes the median
of its first 8 beacon measurements as where the bench put it, and shifts
every later measurement by (target − median). The offset is logged
(`cell.ue.meas_pin`); the cell is held out of the measurement table
while it calibrates.
- Fluctuations and TX ramps still come through; only the standing level
is the suite's.
- The radio links themselves are untouched: the bench still carries the
frames.
- It's a test-bench emulation in the example node, like `CELL_ATTEN_*`
and `CELL_DU_TX_RAMP`; not library behaviour.
- Selftest in `cell_node`.

**testrig** (committed there): `m5_ran_coord.sh` takes `M5_MEAS_LEVELS`
("UE=cell:dBm,...;UE=..."). Each standing-difference M6 suite defaults
it to the medians of its passing runs: two cells UE1 −63/−52, UE2
−53/−38; three cells per UE. The ramp-driven crossings are not pinned,
and `M5_MEAS_LEVELS=""` runs on the bench's own levels.

**Rig, on the changed levels:** all 8 M6 suites pass (handover, measgap,
three_ho, daps, daps_dl, daps_xch, crossing3, crossing). On this exact
build (master with #567, plus this), handover and measgap passed again.
The calibrated offsets show the change: UE1's cell 1 is pulled −13 dB,
the others by −2 to +6.

🤖 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