OAI UE sim based ISAC system
A fork of Duranta OpenAirInterface
that turns the OAI nr-uesoftmodem into a sensing receiver.
An OCUDU RAN transmits standard 3GPP downlink CSI-RS. A receiver co-located on the same mast listens for the reflections and turns them into range-Doppler maps. Nothing is added to the air interface. The waveform stays fully NR-compliant, and the sensing is parasitic on reference signals the network already sends.
The observable is the least-squares channel estimate the UE already computes for CSI reporting. The tap adds no PHY processing; it copies estimates that exist anyway and streams them off-box over UDP.
The reference setup pairs this UE with an OCUDU RAN driving a LiteON O-RAN RU over DPDK fronthaul, on band n78 at 100 MHz. The ISAC panel antenna and the RU share one mast; a USRP X410 feeds the UE, with a Falcon X GM supplying a common clock and time reference.
# build
cd cmake_targets/ran_build/build
cmake ../../.. -DOAI_USRP=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo
make -j$(nproc) nr-uesoftmodem
# run the UE with the sensing tap enabled
sudo ./nr-uesoftmodem -O <your>.conf \
--usrp-args "type=x4xx,addr=192.168.20.2,clock_source=external,time_source=external" \
-r 273 --numerology 1 --band 78 -C 3450720000 --ssb 1518 \
--ue-nb-ant-rx 2 --ue-nb-ant-tx 1 --ue-rxgain 40 --ue-fo-compensation \
--ue-timing-correction-disable \
--uecap_file targets/PROJECTS/GENERIC-NR-5GC/CONF/uecap_ports1.xml \
--isac-udp 127.0.0.1:5556CSI-RS only flows to an RRC_CONNECTED UE, so keep traffic on the link or the gNB releases the UE and sensing goes quiet:
sudo bash -c 'until ip -4 addr show oaitun_ue1 2>/dev/null | grep -q inet; \
do sleep 0.1; done; exec ping -i 0.2 -I oaitun_ue1 8.8.8.8'Check the raw CSI responds to the scene before trusting any map:
tools/isac/isac_csi_monitor.py --port 5556 --csi-type trs --rx 0chg% is the sensitive column, because motion rearranges multipath even when total
power holds steady. If it does not move when someone walks through, the problem
is RF or geometry, not processing.
Then the map:
tools/isac/isac_range_doppler.py --port 5556 --csi-type trs \
--cpi 32 --min-range 0.5 --max-range 6 --clutter-rank -1 \
--dyn-range 15 --update-every 25| file | change |
|---|---|
openair1/PHY/NR_UE_TRANSPORT/nr_ue_isac.{c,h} |
new: the sensing tap and UDP wire format |
openair1/PHY/NR_UE_TRANSPORT/csi_rx.c |
force channel estimation when sensing, push each snapshot |
executables/nr-uesoftmodem.{c,h} |
--isac-udp <ip>:<port> |
CMakeLists.txt |
build the tap into PHY_SRC_UE |
tools/isac/ |
receiver, monitor, offline diagnosis, selftest, link budget |
Total: two new source files and 17 lines of integration. Everything else is stock upstream.
| tool | role |
|---|---|
isac_range_doppler.py |
receiver: range-Doppler map, CA-CFAR, M-of-N tracker. --replay, --record, --headless |
isac_csi_monitor.py |
raw CSI amplitude per occasion. No radar processing |
isac_diagnose.py |
offline: sampling uniformity, delay/amplitude stability, artefact floor |
isac_selftest.py |
13 checks of the processing chain against synthetic ground truth |
isac_link_budget.py |
radar-equation link budget |
At 3450.72 MHz, 273 PRB, 30 kHz SCS (λ = 8.69 cm, occupied BW 98.28 MHz):
| quantity | value | set by |
|---|---|---|
| range resolution | 1.525 m | c / (2·B) |
| unambiguous range | 416 m | c / (2·Δf), Δf = 360 kHz |
| direct-path dead zone | 2.90 m | ≈1.9 × resolution (window mainlobe) |
| update rate | 20 ms | CSI-RS / TRS period |
| unambiguous velocity | ±1.09 m/s | 20 ms TRS burst repetition |
… with --unfold |
±43.4 m/s | 0.5 ms intra-burst baseline |
| velocity resolution | 0.068 m/s | CPI 32 = 0.64 s dwell |
TRS arrives as 2 snapshots in adjacent slots, then 19.5 ms idle, repeating every 20 ms. That is ~100 estimates/s on average, but aliasing follows the 20 ms repetition, not the average. The tight pair adds no independent slow-time sample, it only says where inside the burst each one sat. Deriving the velocity axis from the mean rate reports twice the span that exists, and static clutter aliases into the surplus half as symmetric false pairs.
--unfold recovers the short baseline to resolve the fold.
csi_rs_period accepts {10, 20, 40, 80} ms, but TS 38.214 §5.1.6.1.1 forbids
10 ms above 52 RB. At 273 RB, 20 ms / 50 Hz is the floor. Getting below it
means dropping to ≤52 RB, which takes range resolution from 1.53 m to ~8 m.
Read this before drawing conclusions from a map.
Verified working
- Processing chain:
isac_selftest.pypasses 13/13: range and velocity scale, sign convention, resolution, clutter-rank safety, CFAR + tracker, radar-equation range, common-mode notch. - Motion is detected. Against a still room, a person at ~1.5–2 m raises the per-occasion channel-change metric from a 10.6 ± 0.55 baseline to peaks of 40–49 (≈13 dB margin).
The principal limitation: the range axis does not track
Range rate dR/dt and Doppler velocity v are independent measurements of the
same motion, so a resolved target correlates near +1. Measured over a 260 s
ground-truth walk/stand session:
[range-check] corr(v, dR/dt) = +0.055 over 313 detections -- range is NOT tracking
The target is detected as a modulation of the direct-path leakage, and that leakage sits at delay 0 regardless of where the person stands. Doppler responds to motion; the range coordinate is pinned to the leakage.
Treat this as a motion/presence sensor with a Doppler measurement, not a
ranging radar. The fix is physical: antenna isolation, polarisation, tilt,
separation. --range-check scores any such change in about a minute, which is
far more reliable than judging heat maps by eye.
Other things worth knowing
- Range goes as the fourth root of the target-to-coupling ratio: 1 dB of isolation buys ~5.9% range, and doubling range costs 12 dB.
- A map of many cells reaches ~10 dB above its own median on noise alone. The tool prints that ceiling, so always compare the peak against it before calling anything a target.
- Record an empty room first. A fixed mechanical line was confirmed as a
target in essentially every frame of one session.
--notch VELOCITYremoves it. - Close in, the adaptive subspace canceller nulls targets that share the direct
path's subspace, and reports better cancellation while doing it. Use
--clutter-rank -1under ~3 m.
ISAC node nrUE is developed by coRAN LABS.
This is a fork of Duranta OpenAirInterface, which delivers and maintains an open-source cellular wireless software stack for 4G, 5G and future networking technologies. For upstream documentation, repository structure and community support, see the OAI documentation and openairinterface.org.
The source code is distributed under CSSL v1.0. Some files, such as for orchestration, are distributed under MIT license. Documentation is distributed under Creative Commons Attribution 4.0 International license.
All the files without an explicit copyright header have an implicit "Copyright of OpenAirInterface Authors".
Please see NOTICE for other licenses which are used in the software.
In the past OAI source code has been re-licensed sometimes, here is the history:
- CSSL v1.0 starting tag 2026.w14
- OAI Public License v1.1 starting tag v1.0 till af4b0d53
- OAI Public License v1.0: starting tag v.04 till v1.0
- GPL 3: starting tag v.0 till v.04 (only initial implementation of 4G)


