Deploy the O-RAN SC near-real-time RIC on one COSMOS server, enable the E2 agent inside an OCUDU
gNB on another, and read cell-level and per-UE throughput out of the running RAN with an xApp —
without changing anything on the radio, the core or the UE.
Every earlier tutorial on the learning path built a network and measured it from the outside, with
ping and iperf3 at the UE. This one looks at the same network from the inside. A near-real-time
RAN Intelligent Controller (near-RT RIC) is a platform that connects to the base stations of a
radio access network, collects measurements from them and — where the base station supports it —
sends control actions back. Small applications called xApps run on the RIC and implement the
actual monitoring or control logic. This tutorial deploys the O-RAN Software Community (O-RAN SC)
near-RT RIC, connects an OCUDU gNB to it, and runs an xApp that prints the gNB's own throughput
counters, cell-wide and per UE.
The RIC attaches to the gNB over E2 (SCTP), a control-plane interface entirely separate from
the user-plane path. The gNB's E2SM-KPM service model reports measurements once a second; the
xApp subscribes and prints them. As the Topology figure shows, the RIC sits beside the RAN, not in
it: the UE's traffic still flows from the gNB to the core exactly as before, and removing the RIC
changes nothing on the user plane.
The steps below work unchanged on any OCUDU rig: start with the software-only one (no radio), then
repeat them on an O-RAN 7.2 rig — only the gNB's own start procedure differs. Compare what the xApp
reports against iperf3 as you go.
E2 control (E2SM-RC) is not covered: on this OCUDU build the gNB decodes the reference control
xApp's request and answers RIC Control Failure. See the
RIC page for its current status. The other RIC option
on COSMOS, Aether SD-RAN, is deployed and works with its own RAN simulator
(tutorial), but a real gNB's E2
Setup Request is never acknowledged there; that problem is open and described on the RIC page.
DRB.UEThpDl (downlink throughput on the data radio bearers); E2SM-RC (RAN Control) is theAfter completing this tutorial you will be able to:
i-release) from the srsRAN Docker Compose bundle;| Learning path | Step 6 of 7 — RAN intelligence. Previous: OCUDU + SD-Core on two servers. Next: Amarisoft UE sweep |
| Domain / testbed | COSMOS osc sandbox (any two 7.2-capable servers) |
| Duration | 45–60 min (first RIC build ≈ 10 min) |
| Difficulty | Intermediate |
| RAN stack | OCUDU (srsRAN) |
| Core | Open5GS (soft rig) or Aether SD-Core (radio rig) |
| New interface | E2 (SCTP 36421) |
| Topic group | Cellular 4G/5G & O-RAN |
| Last verified | 2026-09-22 — soft ZMQ + Open5GS, LiteOn FlexFi + SD-Core, Benetel RAN650 + SD-Core (all on platform B = 2× Intel Xeon Gold 6126, 24C/48T) · text revised 2026-09-24 |
Background knowledge
docker compose up, docker exec, docker logs).Account & access
Devices / nodes
| Role | What it needs |
|---|---|
| RIC host | any server with Docker and internet access; it never touches the radio |
| gNB host | the OCUDU node of your existing rig |
| UE | the UE of your existing rig (soft srsue, or a COTS modem) |
Disk images — ocudu.ndz on the gNB host. The RIC host needs only Docker.
Software components — the O-RAN SC RIC i-release (srsran/oran-sc-ric), OCUDU's built-in E2
agent, and the bundle's Python xApps.
Spectrum / RF / special — unchanged from the underlying rig; E2 adds no RF.
Figure — The gNB (CU+DU with its E2 agent) connects to the near-RT RIC over E2 on SCTP. KPM indications flow up to the xApp and work; RC control flowing down is rejected by this OCUDU build. The user plane to the core runs alongside, untouched; SD-RAN is shown as the alternative RIC.
UE ──radio/ZMQ── OCUDU gNB (gNB host) ──N2/N3── 5G core
│
│ E2 / SCTP 36421 (data network)
▼
e2term ── e2mgr ── submgr ── xApp (RIC host, Docker)
Walk through it from the RAN outward. The radio path and the user plane (UE → gNB → N3 →
core) belong to the underlying rig and do not change. The gNB's E2 agent opens an SCTP
association on port 36421 across the data network to the RIC host. There, e2term (E2
termination) terminates the SCTP association and decodes E2AP; e2mgr (E2 manager) records which
E2 nodes are connected and exposes that over a REST API; submgr (subscription manager) relays
xApp subscriptions to the gNB and routes the resulting indications back; and the xApp prints what
it receives. All RIC components run as Docker containers on one host.
Pick the RIC host first. Two rules:
10.0.2.0/24, which is inside COSMOS10.0.0.0/10. Use a subnet reserved for container networks, such as 10.130.2.0/24.This clones the srsRAN Docker Compose packaging of the O-RAN SC RIC, pins it to the verified
commit, moves its container network off COSMOS address space (1a), publishes the E2 port on the
host so a gNB on another machine can reach it (1b), and builds and starts the containers.
git clone https://github.com/srsran/oran-sc-ric.git
cd oran-sc-ric
git checkout 621ade2 # the commit these instructions were verified against; the sed patterns below track it
# 1a. move off 10.0.2.0/24 (COSMOS NJ address space)
for f in .env docker-compose.yml ric/configs/*.rtg ric/configs/*.yaml ric/configs/e2term.conf \
xApps/python/lib/xAppBase.py e2-agents/srsRAN/gnb_zmq.yaml; do
sed -i 's/10\.0\.2\./10.130.2./g' "$f"
done
# 1b. publish E2 so a gNB on another host can reach it (commented out upstream)
sed -i 's|^ #ports:| ports:|; s|^ # - "36421:36421/sctp"| - "36421:36421/sctp"|' \
docker-compose.yml
docker compose up -d --build # first run builds two images, ~10 min
docker compose ps --format '{{.Name}} {{.State}}'
What to look for: seven containers running: ric_e2term, ric_e2mgr, ric_submgr, ric_rtmgr_sim,
ric_appmgr, ric_dbaas, python_xapp_runner.
OCUDU carries its E2 agent in the DU; the e2: block turns it on, selects the service models it
offers, and says where the RIC is. The metrics: block turns on the scheduler, MAC and RLC
metrics at a 1000 ms period; the per-UE throughput counters come from RLC.
Add to the gNB YAML you already use. addrs is the RIC host's data-network address,
bind_addrs the gNB host's own:
e2:
enable_du_e2: true
e2sm_kpm_enabled: true
e2sm_rc_enabled: true
e2sm_ccc_enabled: true
addrs: 10.59.1.19 # RIC host
bind_addrs: 10.59.1.18 # this host
port: 36421
metrics:
layers:
enable_sched: true
enable_mac: true
enable_rlc: true # per-UE DRB throughput comes from RLC
periodicity:
du_report_period: 1000
Start the gNB exactly as your rig's procedure does — for an O-RAN 7.2 rig that includes the O-RU
power cycle and the O-RU state check, unchanged.
At start-up the gNB opens the SCTP association and sends an E2 Setup Request listing its service
models; the RIC answers with E2 Setup Response. Check both sides: the gNB's socket shows the
transport, and the E2 manager's REST API shows that E2 Setup completed.
# gNB host: an ESTABLISHED SCTP association to the RIC
ss -np -A sctp | grep 36421
# RIC host: the RIC's own view
curl -s http://10.130.2.11:3800/v1/nodeb/states
# {"inventoryName":"gnbd_001_001_00019b_0", ... ,"connectionStatus":"CONNECTED"}
What to look for: Both must agree. An established SCTP socket alone does not mean E2 Setup
succeeded — the SD-RAN case on the RIC page is
exactly an established association on which E2 Setup never completed. connectionStatus":"CONNECTED"
is the proof.
The xApp subscribes to two KPM metrics — downlink and uplink DRB throughput — with report style 1,
so each indication carries one value for the whole cell.
Run the xApp detached and stop it by PID (see Troubleshooting for why):
docker exec -d -e PYTHONUNBUFFERED=1 python_xapp_runner sh -c \
"python3 -u ./kpm_mon_xapp.py --metrics=DRB.UEThpDl,DRB.UEThpUl --kpm_report_style=1 > /tmp/kpm.log 2>&1"
docker exec python_xapp_runner grep -c "Successfully subscribed" /tmp/kpm.log
Generate downlink traffic to the UE, then read the reports:
docker exec python_xapp_runner sh -c \
"grep -oE '(ColletStartTime|DRB.UEThp[DU]l, Value): .*' /tmp/kpm.log | paste - - - | tail"
ColletStartTime: 2026-09-22 17:37:57 DRB.UEThpDl, Value: [132931.0] DRB.UEThpUl, Value: [219.0]
What to look for: one line per second, with a nonzero DRB.UEThpDl while traffic flows.
Values are kbit/s — 132931.0 is about 133 Mbit/s. On the Benetel rig (platform B, O-RAN 7.2 +
SD-Core) this read 226–239 Mbit/s while iperf3 measured 208 — the RAN counts RLC payload with
headers, iperf3 counts TCP goodput.
Report style 5 names UEs in the subscription. The command subscribes a range of UE identifiers
(0–7) because the identifier the gNB uses is not predictable — see Troubleshooting.
docker exec -d -e PYTHONUNBUFFERED=1 python_xapp_runner sh -c \
"python3 -u ./kpm_mon_xapp.py --metrics=DRB.UEThpDl,DRB.UEThpUl --kpm_report_style=5 \
--ue_ids=0,1,2,3,4,5,6,7 > /tmp/kpm5.log 2>&1"
Each report then names the UE:
--UE_id: 4 ---Metric: DRB.UEThpDl, Value: [201969.0]
What to look for: the --UE_id: value that carries traffic. That is the identifier to use in
any later per-UE subscription on this gNB, until the UE re-attaches.
Stop an xApp when you are done. The xApp container has no ps or pkill, so the loop finds the
xApp's host PID with docker top and signals only that process:
for p in $(docker top python_xapp_runner -eo pid,args | awk '/kpm_mon_xapp/{print $1}'); do kill $p; done
| Check | Command | Healthy result |
|---|---|---|
| E2 association (gNB host) | ss -np -A sctp \| grep 36421 |
ss -A sctp shows ESTAB to the RIC on 36421 |
| E2 node (RIC) | curl -s http://10.130.2.11:3800/v1/nodeb/states |
e2mgr /v1/nodeb/states lists the gNB as CONNECTED |
| Subscription | docker exec python_xapp_runner grep -c "Successfully subscribed" /tmp/kpm.log |
the xApp logs Successfully subscribed, the gNB logs Admitting action 1 |
| Reports | the Step 4 grep … \| paste - - - \| tail |
one RIC Indication Received per second while traffic flows |
| Values | compare with iperf3 at the UE |
KPM DL within roughly 10–15 % of iperf3 DL, slightly higher |
KPM values measured with this procedure, all on platform B (2× Intel Xeon Gold 6126, 24 cores /
48 threads), ocudu.ndz (OCUDU commit 7238f4f407), O-RAN SC RIC i-release, 2026-09-22. KPM
reads slightly above iperf3 because it counts RLC payload with headers.
| Rig | Core | KPM style 1, cell DL (Mbit/s) | KPM style 5, per-UE DL (Mbit/s) | iperf3 DL at the same time (Mbit/s) |
|---|---|---|---|---|
| Soft ZMQ (no radio) | Open5GS | — | 10.7–10.9 | a 10 Mbit/s DL stream |
| LiteOn FlexFi, O-RAN 7.2 | Aether SD-Core | 122–140 | 136–148 | 115–118 |
| Benetel RAN650, O-RAN 7.2 | Aether SD-Core | 226–239 | 188–206 | 208 |
# RIC host
cd oran-sc-ric && docker compose down
# gNB host: nothing to undo - remove the e2: block when you no longer want the agent
Then release the rig as its own tutorial describes (stop the DU, power the O-RU off, release the
reservation).
| Symptom | Cause | Fix |
|---|---|---|
| The xApp prints nothing | output buffering | run python3 -u with PYTHONUNBUFFERED=1, as in Steps 4–5 |
The next xApp run dies with OSError: [Errno 98] Address already in use |
a leftover xApp: timeout docker exec … kills only the client; the xApp keeps running and holds its HTTP port |
the container has no ps or pkill — find it from the host with docker top and kill by PID (end of Step 5) |
Per-UE (style 5) subscription admitted, but zero indications and no error, while the gNB scheduler still logs ue=0 |
the UE IDs in a style-5 subscription are the gNB's per-attachment IDs, not the DU's UE index; the xApp defaults to [0, 1], which is right only for the first attachment on a freshly started gNB |
subscribe a range (--ue_ids=0,…,7) and read the --UE_id: line, or prove the path with report style 1 first |
| Values read 0.0 | the KPI is an average over the sample, so it is zero unless traffic flows during it; or the traffic generator is not running — two iperf3 clients against one server port leave the second one failing quietly | run traffic during the report period; one iperf3 client per server port |
SCTP association ESTAB on 36421, but e2mgr does not list the gNB as CONNECTED |
E2 Setup did not complete over the association | an established socket is not proof; check e2mgr (Step 3) and the gNB log. The SD-RAN variant of this is open — see the RIC page |
| UE data path on the gNB/core host breaks after installing the RIC there | Docker sets the iptables FORWARD policy to DROP | run the RIC on a different host (Setup) |
| RIC containers unreachable or clash with COSMOS addresses | the bundle's default 10.0.2.0/24 lies inside COSMOS NJ's 10.0.0.0/10 |
remap to 10.130.2.0/24 (Step 1a) |
RIC Control Failure from the gNB |
E2SM-RC control is not supported by this OCUDU build | expected — see the RIC page |
On this wiki
External
e2: and metrics: configuration.Author(s): COSMOS team · Last verified: 2026-09-22 (O-RAN SC RIC i-release, OCUDU commit 7238f4f407; soft ZMQ + Open5GS, LiteOn FlexFi + SD-Core, Benetel RAN650 + SD-Core) · text revised 2026-09-24 · Tested image/release: ocudu.ndz · Tags: 5g, oran, ric, e2, kpm, xapp, ocudu, srsran