Bring up a complete 5G Standalone (SA) network with the Duranta gNB (an OpenAirInterface fork, nr-softmodem) and the OAI CN5G core on COSMOS, driving a USRP over the air on the sb1 or grid domains, then attach a commercial Quectel 5G modem — register it, open a PDU session, reach the internet, and measure real DL/UL throughput.
This tutorial takes the software stack of step 1 and puts a real radio link under it. The same Duranta gNB (nr-softmodem, configured with an OAI libconfig .conf file) and the same OAI CN5G core (the oai-amf / oai-smf / oai-upf / … Docker stack) run on an sb1 (or grid) compute node. Instead of an RFsim socket, the gNB now streams baseband samples to an Ettus USRP N310, which transmits a band n41 (2.6 GHz) cell. The UE is no longer a program but a commercial Quectel RM520N-GL modem — a commercial off-the-shelf (COTS) UE with a programmed test SIM — on a second node.
Someone runs this configuration to study a 5G link with real RF impairments and a real handset protocol implementation, while keeping the whole base station in software where every log line is visible. It is also the configuration that exposes host real-time problems: an SDR must receive a continuous stream of samples, and a gNB thread that runs late shows up as transmit underflows and a UE that cannot decode the downlink.
The pieces talk as the Topology figure shows. The gNB and the core share one host and talk over the Docker bridge (N2 control, N3 user plane), exactly as in step 1. The gNB reaches the N310 over the sandbox's 10 GbE data network. The modem hears the cell over the air, registers with the core through the gNB, and its data session appears on the UE host as the wwan0 interface, driven through Qualcomm MSM Interface (QMI) commands.
The stack is standardized with the other COSMOS 5G platforms: PLMN 00101 and UE pool 192.168.100.0/22 with internet access. A single one-shot script (start-all.sh) brings up the gNB side. What is new compared with the Duranta soft-only tutorial: a shared radio that must be power-cycled and claimed correctly, a COTS modem that must be put into SA mode and locked to the test network, and RF performance that depends on the modem's radio state.
nr-softmodem on the host. That is why host CPU placement matters here: the samples must reach the radio on time. The COSMOS radios are described on SDR hardware; host real-time preparation in depth is on Preparing a real-time host.After completing this tutorial you will be able to:
nr-softmodem gNB + the OAI CN5G core driving a USRP on sb1/grid.LD_LIBRARY_PATH requirement and the dynamic AMF-IP handling.| Learning path | Step 2 of 7 — over the air with an SDR. Previous: Duranta soft-only · Next: Duranta 7.2 with a soft O-RU |
| 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-08 — three-mode parity green on sb1 (srv1-lg1 + N310 sdr1-s1-lg1 + modem sdr1-in3), image duranta-20260907c (Duranta 2026.w36) · text revised 2026-09-24 |
| Upstream source | Duranta (LF Networking, an OpenAirInterface-based RAN + UE stack) — repo github.com/duranta-project/openairinterface5g, tag 2026.w36 (commit b3930e39f5, 2026-09-04) + two COSMOS patches (PUSCH TA-alias guard; Benetel M-Plane get-xml/get-yang) |
Background knowledge
ssh/ip/iperf3 on Linux; basic Docker Compose.Account & access
sb1 (or grid) for the gNB node, the USRP, and the UE node — see Make a reservation.Devices / nodes
| Resource | Role | Qty | Notes |
|---|---|---|---|
| a compute node with a USRP | Duranta gNB (nr-softmodem) + OAI CN5G |
1 | drives the gNB N310 (e.g. 10.39.3.1). The radio is shared — reserve it and coordinate with other users of the sandbox; any N310 on the 10.39.x net works: USRP=<addr> /root/duranta/mk-gnb-oai-40.sh regenerates the config |
| a UE node | UE host — Quectel modem | 1 | USB; AT /dev/ttyUSB2, QMI wwan0 |
| NI N310 | gNB radio | 1 | band n41, 40 MHz, 1T1R |
| Quectel RM520N-GL | commercial 5G UE | 1 | SIM provisioned to a 00101 identity |
Disk images
| Image | Load onto | Provides |
|---|---|---|
duranta.ndz |
gNB node | Duranta nr-softmodem (/opt/duranta, USRP build), OAI CN5G (/opt/oai-cn5g), one-shot start-all.sh + provision.sql, UHD 4.9 |
lte5gue.ndz |
UE node | Quectel tooling (qmicli), UHD 4.9 |
(duranta.ndz / lte5gue.ndz are symlinks to the latest dated builds.)
Software components
| Component | Version | Source |
|---|---|---|
| Duranta gNB (OAI fork) | preinstalled | /opt/duranta/cmake_targets/ran_build/build/nr-softmodem — no license needed |
| OAI CN5G (5GC) | develop (Docker) |
/opt/oai-cn5g/oai-cn5g-fed/docker-compose, docker-compose-basic-nrf.yaml |
| UHD | 4.9.0.0+ds1-1~noble2 | preinstalled |
qmicli (libqmi) |
distro | preinstalled on the UE host |
Spectrum / RF / special
absoluteFrequencySSB 518670), 40 MHz (106 PRB), 30 kHz SCS, TDD (DDDSU). Conducted / shielded RF path on the rig.00101, DNN oai, IMSI 001010000000032 (modem). UE pool 192.168.100.0/22, gateway 192.168.100.1, internet-NAT'd (via oai-ext-dn).
Figure — this tutorial is the over-the-air modality: the gNB and core stay on one server, but the link to the UE now leaves the host through an SDR and crosses a real RF path to a commercial modem.
The gNB + core run on an sb1/grid node with a local USRP; the Quectel modem is on USB on the UE node:
gNB node (sb1 / grid) UE node
┌────────────────────────┐ ┌────────────────────────┐
│ Duranta nr-softmodem │ n41 2.6 GHz │ Quectel RM520N-GL │
│ OAI CN5G (Docker) │◄─────────────►│ │
│ UE pool 192.168.100/22 │ USRP N310 │ wwan0 → 192.168.100.x │
│ ext-dn NAT → internet │ (OTA) │ │
└────────────────────────┘ └────────────────────────┘
Walk through it by plane. Control plane: the modem's NAS messages travel over the air to the gNB and from there over SCTP (N2) to the AMF container; the gNB's own NG Setup with the AMF happens at start-up. User plane: after the PDU session is set up, the modem's IP packets cross the air, enter the gNB and are tunnelled in GTP-U (N3) to the UPF, which passes them to oai-ext-dn for NAT to the internet. Radio path: the gNB host streams baseband samples to the N310 over the sandbox data network (the DATA interface, never the control network); the N310 transmits the n41 carrier to the modem. Management: the modem is controlled through AT commands on /dev/ttyUSB2 and its data session through QMI on /dev/cdc-wdm0.
Duranta/OAI needs no license server — you launch the binary (with LD_LIBRARY_PATH set, because the build was relocated to /opt/duranta; start-all.sh handles it).
ssh <username>@console.<domain>.cosmos-lab.org # e.g. console.sb1... or console.grid...
-r 0) also expands the Duranta root partition:omf load -i duranta.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>
# on the console:
omf tell -a offh -t <sdr-node>
# on the gNB node (the console cannot reach the radio's addresses) -- every wait is bounded and says why it gave up:
for i in $(seq 1 45); do ping -c1 -W1 <usrp-mgmt-ip> >/dev/null 2>&1 || break; sleep 2; done # PoE acts in 20-60 s
ping -c1 -W1 <usrp-mgmt-ip> >/dev/null 2>&1 && echo "radio still answers after 90 s: the port did not power off (check omf stat / the PDU)"
sleep 20 # hold it off
# on the console:
omf tell -a on -t <sdr-node>
# on the gNB node:
for i in $(seq 1 40); do ping -c1 -W1 <usrp-mgmt-ip> >/dev/null 2>&1 && break; sleep 3; done # radio Linux back (~30-60 s)
ping -c1 -W1 <usrp-mgmt-ip> >/dev/null 2>&1 || echo "radio not back after 120 s: power-on was dropped (re-send omf tell -a on) or the radio needs a PDU cold cycle"
for i in $(seq 1 40); do uhd_find_devices --args "addr=<usrp-sfp-ip>" 2>/dev/null | grep -q serial && break; sleep 5; done # MPM up (~70 s)
uhd_find_devices --args "addr=<usrp-sfp-ip>" 2>/dev/null | grep -q serial || echo "MPM did not come up in 200 s -- see Troubleshooting (claimed / wedged radio)"
What to look for: none of the three echo warnings prints. A silent run means the radio went down, came back and its MPM answers discovery.The radio is shared. Never power-cycle or reboot a USRP that another user may be streaming from. Reserve it and coordinate with the other users of the sandbox first.
Why PLMN 00101? A soft UE can only form a 2-digit MNC, so
00101is the canonical testbed PLMN; the COTS SIM is provisioned to match. See the platforms overview.
On an SDR host the order of operations matters more than on RFsim: the radio must be free, the host's interrupt and CPU layout must keep the gNB's sample stream on time, the core must be up with the right subscribers, and the gNB configuration must carry the AMF's current address. The image ships /root/duranta/start-all.sh, a validated one-shot that does all of this in the right order:
ssh root@<gnb-node>
/root/duranta/start-all.sh
It (0) checks the USRP is visible and not claimed by another session (it refuses to start and tells you to power-cycle the radio from the console if it is); (1) applies the RF-underflow IRQ fix and pins the gNB to the isolated cores if cosmos-ofh-isolation --apply was run (else to cores 8–15) — on a two-socket host the gNB and the DATA NICs must share a NUMA node, otherwise the log fills with L (TX-late) letters and the UE cannot decode the DL; (2) brings up OAI CN5G (docker compose up -d); (3) provisions the subscribers (provision.sql: PLMN 00101, DNN oai, IPv4); (4) reads the dynamic AMF container IP and writes it into the gNB .conf (the OAI CN5G AMF IP changes across restarts — this step is essential); (5) derives the DATA interface and pins the USRP route on it — never falling back to CTRL; and (6) launches the gNB (nr-softmodem with LD_LIBRARY_PATH set). The reasoning behind items (1) and (5) — NUMA locality, IRQ placement, isolated cores — is explained on Preparing a real-time host. Confirm:
grep -aE 'got sync|is in service|NGAP_REGISTER_GNB_CNF' /tmp/oai-gnb.log | tail -3
docker compose -f /opt/oai-cn5g/oai-cn5g-fed/docker-compose/docker-compose-basic-nrf.yaml ps
[NR_RRC] cell PLMN 001.01 Cell ID ... is in service
[PHY] got sync (ru_thread)
What to look for: is in service = NG Setup with the AMF succeeded; got sync = the radio is locked and the cell is on air. Both must be present. start-all.sh launches the gNB with setsid; to stop it later, use the scoped stop under Cleanup.
This step does four things on the UE host. It stops ModemManager so that it does not fight the script for the serial port. It sets the modem to 5G SA only, gives it a band mask that includes n41, sets the APN to match the core's DNN, and locks it to PLMN 00101 — each of these is a reason a commercial modem ignores a private cell. It then starts a QMI data call and assigns the address the network gave it. Finally it pings the data network and the internet through wwan0. The general Quectel procedures, including recovery, are on COTS UE: the Quectel RM520N-GL.
ssh root@<ue-node>
systemctl stop ModemManager
python3 - <<'PY'
import serial, time
s = serial.Serial("/dev/ttyUSB2", 115200, timeout=3)
at = lambda c, w=1.0: (s.write((c+"\r\n").encode()), time.sleep(w), s.read_all().decode())[2]
at('AT+QNWPREFCFG="mode_pref",NR5G')
at('AT+QNWPREFCFG="nr5g_disable_mode",0') # 0 = SA enabled
at('AT+QNWPREFCFG="nr5g_band",41:77:78') # the band mask must include n41 (a modem set to 77:78 never sees the cell)
at('AT+CGDCONT=1,"IP","oai"') # IPv4 APN, matches the CN5G DNN "oai"
at('AT+COPS=2',2); at('AT+CFUN=0',3); at('AT+CFUN=1',5); at('AT+COPS=1,2,"00101",12',6) # lock PLMN
time.sleep(10)
print("C5GREG:", at('AT+C5GREG?')); print("QENG:", at('AT+QENG="servingcell"'))
s.close()
PY
echo Y > /sys/class/net/wwan0/qmi/raw_ip; ip link set wwan0 up
qmicli -d /dev/cdc-wdm0 --wds-start-network="apn=oai,ip-type=4" --client-no-release-cid
IP=$(qmicli -d /dev/cdc-wdm0 --wds-get-current-settings | awk '/IPv4 address/{print $NF}')
ip addr add $IP/22 dev wwan0; ip link set wwan0 mtu 1400 up
ip route replace 192.168.100.0/22 dev wwan0
DN=<oai-ext-dn address> # on the gNB node: docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' oai-ext-dn
ping -I wwan0 -c 5 $DN # OAI ext-DN
ping -I wwan0 -c 5 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 means the modem is registered on its home network (001/01) over 5G. IPv4 address: in 192.168.100.0/22 means the PDU session exists. 0 % loss to 8.8.8.8 proves the full user plane.
Use only the scoped route above; a default route via
wwan0black-holes the UE node's management SSH (Routing without cutting off the node). 100 % loss on the first ping batch is the modem leaving RRC idle — the next batch succeeds (Bringing up data); for aCallFailedfrom--wds-start-network, see Troubleshooting below.
The step-by-step 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.
On the console of your domain (console.sb1.cosmos-lab.org):
cd ~ && wget -q https://www.cosmos-lab.org/files/cosmos-tutorials-duranta-ota.zip
unzip -o cosmos-tutorials-duranta-ota.zip # extracts into ~/tutorials/
cd ~/tutorials && ls duranta-ota
| File | Purpose |
|---|---|
duranta-ota_params.yml |
the single parameter source: radio node + USRP address, band mask, PLMN/IMSI, timeouts |
duranta-ota.yml |
Mode 2 — Ansible: cosmos_preflight, radio power-cycle from the console (omf), gNB + core, modem registration, QMI data call, dual-target measurement, contract |
duranta-ota_manual.sh |
Mode 1 scripted: the exact commands of this page, node by node |
duranta-ota_tutorial.ipynb |
Mode 3 — the notebook (kernel on the console, papermill-runnable) |
duranta-ota_modem.py |
the Quectel AT helper (SA-only, band mask, APN, PLMN lock, registration wait). It self-heals the Quectel's USB data path — a non-zero tx_errors on wwan0 triggers a USB device reset of the modem before configuring (a healthy host→modem data path shows 0) — and switches the carrier MBN profile off after the last radio cycle, because on the RM520N-GL even a light CFUN=0/1 re-activates ROW_Commercial |
duranta-ota_lib.py |
shared parser + result contract + the parity comparison spec |
duranta-ota_parity.sh |
runs all three modes on the same two nodes and asserts they agree; it runs papermill with --cwd and runs the teardown on every exit ("releasing the rig" / "rig released") |
duranta-ota_teardown.yml |
clean teardown (data call down, gNB + core down; the radio is left powered) |
OMF_NODESis required and ordered: first node = gNB host (theduranta.ndznode with the USRP), second = UE host (the modem). The radio itself is not inOMF_NODES— it is named in the params (duranta_sdr_node,duranta_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.
cd ~/tutorials
export OMF_NODES="srv1-lg1,sdr1-in3" # gNB host, UE host
ansible-playbook duranta-ota/duranta-ota.yml
The playbook power-cycles the radio and waits until it is genuinely claimable (uhd_usrp_probe, not just discovery — MPM answers discovery ~20 s before it accepts a session), regenerates the gNB config for that radio, runs the image's start-all.sh, waits for in service plus an advancing frame counter, then on the UE host configures the modem (SA-only, band mask including the cell's band, APN, PLMN lock), waits for +C5GREG: 0,1, brings up the QMI data call with scoped routes only, and measures ping to the local ext-DN and to 8.8.8.8 plus iperf3 DL/UL to the ext-DN. Outputs land in $DURANTA_OTA_RUN_DIR (default /tmp/duranta-ota-$USER).
export OMF_NODES="srv1-lg1,sdr1-in3"
/opt/cosmos-jupyter/venv/bin/papermill duranta-ota/duranta-ota_tutorial.ipynb duranta-ota/duranta-ota_tutorial-out.ipynb
bash duranta-ota/duranta-ota_parity.sh srv1-lg1 sdr1-in3
=== duranta-ota parity ===
mode ue_ipv4 rsrp_dbm sinr_db tx_late ping_rtt_ms ping_remote_loss_pct dl_mbps ul_mbps
ansible 192.168.100.11 -77 31 0 11.14 0.0 113.0 15.1
manual 192.168.100.11 -77 29 0 12.25 0.0 112.0 14.0
jupyter 192.168.100.11 -76 30 0 12.0 0.0 113.0 13.6
dl_mbps spread max/min = 1.01 (tol 1.60)
ul_mbps spread max/min = 1.11 (tol 1.80)
PARITY: PASS (same experiment in all modes; all checks green)
Pass criteria: every mode reaches in service, 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 (over-the-air numbers depend on the modem's RRC state and the shared RF space, hence the wide bands). (run on sb1, 2026-09-08: gNB srv1-lg1.sb1 + N310 sdr1-s1-lg1, modem on sdr1-in3.sb1, image duranta-20260907c). Green parity is what sets this page's Last verified; re-running it after an image refresh is the regression test.
| Check | Command | Expected |
|---|---|---|
| Cell on air | grep -aE 'got sync\|is in service' /tmp/oai-gnb.log (gNB node) |
cell PLMN 001.01 … is in service and got sync |
| Modem registered | AT+C5GREG? on /dev/ttyUSB2 |
+C5GREG: 0,1 on 001/01; the AMF logs 5GMM-REGISTERED for the SUPI |
| PDU session | qmicli -d /dev/cdc-wdm0 --wds-get-current-settings |
an IPv4 address in 192.168.100.0/22 |
| User plane | ping -I wwan0 -c 5 $DN and ping -I wwan0 -c 5 8.8.8.8 |
0 % loss on both |
| RF stream healthy | inspect /tmp/oai-gnb.log |
no sustained [HW] [xmit] tx samples 0 underflow |
Measure air capacity with UDP: TCP's congestion control reacts to radio retransmissions and under-reports what the link can carry. Run an iperf3 server inside the oai-ext-dn container (the data network) and keep a keepalive ping running so the modem stays in RRC-connected state:
# on the gNB/core node:
docker exec -d oai-ext-dn iperf3 -s
# on the UE node:
ping -I wwan0 -i 0.3 $DN >/dev/null 2>&1 &
iperf3 -c $DN -B $IP -u -b 150M -R -t 12 # DL
iperf3 -c $DN -B $IP -u -b 20M -t 12 # UL (TCP UL can drop iperf3's control channel under retransmissions — measure UL with UDP)
Measured with a Quectel RM520N-GL (band n41, 40 MHz, 30 kHz SCS, TDD DDDSU), by the gNB server's CPU platform:
| Date | gNB CPU platform | Image | Method | DL, Mbit/s | UL, Mbit/s | RTT | Notes |
|---|---|---|---|---|---|---|---|
| 2026-09-08 | 2× Intel Xeon Gold 6226 @ 2.70 GHz, 24 cores / 24 threads | duranta-20260907c |
the parity bundle, three modes (see above) | 112–113 | 13.6–15.1 | 11–12 ms | RSRP −77, SINR ~30 |
| 2026-09-04 | 2× Intel Xeon Gold 6126 @ 2.60 GHz, 24 cores / 48 threads | duranta-20260905b |
manual, UDP DL / TCP UL | 102 (0.6 % loss at 150 M offered) | 8.5 | ~13 ms | RSRP −87; DL MCS 25, UL MCS 14 after the config notes below |
| 2026-07-15 | not recorded | duranta.ndz of that date |
manual, UDP | ≈115 | ≈4 | ~20 ms | RSRP −79; air-limited |
Config notes. OAI's stock band-41 file (gnb.sa.band41.fr1.106PRB.usrpb210.conf) carries lab values that cap this cell: pusch_TargetSNRx10 = 150, ul_min_mcs = 16 (UL never adapts and runs one step too hot) and dl_max_mcs = 10. /root/duranta/mk-gnb-oai-40.sh sets 200 / 0 / 28 and att_tx = 12 (0 dB overdrives a nearby modem). After a gNB restart the first --wds-start-network may return CallFailed; the deregister/re-register retry is step 2 of the COTS UE recovery ladder.
Stop the gNB and the core, release the modem's data call, and power the nodes off. The radio is left powered for the next user. If you used the bundle, its duranta-ota_teardown.yml does this. After the manual path, stop the gNB as a process group identified by its executable — never with pkill -x or pkill -f, which match by name or command line and can hit another user's process or your own shell. On the gNB node, define the helper once:
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. start-all.sh launched the gNB with setsid, so the group is exactly that launch. It sends TERM first, so the gNB can release the radio; if it is still running after a few seconds, repeat the call with KILL as the second argument.
ssh root@<gnb-node>
stop_exe /opt/duranta/cmake_targets/ran_build/build/nr-softmodem
docker compose -f /opt/oai-cn5g/oai-cn5g-fed/docker-compose/docker-compose-basic-nrf.yaml down
Then release the modem's data call and power the nodes off:
ssh root@<ue-node> 'qmicli -d /dev/cdc-wdm0 --wds-stop-network=disable-autoconnect 2>/dev/null'
omf tell -a offh -t <gnb-node>,<ue-node>
A symptom usually belongs to one stage of the attach; the registration and PDU session sequence below shows the order in which the stages happen.
Figure — the attach sequence from cell search to user data; a failure is diagnosed by the last stage that completed (for example: registered, but no PDU session → look at the SMF/UPF, not the radio).
| Symptom | Cause | Fix |
|---|---|---|
| Modem registers and gets an IP, but 100 % loss; the gNB sees no SR/BSR from the UE | the host→modem USB data endpoint is stuck: every uplink packet fails with -EPROTO before it reaches the modem |
ip -s link show wwan0 — TX errors +1 per packet, TX packets 0 confirms it. A USB device reset of the modem fixes it (the bundle's modem helper does this automatically); a qmi_wwan unbind/bind does not |
ansible-playbook preflight: "OMF_NODES selected 1 node(s) … needs exactly 2" while both nodes answer ssh |
omf stat shows a node as Unknown (UNREACHABLE) (its BMC is wedged), so cosmos-inventory.py drops it |
point Ansible at a static inventory for the run: ANSIBLE_INVENTORY=<file> with a YAML group nodes listing the gNB host then the UE host (ansible_user: root); report the BMC to the testbed operators |
gNB: dlopen(libparams_libconfig.so) … No such file → exits |
relocated build, stale rpath | export LD_LIBRARY_PATH=/opt/duranta/cmake_targets/ran_build/build:$LD_LIBRARY_PATH (start-all.sh does this) |
gNB aborts on UHD stream / start-all.sh says route via CTRL |
fronthaul route fell back to the control net | start-all.sh derives the DATA iface and refuses to launch over CTRL — re-run it (it pins the route on the DATA iface) |
gNB: NGSetup fails / AMF not associated |
OAI CN5G AMF IP is dynamic and the .conf has a stale one |
re-run start-all.sh (it reads docker inspect oai-amf and updates amf_ip_address); background on the OAI CN5G page |
Modem registers but --wds-start-network → CallFailed |
stale UPF F-TEID after failed PDU cycles | docker compose restart oai-upf oai-smf |
[HW] [xmit] tx samples 0 != …, UE DL glitchy |
fronthaul NIC IRQs on the PHY cores | start-all.sh stops irqbalance + remaps DATA IRQs to housekeeping cores |
Modem won't camp (SEARCH) |
nr5g_disable_mode=1 disables SA on RM520N-GL |
set to 0; lock AT+COPS=1,2,"00101",12 |
AT+COPS=1,2,"00101",12 → +CME ERROR: 30, modem stays in SEARCH while the cell is up |
the modem's NR band mask excludes n41 (AT+QNWPREFCFG="nr5g_band" shows e.g. 77:78) |
AT+QNWPREFCFG="nr5g_band",41:77:78 |
Power-cycle completes and the radio pings, but uhd_find_devices never sees it ("MPM did not come up") |
the radio's MPM daemon did not start or is wedged (RPC timeout); an omf power cycle of the node is not always enough | ssh root@<usrp-mgmt-ip> reboot (N3xx run Linux); if it still does not answer, the radio itself needs a cold cycle at its PDU — ask the testbed operators |
gNB: Someone tried to claim this device again / start-all.sh reports claimed: True |
another session holds the USRP (or a stale claim after a crash) | coordinate with the other user; never reboot a radio someone may be using. If it is yours: omf tell -a offh / 10 s / on on the console |
Log floods with L letters, pucch0_DTX climbs, DL BLER ≈ 1 |
TX samples reach the USRP late: gNB threads on the other NUMA node or sharing cores with NIC IRQs / hyper-thread siblings; or the USRP NIC left with a single TX queue | run cosmos-ofh-isolation --apply + reboot before start-all.sh (it then pins the gNB); keep the USRP NIC at ≥ 8 queues. Why: Preparing a real-time host |
ping <ext-dn> works but iperf3 → connection refused / Bad file descriptor |
Docker assigned that address to another container (.131 can be mysql) |
use docker inspect … oai-ext-dn for the target |
Modem: Unknown DNN reject |
APN ≠ the CN5G DNN | AT+CGDCONT=1,"IP","oai" (not internet); clear back-off with AT+CFUN=1,1 |
UE ping 8.8.8.8 fails, ext-DN pings |
ext-dn NAT / route stale | docker exec oai-ext-dn ping 8.8.8.8 to confirm; restart the stack so ext-dn re-reads UE_NETWORK=192.168.100.0/22 |
The modem rows (USB -EPROTO, nr5g_disable_mode, band mask, Unknown DNN) are explained in more depth, with the full recovery ladder, under Troubleshooting on the COTS UE page.
nr-softmodem as an RFsim server + OAI's nr-uesoftmodem as the client — no USRP. See the Duranta soft-only tutorial (see its Performance table)..conf TDD pattern for more UL at the cost of DL — see the oai-gnb skill.On this wiki
omf-duranta (testbed operations), oai-gnb (OAI libconfig + build), quectel-5g-modem (the modem).External
duranta-20260907c is built from tag 2026.w36 (commit b3930e39f5); upstream OAI docs at gitlab.eurecom.fr/oai/openairinterface5g.basic-nrf config, /opt/oai-cn5g/oai-cn5g-fed/docker-compose on the image).Author(s): COSMOS team · Last verified: 2026-09-08 (three-mode parity on sb1, duranta-20260907c) · text revised 2026-09-24 · Tested image/release: duranta.ndz = duranta-20260907c, built from Duranta tag 2026.w36 (Duranta/OAI nr-softmodem USRP build + OAI CN5G) + lte5gue.ndz, PLMN 00101, pool 192.168.100.0/22 · Bundle: cosmos-tutorials-duranta-ota.zip · Tags: duranta, oai, openairinterface, oai-cn5g, 5g, sa, sdr, usrp, quectel