Bring up a complete 5G Standalone network with the permissively-licensed OCUDU gNB (built on srsRAN Project) and a native Open5GS core on COSMOS, driving a USRP over the air on the sb1 or grid domains, then attach two different UEs to the same cell — a commercial Quectel 5G modem and Amarisoft's soft lteue — register each, open a PDU session, reach the internet, and measure real DL/UL throughput.
This tutorial builds a licence-free 5G SA cell using OCUDU — a combined CU/DU/RU built on srsRAN Project (standard srsRAN YAML config, no licence server) — paired with a native Open5GS 5G core. The gNB runs on an sb1 (or grid) compute node and drives a USRP (NI N310) over a real n41 (2.6 GHz) air interface.
Compared with the soft-only tutorial, two things are new. First, the radio is real: the gNB streams baseband samples to a software-defined radio (SDR) over the node's data network, and the SDR converts them to RF. That makes the gNB a real-time program — if the host cannot deliver samples on time the radio underflows, and the UE sees a degraded or dropped link. Second, the UE is no longer a matched soft UE from the same software family: it is a commercial off-the-shelf (COTS) modem with its own firmware and defaults, which exposes interoperability issues a soft-only rig never shows.
The point of the tutorial is cross-vendor interoperability: the same OCUDU cell and Open5GS core serve two completely different UEs back-to-back — a commercial off-the-shelf Quectel RM520N-GL modem and Amarisoft's software UE (lteue) running over a second USRP. You will learn the one core-side fix that makes non-srsUE UEs work (IPv4-only PDU sessions), why the testbed PLMN is 00101, and the RF-underflow fix that keeps the radio glitch-free. All UEs get an IP in the standard 192.168.100.0/22 pool with internet access.
As the Topology figure shows, the gNB node carries both the gNB and the core, so N2 and N3 stay on its loopback interface exactly as in the soft-only rig. What changes is the radio path: gNB → N310 over the data network → antenna → air → UE. The modem path (Path A) is the one verified by the three-mode parity harness; the soft lteue path (Path B) is manual only.
00101; the 5G SA fundamentals page explains the identities, and COTS UE: Quectel covers the modem and its SIM.isolcpus, show up as RF underflows. Real-time host preparation explains CPU isolation and IRQ pinning in depth; this tutorial deliberately runs without isolcpus.After completing this tutorial you will be able to:
sb1/grid.lteue to the same cell/core.| Learning path | Step 2 of 7 — Over the air with an SDR. Previous: OCUDU soft-only. Next: OCUDU O-RAN 7.2 with ProtO-RU. Same step on the other stacks: Duranta + OAI CN5G, Amarisoft SDR |
| Difficulty | Advanced |
| Estimated time | 60–90 min |
| Domain / sandbox | sb1 or grid (USRP radios) |
| Topic group | Cellular (4G/5G/O-RAN) |
| Last verified | 2026-09-16 — three-mode parity green on sb1 (gNB srv1-lg1 + N310 sdr1-s1-lg1 + modem sdr1-in3), image ocudu-20260915, no isolcpus · text revised 2026-09-24 |
Background knowledge
ssh/ip/iperf3 on Linux.Account & access
sb1 (or grid) for the gNB node, the USRP(s), and the UE node — see Make a reservation.Devices / nodes
| Resource | Role | Qty | Notes |
|---|---|---|---|
| a compute node with a USRP | OCUDU gNB (gnb) + Open5GS core |
1 | drives the gNB N310 (e.g. 10.39.2.1 for sdr1-s1-lg1) |
| a UE node | UE host — Quectel modem or soft lteue |
1 | one UE at a time on the same cell |
| NI N310 | gNB radio | 1 | band n41, 40 MHz, 1T1R |
| NI N310 or X310 | soft-UE radio (for lteue) |
1 | driven by lteue over UHD |
| Quectel RM520N-GL | commercial 5G UE | 1 | USB on the UE node; AT /dev/ttyUSB2, QMI wwan0 |
Disk images
| Image | Load onto | Provides |
|---|---|---|
ocudu.ndz |
gNB node | OCUDU gnb (/opt/ocudu), Open5GS (/opt/open5gs), launch scripts, UHD 4.9, IRQ-affinity service |
lte5gue.ndz |
UE node | Amarisoft lteue, UHD 4.9 + trx_uhd, amari_cmd.py, UE configs (soft-UE path); qmicli (modem path) |
(ocudu.ndz / lte5gue.ndz are symlinks to the latest dated builds.)
Software components
| Component | Version | Source |
|---|---|---|
| OCUDU gNB (srsRAN Project) | preinstalled | /opt/ocudu/build/apps/gnb/gnb — no license needed |
| Open5GS 5GC | build-tree | /opt/open5gs, launcher /root/open5gs/start.sh |
Amarisoft lteue |
2026-06-12 | preinstalled in the UE image (soft-UE path only) |
qmicli (libqmi) |
distro | preinstalled on the UE host (COTS-modem path) |
Spectrum / RF / special
00101 (MCC 001 / MNC 01). Soft UEs (lteue, srsue) can only form a 2-digit MNC, so 00101 is the canonical testbed PLMN. UE pool 192.168.100.0/22, gateway 192.168.100.1, internet-NAT'd.
Figure — The three COSMOS modalities. This tutorial is the over-the-air one: gNB and SDR on one side, the air in the middle, a UE on the other side; the core stays on the gNB node.
The gNB + core run on an sb1/grid node with a local USRP; a second node hosts the UE (modem on USB, or soft lteue on a second USRP):
gNB node (sb1 / grid) UE node
┌───────────────────────┐ ┌────────────────────────┐
│ OCUDU gnb (srsRAN) │ n41 2.6 GHz │ Quectel RM520N-GL │
│ Open5GS 5GC │◄─────────────►│ —or— │
│ ogstun 192.168.100.1 │ USRP N310 │ lteue → 2nd USRP │
│ AMF 127.0.0.5:38412 │ (OTA) │ │
└───────────────────────┘ └────────────────────────┘
Walk through it by plane. The control plane (N2/NGAP) runs from the gNB to the AMF at 127.0.0.5:38412 on the gNB node's loopback, as in the soft-only rig. The user plane (N3/GTP-U) also stays on the gNB node, from the gNB to the UPF and out through ogstun (192.168.100.1) with NAT to the internet. The radio path is new: the gNB streams I/Q samples to the N310 over the node's data network (the radio's data-plane address, for example 10.39.2.1), the N310 transmits in band n41, and the UE receives the cell over a conducted or shielded RF path. On the UE side, the Quectel modem attaches over USB and exposes a QMI data interface (wwan0); the soft lteue instead drives a second USRP and puts its data interface in a network namespace. There is no fronthaul network in this modality — the SDR link is sample streaming, not O-RAN 7.2.
OCUDU needs no licence server — you launch the binary.
ssh <username>@console.<domain>.cosmos-lab.org # e.g. console.sb1... or console.grid...
-r 0):omf load -i ocudu.ndz -t <gnb-node> -r 0 -o 1200
omf load -i lte5gue.ndz -t <ue-node> -r 0 -o 1200
omf tell -a on -t <gnb-node>,<ue-node>
Why PLMN 00101? A soft UE splits its IMSI as MCC(3)+MNC(2)+MSIN(10) — it cannot form a 3-digit MNC. Pointing one at a 3-digit PLMN makes it emit a mismatched SUCI → the AMF returns Registration reject [95]. Only a COTS modem can do a 3-digit MNC. So the testbed standardises on 00101.
The core starts first because the gNB sends its NG Setup Request once, at start-up. The launcher starts the network functions, creates ogstun with NAT and provisions subscribers; the ss check confirms the AMF is listening.
ssh root@<gnb-node>
/root/open5gs/start.sh # NFs + ogstun 192.168.100.1/22 + NAT→internet + provisions subscribers
ss -lnp -A sctp | grep 38412
LISTEN 0 0 127.0.0.5:38412 0.0.0.0:* users:(("open5gs-amfd",...))
What to look for: the LISTEN socket on 127.0.0.5:38412 owned by open5gs-amfd.
The launcher provisions subscribers from subscriber_db.csv, sets up ogstun on 192.168.100.1/22 with a MASQUERADE rule (so UEs reach the internet), and pins the NFs to housekeeping cores.
Cross-vendor PDU fix — do this once. The srsRAN/OCUDU gNB supports IPv4 only. Amarisoft
lteueand many COTS modems request a dual-stack IPv4v6 PDU session by default, which the gNB rejects every time ([NGAP] Unsupported PDU Session Type: ipv4v6). Force each subscriber's session to IPv4 (type:1) with the command below.
mongosh open5gs --quiet --eval 'db.subscribers.updateMany({}, {$set: {"slice.0.session.0.type": 1}})'
This edits every subscriber in the Open5GS database: type: 1 is IPv4. The core then answers any IPv4v6 request with an IPv4 session, which the gNB accepts.
Also provision the modem's SIM (IMSI 001010000000032) if it is not already present — see the modem path below.
The gNB needs three things to match reality: the radio's type, clock and address (ru_sdr), the core's PLMN and TAC (cu_cp.amf), and the cell (cell_cfg). This step checks the radio and the core, then writes a config that agrees with both.
Warning: Do not use the image's
/root/ocudu/gnb-ocudu-40.yml. It is an old X310 file (type=x300, 184.32 MHz master clock, 46.08 Msps) with PLMN 310014 and TAC 1. Against this image's core (PLMN 00101, TAC 7) the gNB exits withNG Setup … unknown-PLMN-or-SNPN/CU-CP failed to connect to AMF— and fixing only the PLMN is not enough, because the TAC must match too. An N310 cannot run thosedevice_argsat all: it has no 184.32 MHz master clock. Write the config below instead.
The configuration below is the one the three-mode parity run uses (gNB srv1-lg1.sb1 freshly loaded with ocudu.ndz, no isolcpus, N310 sdr1-s1-lg1, modem on sdr1-in3.sb1): NG Setup succeeds and all three modes run with 0 RF real-time failures — see Same experiment, same result below.
First find your N310's data-plane address, and confirm it is an N310 that nobody holds (type: n3xx, claimed: False). The address is per radio — for example sdr1-s1-lg1 on sb1 is 10.39.2.1:
USRP=10.39.2.1 # <-- your reserved N310
uhd_find_devices --args "addr=$USRP" | grep -E 'serial|claimed|product|type'
[INFO] [UHD] linux; GNU C++ version 13.3.0; Boost_108300; UHD_4.9.0.0+ds1-1~noble2
serial: 3176DF5
claimed: False
product: n310
type: n3xx
What to look for: type: n3xx and claimed: False. claimed: True means another process — possibly another user's — holds the radio; do not start the gNB against it.
Check what the core expects — the gNB's PLMN and TAC must match these:
grep -A4 'tai:' /opt/open5gs/etc/amf.yaml
tai:
- plmn_id:
mcc: 001
mnc: 01
tac: 7
Now write the gNB config — n41, 40 MHz, 30 kHz SCS, 1T1R; an N310 needs the 122.88 MHz master clock (61.44 Msps = 122.88/2) and a frame size of 7900 (8000 silently breaks the uplink on an N3xx):
cat > /root/ocudu/gnb-ocudu-n310.yml <<EOF
cu_cp:
amf:
addrs: 127.0.0.5
port: 38412
bind_addrs: 127.0.0.1
supported_tracking_areas:
- tac: 7
plmn_list:
- plmn: "00101"
tai_slice_support_list:
- sst: 1
ru_sdr:
device_driver: uhd
device_args: type=n3xx,addr=${USRP},master_clock_rate=122.88e6,recv_frame_size=7900,send_frame_size=7900
srate: 61.44
otw_format: sc16
tx_gain: 25
rx_gain: 40
cell_cfg:
dl_arfcn: 519000
band: 41
channel_bandwidth_MHz: 40
common_scs: 30
plmn: "00101"
tac: 7
pci: 1
log:
filename: /tmp/gnb.log
all_level: info
pcap:
mac_enable: false
ngap_enable: false
EOF
grep -E 'tac|plmn|device_args' /root/ocudu/gnb-ocudu-n310.yml
What to look for: the final grep shows tac: 7 and plmn: "00101" in both the cu_cp and cell_cfg blocks, and device_args carries your N310's real address (the heredoc expands ${USRP}; an empty addr= means the variable was not set in this shell).
The gNB now connects to the AMF, opens the radio and puts the cell on the air. The loop waits up to 90 s for the start-up banner; the last three commands show NG Setup, the SCTP association to the AMF, and the count of RF real-time failures.
srsRAN writes its own log (log.filename above, /tmp/gnb.log) — that file carries gNB started, NG Setup and any RF failure. Send the process's console output somewhere else, or the two interleave into one unreadable file. Keep stdin open (sleep infinity |): the gNB exits when its stdin closes.
cd /opt/ocudu/build
setsid bash -c 'sleep infinity | ./apps/gnb/gnb -c /root/ocudu/gnb-ocudu-n310.yml' >/tmp/gnb-console.log 2>&1 &
for i in $(seq 1 30); do grep -aq 'gNB started' /tmp/gnb.log 2>/dev/null && break; sleep 3; done
grep -aE 'NG Setup|gNB started' /tmp/gnb.log | tail -3
ss -np -A sctp | grep -E 'ESTAB.*38412'
grep -ac 'Real-time failure in RF' /tmp/gnb.log
2026-09-10T12:48:01.100880 [NGAP ] [I] "NG Setup Procedure" finished successfully
2026-09-10T12:48:01.142060 [GNB ] [I] ==== gNB started ===
ESTAB 0 0 127.0.0.5:38412 127.0.0.1:... users:(("open5gs-amfd",...))
3
What to look for: gNB started plus an established SCTP association = NG Setup succeeded and the cell is on air. The last number is the count of RF real-time failures: it should stay at or near 0 (see RF underflow below). If NG Setup fails, /tmp/gnb-console.log holds the reason.
Warning: Stop the gNB with the scoped stop under Cleanup, never with
pkill.pkill -f apps/gnb/gnbmatches (and kills) your own SSH shell; a name match such aspkill -x gnbwould also kill another user's gNB on a shared host.
RF underflow. If the gNB log shows
[RF] Real-time failure in RF: underflowand UEs see CQI 1–2 / drop mid-registration, the NIC IRQs are landing on the gNB PHY cores.ocudu-irq-affinity.service(check withsystemctl is-enabled ocudu-irq-affinity) pins the DATA-iface IRQs to housekeeping cores andopen5gs/start.shpins the NFs there too; the bundle'socudu-ota_gnbfix.shadditionally moves the USRP-NIC IRQs onto that NIC's own NUMA node and restarts the gNB once on an RF-failure flood. IRQ pinning alone does not always stop the flood on every host; with the IRQ service, noisolcpusand the bundle's fix-up, all three parity modes run with 0 RF failures.
Warning: Do not reuse a gNB host that still carries
isolcpusfrom an O-RAN 7.2 run: this gNB is not pinned to the isolated cores, so it gets only the non-isolated ones and stalls. Re-loadocudu.ndz(the image carries noisolcpus) or remove the isolation as in Removing isolation;grep -o 'isolcpus=[^ ]*' /proc/cmdlinemust print nothing.
The modem path has two parts: the core must know the SIM (A1), and the modem must be told to use SA mode, this band and this PLMN, and then bring up a data call (A2). The general Quectel procedures — AT commands versus QMI, SA-only mode, routing and recovery — are covered on COTS UE: Quectel; this section gives the values this tutorial was verified with, including its band mask (n41).
This writes one subscriber record into the Open5GS database: the IMSI, one slice (sst:1) with an IPv4 (type:1) session on the internet DNN, and the authentication credentials the core uses to challenge the SIM.
mongosh open5gs --quiet --eval '
db.subscribers.deleteMany({imsi:"001010000000032"});
db.subscribers.insertOne({imsi:"001010000000032",
slice:[{sst:1,default_indicator:true,session:[{name:"internet",type:1,
ambr:{uplink:{value:1,unit:3},downlink:{value:1,unit:3}},
qos:{index:9,arp:{priority_level:8,pre_emption_capability:1,pre_emption_vulnerability:1}}}]}],
ambr:{uplink:{value:1,unit:3},downlink:{value:1,unit:3}},
security:{k:"<K>",amf:"9001",
op:"<OP>",opc:null},
subscribed_rau_tau_timer:12, network_access_mode:2, subscriber_status:0,
access_restriction_data:32, schema_version:1, __v:0});'
The key values are left out of this page on purpose: the core image provisions the COSMOS SIMs itself from its SIM database (cosmos-sim-db), so for a COSMOS SIM read <K> and <OP> (or <OPc>, with opc set instead of op) from there, and for your own SIM use its values. Use deleteMany, not deleteOne: the image's SIM catalogue can hold two records for the same IMSI, and Open5GS authenticates against whichever it finds first.
ModemManager is stopped first so that it does not hold the AT port. A short Python block then sends the AT commands — SA-only mode, a band mask including n41, an IPv4 APN, a radio off/on cycle, and a manual lock onto PLMN 00101 — and after registration the shell switches wwan0 to raw-IP mode, starts a QMI data call, and adds the address and a scoped route. This tutorial's block is reproduced verbatim on the COTS UE page: run Option B — AT commands by hand and then Bringing up data on <ue-node>, with this tutorial's values: AT port /dev/ttyUSB2, band mask 41:77:78 (this cell is n41; a mask left at 77:78 by a 7.2 run never sees it), APN internet (IPv4, in both AT+CGDCONT and --wds-start-network), PLMN lock AT+COPS=1,2,"00101",12, address $IP/22 with MTU 1400, and the scoped route 192.168.100.0/22 dev wwan0. Test with ping -I wwan0 to 192.168.100.1 (core gateway) and 8.8.8.8 (the internet).
+C5GREG: 0,1 # registered, home, on NR5G-SA 001/01
IPv4 address: 192.168.100.2
5 packets transmitted, 5 received, 0% packet loss (8.8.8.8)
What to look for: +C5GREG: 0,1 — the second field 1 means registered on the home network. Any other value (for example 2, searching) means the modem has not registered and the data call will fail. Then an IPv4 address from 192.168.100.0/22 and 0 % loss to 8.8.8.8.
Warning: Use only the scoped route — a
default via wwan0black-holes the node's management SSH (Routing without cutting off the node). If the first ping batch shows 100 % loss, the modem is leaving RRC idle; the next batch succeeds.
Warning: Registered, PDU up, IP on
wwan0— and every ping still lost? The modem's USB data endpoint is stuck;ip -s link show wwan0shows TX errors rising by one per packet. The diagnosis and the USB device reset that clears it are on Detecting a dead USB data path. The bundle's helper does it for you:ocudu-ota_modem.py configurefirst resets the modem ifwwan0shows TX errors, andpython3 ocudu-ota_modem.py usbheal wwan0does only that step. The same helper switches off the active carrier MBN profile after the last radio cycle and waits for+CPIN: READYbefore the PLMN lock (Configuring Standalone-only operation).
lteue (soft UE, over a second USRP)The soft UE runs on the UE node and drives its own USRP, so the same cell is received by a completely different UE implementation. lteue needs jumbo frames on the interface toward its USRP (DATA2, MTU 9000) and does not attach on its own: the power_on command sent over its remote API starts the attach. Its data interface pdn0 sits in the ue1 namespace.
ssh root@<ue-node>
cd /opt/amarisoft/lteue-linux-2026-06-12
ip link set DATA2 mtu 9000
setsid bash -c "tail -f /dev/null | ./lteue-avx2 config/ue-ocudu-00101.cfg" >/tmp/ue_run.log 2>&1 &
sleep 12
python3 amari_cmd.py 9002 '{"message":"power_on","ue_id":1}' # lteue does NOT auto-attach; retry if it races
sleep 10
ip netns exec ue1 ip -br addr show pdn0
ip netns exec ue1 ping -c 4 192.168.100.1
ip netns exec ue1 ping -c 4 8.8.8.8
What to look for: pdn0 holds an address in 192.168.100.0/22 and both pings show 0 % loss.
The soft UE registers and gets an IP in 192.168.100.0/22 — same cell, same core as the modem. After stopping lteue, wait ~20 s for the USRP MPM claim to release before relaunching, or the next lteue aborts with Someone tried to claim this device again.
Danger: Amarisoft licences are one seat per tag across the whole testbed, and a leftover Amarisoft client anywhere makes other users' Amarisoft processes segfault. Stop
lteuewhen you finish (see Cleanup) and do not leave it running between sessions.
Path A above is Mode 1 (manual CLI). The same experiment ships as an Ansible playbook and a Jupyter notebook that read one parameter file, share one log parser and emit one JSON result contract, so the three can be checked against each other. (Path B, the soft lteue, is manual-only for now.)
On the console of your domain (console.sb1.cosmos-lab.org):
cd ~ && wget -q https://www.cosmos-lab.org/files/cosmos-tutorials-ocudu-ota.zip
unzip -o cosmos-tutorials-ocudu-ota.zip # extracts into ~/tutorials/
cd ~/tutorials && ls ocudu-ota
| File | Purpose |
|---|---|
ocudu-ota_params.yml |
the single parameter source: radio node + USRP address, N3xx device args and sample rate, PLMN/IMSI/K/OP, APN, gains, timeouts |
ocudu-ota_gnb.yml.tmpl |
the gNB config template; ocudu-ota_lib.py render-gnb fills it from the params (the image's gnb-ocudu-40.yml is an X310 / PLMN 310-014 file — do not use it as-is) |
ocudu-ota.yml |
Mode 2 — Ansible: cosmos_preflight, radio power-cycle from the console (omf), Open5GS + SIM provisioning + IPv4-only PDU fix, gNB as a named unit, modem registration, QMI data call, dual-target measurement, contract |
ocudu-ota_manual.sh |
Mode 1 scripted: the exact commands of Path A, node by node |
ocudu-ota_tutorial.ipynb |
Mode 3 — the notebook (kernel on the console, papermill-runnable) |
ocudu-ota_modem.py |
the Quectel AT helper: USB self-heal of the data path (usbheal), SA-only, band mask, APN, carrier-MBN off, PLMN lock after +CPIN: READY, registration wait with one radio kick |
ocudu-ota_gnbfix.sh |
gNB-host fix-up: USRP-NIC IRQs on their own NUMA node, RF real-time-failure flood detection with one restart |
ocudu-ota_lib.py |
shared parser + result contract + the parity comparison spec |
ocudu-ota_parity.sh |
runs all three modes on the same two nodes, asserts they agree, and runs the teardown on every exit (pass, fail or Ctrl-C) |
ocudu-ota_teardown.yml |
clean teardown (data call down, gNB/iperf units and Open5GS down; the radio is left powered) |
OMF_NODESis required and ordered: first node = gNB host (theocudu.ndznode with the USRP), second = UE host (the modem). The radio is not inOMF_NODES— it is named in the params (ocudu_sdr_node,ocudu_usrp_addr) because it is a shared sandbox resource that the playbook power-cycles withomffrom the console: reserve it and coordinate with its other users first.
Two traps the bundle handles for you, both in the shipped config. The image's
gnb-ocudu-40.ymldeclares TAC 1 while its Open5GSamf.yamlserves TAC 7 — that pair fails NG Setup withunknown-PLMN-or-SNPNand the gNB exits ("CU-CP failed to connect to AMF"). It also targets an X310 (type=x300, 46.08 Msps) and PLMN 310-014. The bundle therefore renders its own gNB config fromocudu-ota_params.yml(N3xx device args, 61.44 Msps, PLMN 00101, TAC 7). Second: srsRAN writes==== gNB started ===and anyReal-time failure in RFline to its log file (log.filename), not to stdout — watch that file, not the console output.
cd ~/tutorials
export OMF_NODES="srv1-lg1,sdr1-in3" # gNB host, UE host
ansible-playbook ocudu-ota/ocudu-ota.yml
export OMF_NODES="srv1-lg1,sdr1-in3"
/opt/cosmos-jupyter/venv/bin/papermill ocudu-ota/ocudu-ota_tutorial.ipynb ocudu-ota/ocudu-ota_tutorial-out.ipynb
bash ocudu-ota/ocudu-ota_parity.sh srv1-lg1 sdr1-in3
CPU platform (gNB host): 2x Intel(R) Xeon(R) Gold 6226 CPU @ 2.70GHz, 24 cores / 24 threads
=== ocudu-ota parity ===
mode ue_ipv4 rsrp_dbm sinr_db rf_failures ping_rtt_ms ping_remote_loss_pct dl_mbps ul_mbps
----------------------------------------------------------------------------------------------
ansible 192.168.100.2-104 12 0 25.68 0.0 65.7 59.5
manual 192.168.100.2-105 12 0 36.01 0.0 66.4 59.2
jupyter 192.168.100.2-104 12 0 31.18 0.0 66.0 59.0
dl_mbps spread max/min = 1.01 (tol 1.60)
ul_mbps spread max/min = 1.01 (tol 1.80)
PARITY: PASS (same experiment in all modes; all checks green)
artifacts kept in: /tmp/ocudu-ota-parity-seskar.jsobbk
### releasing the rig (teardown)
rig released
(Real output, 2026-09-14. The table runs the UE address into the next column: 192.168.100.2, RSRP -104.)
What to look for: PARITY: PASS, rf_failures 0 in every mode, and rig released at the end. The RSRP (reference signal received power, about −104 dBm) and SINR (12 dB) columns describe the radio link the modem measured.
Pass criteria: every mode reaches gNB started with NG Setup established, registers (+C5GREG: 0,1) and gets a PDU-session address on the same PCI/band; local ping loss ≤ 20 %; DL in 20–400 and UL in 2–120 Mbit/s and within 60 %/80 % of each other across modes. (run on sb1, 2026-09-14: gNB srv1-lg1.sb1 + N310 sdr1-s1-lg1, modem on sdr1-in3.sb1, image ocudu.ndz, no isolcpus). Green parity is what sets this page's Last verified; re-running it after an image refresh is the regression test.
The harness always ends by running ocudu-ota_teardown.yml (the releasing the rig lines), because the three modes deliberately leave their units running for inspection. It also runs the notebook from the bundle's own directory (papermill --cwd), so it always runs the notebook from the same bundle as the rest of the harness, wherever the bundle is unpacked.
| Check | Command | Expected |
|---|---|---|
| Core listening | ss -lnp -A sctp \| grep 38412 (gNB node) |
LISTEN … 127.0.0.5:38412 … open5gs-amfd |
| gNB up, NG Setup done | grep -aE 'NG Setup\|gNB started' /tmp/gnb.log; ss -np -A sctp \| grep -E 'ESTAB.*38412' |
Cell pci=1 … band n41, gNB started, an established SCTP association on port 38412 |
| Radio healthy | grep -ac 'Real-time failure in RF' /tmp/gnb.log |
at or near 0, no sustained [RF] … underflow (IRQ-affinity service active) |
| Modem registered | AT+C5GREG? |
+C5GREG: 0,1 on 001/01; or lteue reaches 5GMM-REGISTERED |
| UE address | qmicli -d /dev/cdc-wdm0 --wds-get-current-settings (or ip netns exec ue1 ip -br addr show pdn0) |
an IP in 192.168.100.0/22 |
| User plane | ping -I wwan0 -c 5 192.168.100.1 and … 8.8.8.8 |
0% loss to both |
Measure air capacity with UDP (TCP is RTT-limited). Run an iperf3 server bound to ogstun and keep a keepalive ping running so the UE never drops to RRC-idle:
# on the gNB/core node:
iperf3 -s -B 192.168.100.1 -D
# on the UE node — Quectel path:
ping -I wwan0 -i 0.3 192.168.100.1 >/dev/null 2>&1 &
iperf3 -c 192.168.100.1 -B $IP -u -b 150M -R -t 12 # DL
iperf3 -c 192.168.100.1 -B $IP -u -b 40M -t 12 # UL
# lteue path: prefix each iperf3/ping with ip netns exec ue1
Measured (band n41, 40 MHz, 30 kHz SCS). Throughput is a property of the gNB host's CPU platform as much as of the radio, so every figure is listed against the platform that produced it:
| Date | gNB host CPU platform | Image | UE | Method | DL | UL |
|---|---|---|---|---|---|---|
| 2026-09-16 | 2× Intel Xeon Gold 6226 @ 2.70 GHz, 24 cores / 24 threads (no CPU isolation) | ocudu-20260915 |
Quectel RM520N-GL | iperf3 TCP, receiver line, 10 s — the parity bundle, three modes |
58.1–58.7 Mbit/s | 58.8–59.1 Mbit/s |
| 2026-09-14 | 2× Intel Xeon Gold 6226 @ 2.70 GHz, 24 cores / 24 threads (no CPU isolation) | ocudu.ndz |
Quectel RM520N-GL | iperf3 TCP, receiver line, 10 s — the parity bundle, three modes |
65.7–66.4 Mbit/s | 59.0–59.5 Mbit/s |
| 2026-09-08 | 2× Intel Xeon Gold 6226 @ 2.70 GHz, 24 cores / 24 threads | ocudu.ndz |
Quectel RM520N-GL | same | 55.5–57.8 Mbit/s | 54.0–54.7 Mbit/s |
| 2026-07 | not recorded | ocudu.ndz |
Quectel RM520N-GL | UDP, as above | ≈120 Mbit/s | ≈20 Mbit/s |
| 2026-07 | not recorded | ocudu.ndz + lte5gue.ndz |
Amarisoft lteue over a second USRP |
UDP, as above | ≈14–50 Mbit/s | ≈20–30 Mbit/s |
UL is bounded by the TDD pattern. The TCP figures are what the three modes measure and compare; the UDP commands above measure raw air capacity instead.
If you used the bundle (Mode 2 or 3), run its teardown — it stops the data call, the ocudu-gnb / ocudu-iperf units and Open5GS, and hands the modem back to ModemManager; the radio is left powered. The parity harness already does this on every exit. The bundle launches the gNB and the iperf3 server with systemd-run --unit=…, so the teardown stops them by unit — its task is named "Stop the supervised units (exact handles -- no pkill -f)" and runs systemctl stop {{ item }} 2>/dev/null; true over [ocudu-gnb, ocudu-iperf].
cd ~/tutorials && export OMF_NODES="srv1-lg1,sdr1-in3"
ansible-playbook ocudu-ota/ocudu-ota_teardown.yml
After the manual path the processes have no systemd unit, so stop them as process groups identified by their executable. Define this helper in your shell on each node you log into:
stop_exe() { # signal the process GROUP of every process executing exactly $1 (default TERM)
local exe sig=${2:-TERM} d st pgid
exe=$(readlink -f "$1")
for d in /proc/[0-9]*; do
[ "$(readlink "$d/exe" 2>/dev/null)" = "$exe" ] || continue
st=$(cat "$d/stat" 2>/dev/null) || continue
st=${st##*) } # drop "pid (comm) ", safe for any comm
pgid=$(echo "$st" | awk '{print $3}') # remaining fields: state ppid pgrp
kill -"$sig" -- "-$pgid" 2>/dev/null
done
}
The function identifies a process by the binary it is executing (/proc/<pid>/exe), which a command line cannot fake, and signals its whole process group. Each launch above used setsid, so the group is exactly that pipeline — the binary plus its own sleep infinity / tail -f /dev/null keep-alive — and nothing else on the host. It sends TERM first, so the program can release the radio; if something is still running after a few seconds, repeat the call with KILL as the second argument.
On the gNB node, stop the gNB and the iperf3 server, then the core (/usr/bin/iperf3 matches every iperf3 on the node — on your reserved gNB node that is only yours):
ssh root@<gnb-node>
stop_exe /opt/ocudu/build/apps/gnb/gnb
stop_exe /usr/bin/iperf3
/root/open5gs/stop.sh
On the UE node, stop lteue (if you ran Path B) and the modem's data call:
ssh root@<ue-node>
stop_exe /opt/amarisoft/lteue-linux-2026-06-12/lteue-avx2
qmicli -d /dev/cdc-wdm0 --wds-stop-network=disable-autoconnect 2>/dev/null
Then, from the console, power both nodes off:
omf tell -a offh -t <gnb-node>,<ue-node>
| Symptom | Cause | Fix |
|---|---|---|
gNB: NG Setup … unknown-PLMN-or-SNPN |
core PLMN ≠ gNB plmn |
align both to 00101; restart the gNB (NG Setup is sent once) |
gNB exits with CU-CP failed to connect to AMF using the image's gnb-ocudu-40.yml |
that file declares PLMN 310014 and TAC 1 and targets an X310; the core serves 00101 / TAC 7 | write the N310 config of Step 2; PLMN and TAC must match amf.yaml |
Registration OK but no PDU; Unsupported PDU Session Type: ipv4v6 |
UE requested dual-stack; gNB is IPv4-only | set subscriber session.type:1 (Step 1), power-cycle the UE |
Soft UE: Registration reject [95] |
3-digit MNC on a soft UE | use PLMN 00101 end-to-end |
| gNB relaunch → SSH exit 255, no output | pkill -f apps/gnb/gnb killed your shell |
stop the gNB with stop_exe /opt/ocudu/build/apps/gnb/gnb (Cleanup) |
[RF] … underflow, UE CQI 1–2 |
NIC IRQs on the PHY cores | ensure ocudu-irq-affinity.service is active |
gNB aborts: free_buff_pool::pop … Assertion |
USRP streaming wedged (shared radio switched stacks) | omf tell -a reset the USRP host, then one clean start |
Modem detects the cell but won't camp (SEARCH) |
nr5g_disable_mode=1 disables SA on RM520N-GL |
set nr5g_disable_mode to 0; lock AT+COPS=1,2,"00101",12 |
| Modem never sees the n41 cell after a 7.2 run | its band mask was left at 77:78 (n78) |
AT+QNWPREFCFG="nr5g_band",41:77:78 (Step A2) |
PLMN lock fails with +CME ERROR: 13 |
the lock was sent before the SIM reported +CPIN: READY |
wait for +CPIN: READY; the bundle's ocudu-ota_modem.py does |
UE ping 8.8.8.8 fails, gateway pings |
no internet NAT | open5gs/start.sh adds MASQUERADE -s 192.168.100.0/22 ! -o ogstun |
| Node management SSH lost after the data call | a default route via wwan0 |
use only the scoped route 192.168.100.0/22 dev wwan0 |
lteue aborts: Someone tried to claim this device again |
previous lteue didn't release the USRP claim |
stop it with SIGTERM, not -9 (stop_exe defaults to TERM), and wait ~20 s before relaunch |
Modem registered, PDU up, IP on wwan0, but 100 % ping loss both ways; the gNB never sees a scheduling request |
the modem's USB data endpoint is stuck (-EPROTO): ip -s link show wwan0 shows TX errors rising by one per packet and TX packets 0 |
USB device reset of the modem (a qmi_wwan rebind is not enough); the bundle's ocudu-ota_modem.py configure does it automatically, ocudu-ota_modem.py usbheal wwan0 on its own |
Ansible stops at the preflight: OMF_NODES selected 1 node(s) … but this tutorial needs exactly 2 |
omf stat reports one node as Unknown (UNREACHABLE) — its BMC is not answering — and cosmos-inventory.py only lists POWERON nodes, so it silently drops it even though it answers ssh |
report the node to the testbed admins; meanwhile run with a static inventory — ANSIBLE_INVENTORY=~/static-inv.yml with a YAML group nodes listing the gNB host first (see below) |
Ansible never reaches NG Setup, or the other modes stall after DL (UL iperf3 … Connection timed out) |
the gNB host still carries isolcpus from an O-RAN 7.2 run, so this gNB gets only the non-isolated cores |
re-load ocudu.ndz or restore the GRUB command line and reboot; /proc/cmdline must show no isolcpus |
| One mode's DL test runs much longer than 10 s at a low rate and its UL test then cannot connect, while the other modes are fine | a real-time slip on the gNB host (Real-time failure in FAPI: Received late DL_TTI.request, Downlink data late in the gNB log) — timing, not the modem (wwan0 TX errors stay 0) |
re-run the parity; keep other load off the gNB host |
A manual AT session right after the bundle gets device reports readiness to read but returned no data |
the teardown hands the modem back to ModemManager, which then holds the AT ports | systemctl stop ModemManager first (the bundle's modes do this themselves) |
Static inventory for the UNREACHABLE case (first host = gNB host, second = UE host):
# ~/static-inv.yml
all:
children:
nodes:
hosts:
srv1-lg1.sb1.cosmos-lab.org:
sdr1-in3.sb1.cosmos-lab.org:
vars:
ansible_user: root
cd ~/tutorials && ANSIBLE_INVENTORY=~/static-inv.yml ./ocudu-ota/ocudu-ota_parity.sh srv1-lg1 sdr1-in3
The modem rows are explained in more depth, with the full recovery ladder, under Troubleshooting on the COTS UE page.
To place a failure in the attach sequence (cell search, random access, registration, authentication, PDU session), compare the last milestone reached with the flow on the 5G SA fundamentals page.
srsue + Open5GS over a ZMQ virtual radio on one node — no USRP. See the OCUDU soft-only tutorial.gnb-ocudu-40-l3.yml (recv/send_frame_size 7900) and ip route replace 10.39.0.0/16 via <data-gw> dev <data-iface> so the USRP subnet never falls back to the control net.On this wiki
External
ru_sdr and cell_cfg.lteue documentation.Author(s): COSMOS team · Last verified: 2026-09-16 (three-mode parity on sb1, modem path; first verified 2026-07-15) · text revised 2026-09-24 · Tested image/release: ocudu.ndz (OCUDU srsRAN + Open5GS) + lte5gue.ndz, PLMN 00101, pool 192.168.100.0/22 · Bundle: cosmos-tutorials-ocudu-ota.zip · Tags: ocudu, srsran, open5gs, 5g, sa, sdr, usrp, quectel, lteue, cross-vendor