Skip to main content

Runtime Commands

The gNB components of a Racora cell — CNS OCUDU — expose a runtime remote-control interface: JSON commands over WebSocket, port 8001. The CU-CP's instance is the operational one — it carries the cell lifecycle (lock/bar), the runtime mobility configuration, and the handover triggers. Every command below is in the gnb image Racora ships. The cell lifecycle commands (lock, unlock, bar, unbar, status) and sib_update are upstream OCUDU's; the mobility configuration and handover-trigger commands and pci_set are CNS OCUDU's (what CNS OCUDU adds).

Normal operation does not need this surface. The NRCell spec is the operator interface — see Configure Mobility and Cell State — and the controller projects spec changes into the boot overlay and applies them to the running CU-CP over these same commands (no restarts). This page documents the machinery itself: what the gNB image is capable of. Reach for direct WS access for inspection (cell_status), for actions with no declarative form (the handover triggers), and as an escape hatch.

Reaching the Surface​

In-cluster, the CU-CP answers at ws://cu-cp.centralized-unit.svc.cluster.local:8001 (Service port metrics-ws). The convenient interactive client:

# needs Docker Hub and npm; on a mirrored cluster, `kubectl -n centralized-unit port-forward svc/cu-cp 8001` and a local wscat instead
kubectl run wsclient --rm -it --restart=Never --image=node:22-alpine \
-n centralized-unit -- sh -c 'npx --yes wscat -c ws://cu-cp:8001'

Requests are single-line JSON: {"cmd":"<name>", ...args}. Success replies echo {"cmd","timestamp"} (plus "result" where applicable); failures reply {"cmd","error","timestamp"}.

DUs run the same server (see DU commands), but no Service exposes it — use kubectl exec into the DU pod. Racora applies PCI changes through the NRCell spec, not this path.

Two Command Semantics​

Config commands are synchronous. The reply carries the real outcome: an ack means the change is live in the CU-CP registry now; an error means nothing changed. Applies to: cell_lock/unlock/bar/unbar, mobility_cell_set/remove, neighbor_add/remove, report_config_set/remove, periodic_report_set.

Trigger commands are fire-and-forget. For trigger_handover and trigger_conditional_handover the ack means only "arguments parsed" — a trigger against a stale RNTI or unknown target acks successfully and then logs a warning. The outcome exists only in the CU-CP log (/tmp/cu_cp.log in the pod, mirrored to ClickHouse). Success chain, in order:

ue=N: Trigger intra-CU (inter-DU) handover from source_du=A to target_du=B
ue=N: "Intra CU Handover Routine" finished successfully
ue=M: "Intra CU Handover Target Routine" finished successfully

Two more properties of every config command:

  • No push to connected UEs. Runtime mobility changes reach a UE's measConfig only at its next RRC reconfiguration (What a Connected UE Sees).
  • Not persisted by the gNB. Runtime state dies with the CU-CP process. The controller's boot overlay (from NRCell spec) is what restarts converge to — manual WS changes that should survive belong in the spec.

Command Reference​

Every command is one JSON object: cmd plus its arguments. An optional top-level traceparent (W3C) makes the command's span a child of that trace. Success replies echo cmd and add timestamp (cell_status also adds result); a failure replies error instead. NCIs are decimal integers on the wire (kubectl get nrcell shows the hex form in its NCI column; status.nciDecimal holds this one), PLMNs are 5- or 6-digit strings, PCIs are 0–1007.

The envelope is validated before any command: an unknown cmd replies Unknown command type: <cmd>, a missing cmd replies 'cmd' object is missing and it is mandatory, a non-string cmd replies 'cmd' object value type should be a string, and malformed JSON replies an error with no cmd key at all.

Cell Lifecycle (CU-CP, CGI-Addressed)​

All five take the cell's global identity, cgi = {plmn, nci}:

{"cmd": "cell_lock", "cgi": {"plmn": "90170", "nci": 6733824}}
CommandEffectRejection
cell_lockGraceful stop: bar → release UEs → deactivate → radio stop. The CU-CP keeps the intent across DU restarts until an explicit cell_unlock. Declarative form: spec.adminState: Locked.CU-CP rejected cell_lock: no served DU matches the provided CGI, or scheduling failed
cell_unlockReactivates the cell (spec.adminState: Unlocked).CU-CP rejected cell_unlock: … (same wording)
cell_bar / cell_unbarSets / clears MIB cellBarred: the cell stays on air, UEs may not camp. Tracked independently of the lock. Declarative: spec.cellBarred.CU-CP rejected cell_bar: … / cell_unbar: …
cell_statusReads the cell back: "result": {"admin_state": "locked" | "unlocked", "operational_state": "enabled" | "disabled", "cell_barred": true | false}. Matches by NCI alone.CU-CP has no cell matching the provided CGI, or the state read failed

The cgi schema errors: 'cgi' object is missing and it is mandatory, 'cgi.plmn' object is missing and it is mandatory, 'cgi.nci' object value type should be an unsigned integer, Invalid PLMN identity value, Invalid NR cell identity value. Note that cell_lock rejects a PLMN that does not equal the cell's spec.plmn exactly, while cell_status still answers — the most common "lock does nothing" cause.

Runtime Mobility Configuration (CU-CP, NCI-Addressed)​

neighbor_add — a directional relation; declare both ways for symmetric mobility. Declarative: spec.neighbors.

{"cmd": "neighbor_add", "nci": 6733824, "neighbor_nci": 6733825, "report_configs": [2]}
KeyRequiredValue
nci, neighbor_nciyesunsigned integers, valid NCIs (Invalid NR cell identity value in '<key>')
report_configsyesnon-empty array of report config ids 1–63 ('report_configs' entries must be in range [1, 63]); each must exist and be event-triggered

Rejection: CU-CP rejected neighbor_add: unknown cell, or invalid report config reference.

neighbor_remove — {"cmd": "neighbor_remove", "nci": 6733824, "neighbor_nci": 6733825}. Rejection: CU-CP rejected neighbor_remove: no such neighbor relation.

report_config_set — upsert of a CU-wide measurement report configuration; retunes a live A3 in place. Declarative: spec.mobility.reportConfigs.

{"cmd": "report_config_set", "report_cfg_id": 2, "report_type": "event_triggered",
"event_triggered_report_type": "a3", "meas_trigger_quantity": "rsrp",
"meas_trigger_quantity_offset_db": 3, "hysteresis_db": 0,
"time_to_trigger_ms": 100, "report_interval_ms": 1024}
KeyRequiredValue
report_cfg_idyes1–63
report_typeyesperiodical | event_triggered | cond_trigger
event_triggered_report_typefor eventsa1 … a6. d1, d2, t1 are refused here (Distance and time based events (d1, d2, t1) are not supported through this command) — declare those through the NRCell
meas_trigger_quantitynorsrp | rsrq | sinr
meas_trigger_quantity_threshold_db, meas_trigger_quantity_threshold_2_db, meas_trigger_quantity_offset_dbnointegers (A1/A2/A4/A5 threshold, A5 second threshold, A3/A6 offset)
hysteresis_dbno0–15
time_to_trigger_msno0, 40, 64, 80, 100, 128, 160, 256, 320, 480, 512, 640, 1024, 1280, 2560, 5120
report_interval_msfor periodical and event_triggered120, 240, 480, 640, 1024, 2048, 5120, 10240, 20480, 40960, 60000, 360000, 720000, 1800000
t312no0, 50, 100, 200, 300, 400, 500, 1000
periodic_ho_rsrp_offset_dbno−1 … 30 (−1 disables handover from periodical reports)

Rejections: Invalid report configuration (see CU-CP log for the cause) and CU-CP rejected report_config_set: type conflicts with existing references (a config's type class cannot change while a relation or a periodic report references it — use a new id).

report_config_remove — {"cmd": "report_config_remove", "report_cfg_id": 3}. Rejection: CU-CP rejected report_config_remove: unknown id, or still referenced.

periodic_report_set — the serving cell's periodical report; omit report_cfg_id to clear it. Declarative: spec.periodicReportCfgId.

{"cmd": "periodic_report_set", "nci": 6733824, "report_cfg_id": 1}

Rejection: CU-CP rejected periodic_report_set: unknown cell or non-periodical report config.

mobility_cell_set — declares or updates an external cell (another gNB's) in the mobility registry. On a local cell the next F1 Setup overwrites it, and a partial set degrades the entry to incomplete until then.

{"cmd": "mobility_cell_set", "nci": 6750208, "gnb_id_bit_length": 22, "plmn": "90170",
"pci": 7, "tac": 7, "band": 3, "ssb_arfcn": 368410, "ssb_scs": 15,
"ssb_period": 20, "ssb_offset": 0, "ssb_duration": 1}
KeyRequiredValue
nciyesthe external cell's NCI
gnb_id_bit_lengthyes22–32
plmnnostring
pcino0–1007
tacno1–16777213 (0xfffffd)
band, ssb_arfcnnounsigned integers
ssb_scsno15, 30, 60, 120, 240
ssb_period, ssb_offset, ssb_durationtogether or not at allperiod 5, 10, 20, 40, 80, 160 ms; offset < period; duration 1–5

Rejection: CU-CP rejected mobility_cell_set.

mobility_cell_remove — {"cmd": "mobility_cell_remove", "nci": 6750208}. Cascades: relations pointing at the cell are removed first. Rejection: CU-CP rejected mobility_cell_remove: no cell with the provided NCI.

Handover Triggers (CU-CP, Fire-and-Forget)​

{"cmd": "trigger_handover", "serving_pci": 1, "rnti": 17921, "target_pci": 0,
"plmn": "90170", "tac": 7}

All five keys are required (serving_pci, target_pci 0–1007; rnti 0–65535; tac 1–16777213; plmn a string). (serving_pci, rnti) selects the UE; target_pci resolves against all cells the CU knows: a local match runs an intra-CU handover, no local match takes the inter-CU path (the only place plmn/tac are consumed). The ack means "arguments parsed"; the outcome is in the CU-CP log and on the command's span (mobility.trigger_outcome).

{"cmd": "trigger_conditional_handover", "serving_pci": 1, "rnti": 17921,
"target_pcis": [2, 3], "timeout_ms": 5000}

target_pcis is 1–8 PCIs; timeout_ms (optional) 1–600000; t1_thres (optional, a timestamp string) overrides the T1 threshold. Arms Rel-16 conditional handover — requires UE CHO capability, aborts gracefully without it.

The RNTI workflow: the CU log prints it in hex (grep "Updated UE with" /tmp/cu_cp.log), the command takes decimal, and it changes on every re-attach and every handover — always re-read immediately before triggering.

DU Commands​

Each DU serves the same protocol on its own port 8001 (no Service; use kubectl exec into the DU pod). CNS OCUDU adds pci_set, a DU command surface for changing a cell's PCI: in this release the DU accepts the request and does not change the cell's PCI. Racora does not use it: a PCI decision patches spec.pci, and the controller regenerates and rolls the DU under a cell lock. Do not call it by hand on a Racora cell; the controller owns spec.pci:

{"cmd": "pci_set", "cells": [{"plmn": "90170", "nci": 6733824, "pci": 5}]}

The other DU commands — ssb_set (cells[].ssb_block_power_dbm), sib_update (cells[].sib.{type, content}), rrm_policy_ratio_set (policies[]) and ntn_config_update — are upstream OCUDU's and are documented at docs.ocudu.org.

Reading Rejections — The Error Contract​

Validation is two-layer, and the reply tells you which layer spoke:

  • Precise message ('tac' must be in range [1, 0xfffffd], 'report_configs' entries must be in range [1, 63], …): the schema layer rejected it; the CU-CP never saw the command.
  • Coarse message (CU-CP rejected <cmd>: …): the CU-CP's state validation refused it — the precise cause is in the CU-CP log.

Common coarse rejections and their log-side causes:

WS error (coarse)CU log causes
neighbor_add: unknown cell, or invalid report config referenceNo cell config for neighbor nci=… · A cell cannot neighbor itself · Report config id=N is periodical (serving cell only)
report_config_set: type conflicts with existing referencesReferenced by a neighbor relation of nci=… · Used as periodic report of nci=…
report_config_remove: unknown id, or still referencedNo report config with this id or Referenced by a neighbor relation of nci=… — the WS string does not distinguish; check the log
periodic_report_set: unknown cell or non-periodical report configReport config id=N is not periodical · No cell config for nci=…
cell_lock: no served DU matches the provided CGI, or scheduling failedmost often a PLMN mismatch — the CGI's plmn must equal the cell's spec.plmn exactly (note: cell_status matches by NCI alone and will "work" with a wrong PLMN; cell_lock will not)
CU-CP has no cell matching the provided CGI, or the state read failed (cell_status)unknown NCI
mobility_cell_remove: no cell with the provided NCICannot remove cell nci=… Cause: No cell config for this NCI

The envelope errors are listed with the command reference above. The controller depends on the first of them: an Unknown command type reply is how it classifies a gnb image that predates a command as unavailable (tolerated), not as a failure.

Known Failure Modes​

  • Handover triggered at a locked cell is rejected cleanly: the trigger acks (arguments parsed), the CU-CP logs Ignoring Handover Request. Cause: Target cell with pci=N is administratively deactivated, and the UE stays put.
  • Locked cells linger as neighbors. Locking does not touch other cells' measurement configuration (Administrative State).
  • A forced handover against the RF gradient does not stick unless the A3 policy allows it: with symmetric neighbor config and trigger_handover_from_measurements on, the automation hands the UE back within about a second. Durable forced placement requires a measurement dead-band (offset/hysteresis), a removed relation, or the UE genuinely sitting between cells.
  • "My runtime change did nothing" — check the two semantics first: connected UEs need churn to receive measConfig updates, and a CU-CP restart since the change means it's gone (re-apply, or put it in the spec where it belongs).
  • Ping-pong between adjacent cells is correct behavior at hysteresis 0 — each bounce is a successful handover tracking the strongest cell. Tune the trade live: report_config_set with a higher meas_trigger_quantity_offset_db and/or hysteresis_db widens the dead-band.

Observing Outcomes​

Every command logs its effect in the CU-CP's log file and in ClickHouse, the "RAN Mobility" dashboard counts the handover markers, and every command executes inside a cucp.<cmd>.execute span that a traceparent on the payload parents to your trace. Where each of these is and how to query them is Read Logs and Traces; the span names and attributes are the span schema.