How Mobility Reaches the CU-CP
The operator declares neighbors, report configurations and administrative intent on NRCell
resources (Configure Mobility and Cell State). This page is how the
controller makes the running CU-CP agree, and what has to be true on the air first.
Physical Prerequisite: Aligned Frames
A single cell runs on the radio's internal clock. Two or more cells between which UEs are expected to hand over need their frames time-aligned: a UE measures a neighbor inside a window derived from its serving cell's timing, and two free-running cells put each other's SSB outside that window permanently. Connected-mode measurement, and so handover, is then physically impossible whatever the configuration says.
On USRPs that means a GPSDO on every radio, clockSource: gpsdo on every cell, and GPS
lock before mobility is expected (external works the same way with a shared reference).
The time source (syncSource) follows the clock source automatically when that source can
supply time; a disciplined oscillator alone aligns frequency, not frames. How to check lock
is on Connect a Radio.
Mobility
The operator surface is spec.neighbors[] (directional relations, so declare both ways),
spec.periodicReportCfgId, and spec.mobility.reportConfigs[], camelCase mirrors of the
CU-CP's cu_cp.mobility.report_configs keys. Report configurations are CU-wide in the
CU-CP, so the controller unions every cell's contributions by reportCfgId: define each id
once, on one cell, or identically on all. Differing definitions of the same id resolve to the
lexicographically first cell name and raise a ReportConfigConflict condition on the others.
Two built-in ids exist unless overridden and are never removed: 1 (periodical, every 1024 ms)
and 2 (A3 on RSRP, 3 dB offset, no hysteresis, 100 ms time to trigger).
One desired state, computed from the whole cell set, has two sinks:
- The boot overlay, the ConfigMap
nrcell-cu-cp-configincentralized-unit. A starting CU-CP reads it, so restarts converge from it with no controller action. - The runtime commands on the CU-CP's command WebSocket (
neighbor_add,neighbor_remove,report_config_set,report_config_remove,periodic_report_set). This is the live path: a mobility edit takes effect without a CU-CP restart and without a DU rollout. The controller keeps the last state it pushed in theracora.io/applied-mobilityannotation on the overlay ConfigMap, plans the difference, and sends only what changed; every command is an idempotent upsert or removal. A periodic re-push (racora-controller.mobility.resyncIntervalS, default 300 s) closes the one race left, a CU-CP crash inside the kubelet's ConfigMap sync window.
If the command surface is unreachable the sync logs that it is unavailable and returns; the boot overlay remains the convergence path.
One mobility setting is CU-wide and boot-only, so it lives on the controller chart rather
than the CRD: racora-controller.mobility.triggerHandoverFromMeasurements, automatic
A3-driven handover. Changing it takes effect at the next CU-CP restart.
What a Connected UE Sees
None of the runtime commands pushes an RRC reconfiguration. A UE picks up its new measurement configuration at its next reconfiguration: a PDU session set up, modified or released, a handover, or a re-attach. An idle but connected phone does not see a new neighbor until such churn. This is gNB behavior, not a controller delay.
The CU-CP also rejects changing a referenced report configuration's class in place (periodical to event-triggered or back). Use a new id, or let a CU-CP restart apply the change wholesale from the overlay.
Administrative State
spec.adminState: Unlocked|Locked and spec.cellBarred: true|false mirror the CU-CP's
logical-cell intent. The same two-sink model applies:
- at runtime, through
cell_lock,cell_unlock,cell_barandcell_unbar. Locking a serving cell drains it gracefully (bar, release the UEs, deactivate) and the CU-CP keeps the intent across DU restarts; - at boot, through the overlay's
cu_cp.logical_cellslist, which records every cell's intent by its controller-assignedsector_id.
The logical_cells section is emitted only when some cell departs from the default
intent (Unlocked, not barred). When it is emitted, every cell with an identity is listed,
because the declared set doubles as the CU-CP's activation whitelist: a cell that
reports in from outside it is realised locked. A cell created while the CU-CP is running is
by definition outside the booted whitelist, so it comes up dormant until the controller's
runtime sync pushes its declared state, within a reconcile retry of the DU's F1 attach.
With an all-default cell set there is no whitelist and a new cell activates at F1 setup;
the dormant-first behavior depends on some cell carrying a non-default intent.
Interplay with the decision loop: a decision-driven DU rollout drains
the cell with cell_lock and normally unlocks it afterwards. If spec.adminState is
Locked, the post-rollout unlock is withheld: operator intent wins. The CU-CP tracks
cellBarred independently of the lock, and a locked cell still appears in other cells'
measurement configuration as a neighbor, so pair a long-term lock with removing the
relations that point at it.