LEO NTN Testbed: 5G NR over a moving satellite, with handover
A 5G Standalone network in which the UE reaches the gNB through a low-Earth-orbit satellite at 600 km. Unlike GEO, the satellite moves: the delay changes continuously, the satellite is only visible for a few minutes per pass, and keeping a UE connected means handing it from one satellite to the next. Four scenarios run on one host, from a single pass to a satellite train that keeps a cell on the air indefinitely.
What this is
This testbed covers four scenarios on one host:
| # | Scenario | What moves | Cells |
|---|---|---|---|
| 1 | Single-cell LEO pass | One satellite, horizon to horizon | 1 gNB |
| 2 | Inter-cell handover (Xn) | Two satellites, one gNB each; the UE hands over between cells | 2 gNBs |
| 3 | Intra-cell satellite switch, one-shot | Two satellites behind one cell; the cell switches satellite once | 1 gNB |
| 4 | Intra-cell satellite switch, loop (satellite train) | N satellites behind one cell; the cell switches to the next one every time the serving one sets | 1 gNB |
Components:
- OCUDU gNB (
TOSSI-Foundation/OCUDU-RAN, branchntn) with the OAI rfsimulator radio driver - OAI nrUE (
TOSSI-Foundation/OAI-RAN, branchrfsim_ocudu) with SIB19 Rel-18/19 decoding andsatSwitchWithReSync-r18 - SD-Core as the 5G core for every handover scenario
- LEO tracker: a small web dashboard that follows the gNB and UE logs
This replaces the earlier ZMQ-based LEO testbed, where the channel model ran inside the UE's ZMQ driver.
One ephemeris, two consumers
The gNB propagates the configured satellite state vector (Keplerian, from epoch_timestamp) every SIB19 update. The same propagated state feeds two things:
- SIB19, which the UE uses to pre-compensate its timing advance and to know when the satellite changes.
- The emulated channel: the rfsimulator applies the service-link round-trip delay to the gNB's receive path, extrapolated between updates with the delay drift. When the satellite is below the horizon at the reference location the link is muted in both directions.
Because the channel and the broadcast ephemeris come from one propagator, they cannot drift apart: the delay the UE's uplink experiences is exactly the timing advance it derived from SIB19.
The whole round trip is carried on one leg (the gNB receive path). Timing is exact; the downlink carries no delay and, by default, no Doppler. Service-link Doppler emulation exists (cell_cfg.ntn.emulate_doppler: true) but must be paired with UE-side continuous frequency-offset compensation and is off in every configuration below.
Orbit and link budget
| Property | Value |
|---|---|
| Orbit | 600 km circular, polar, ECEF state vector |
| Orbit radius / speed | 6 956 752 m / 7 569 m/s (0.0623 deg/s) |
| Reference location / UE position | North pole (lat 90, lon 0), WGS84 surface |
| Round-trip delay | 4.0 ms at zenith, ~19 ms at the horizon |
| Delay drift | up to ~46 us/s at the horizon, 0 at zenith |
| Pass, horizon to horizon | ~12.8 min |
| Cell-specific K_offset | 40 slots |
| UL sync validity | 30 s (5 s for satellite switching) |
The air interface is identical to the GEO testbed: band n256, 2185 MHz DL, -190 MHz UL offset, 10 MHz / 52 PRB / 15 kHz, SSB at 62 subcarriers from Point A, HARQ feedback disabled, UL HARQ mode B.
Prerequisites
- Ubuntu 22.04 or 24.04, x86-64, 8+ cores recommended
sudorights- SD-Core reachable from the host
- Accurate system time (NTP): the gNB propagates from wall-clock UTC and the UE switches satellites at a UTC deadline
- Python 3 for the pass placement script and the tracker (standard library only)
1. Get the sources
mkdir -p ~/ntn && cd ~/ntn
git clone -b ntn https://github.com/TOSSI-Foundation/OCUDU-RAN.git
git clone -b rfsim_ocudu https://github.com/TOSSI-Foundation/OAI-RAN.git OAI_RAN
The run scripts find the UE at ../OAI_RAN relative to OCUDU-RAN; set OAI=/path/to/OAI_RAN if you place it elsewhere.
2. Build
OCUDU gNB:
sudo apt-get update
sudo apt-get install -y cmake make gcc g++ pkg-config git python3 \
libfftw3-dev libmbedtls-dev libsctp-dev libyaml-cpp-dev libgtest-dev \
libzmq3-dev libboost-program-options-dev
cd ~/ntn/OCUDU-RAN
cmake -S . -B build_ntn -DCMAKE_BUILD_TYPE=Release -DENABLE_RFSIM=ON
make -C build_ntn -j"$(nproc)" gnb
ls -l build_ntn/apps/gnb/gnb
OAI nrUE:
cd ~/ntn/OAI_RAN/cmake_targets
./build_oai -I
./build_oai --nrUE -w SIMU
ls -l ran_build/build/nr-uesoftmodem ran_build/build/librfsimulator.so
Core network
This testbed has been tested against both SD-Core and OAI CN5G. The single-cell pass and the satellite switch work with either. OAI CN5G does not support the NGAP Path Switch procedure, so the UE context and PDU session do not survive an inter-cell handover with it. All handover scenarios below use SD-Core.
The provided configs expect:
| Item | Value | Config keys |
|---|---|---|
| AMF | 192.168.1.115 | cu_cp.amf.addr in sdcore.yml, leo_ho_sat1.yml, leo_ho_sat2.yml |
| gNB (single cell, and satellite 1) N2/N3 bind | 192.168.1.117 | cu_cp.amf.bind_addr, cu_up.ngu.socket[0].bind_addr |
| gNB satellite 2 N2/N3 bind | 192.168.1.120 | same keys in leo_ho_sat2.yml |
| PLMN / TAC | 00101 / 1 | |
| Slice | SST 1, SD 010203 (66051) | tai_slice_support_list, cell_cfg.slicing |
| Subscriber | IMSI 001010100000001, key 5122250214c33e723a5dd523fc145fc0, OPc 981d464c7c52eb6e5036234984ad0bcf, DNN internet | UE command line |
Replace the addresses with your own. The two gNBs of the inter-cell handover each need their own host address on the network that reaches the AMF, for example:
ip -4 addr # find the interface facing SD-Core
sudo ip addr add 192.168.1.120/24 dev <iface> # only if the second address is missing
The two gNBs talk Xn over loopback (127.0.0.1 and 127.0.0.2), which needs no setup on Linux.
All scenarios use this UE identity and slice on the command line:
UE_SDCORE="--uicc0.imsi 001010100000001 \
--uicc0.key 5122250214c33e723a5dd523fc145fc0 --uicc0.opc 981d464c7c52eb6e5036234984ad0bcf \
--uicc0.nssai_sst 1 --uicc0.nssai_sd 0x010203 \
--uicc0.pdu_sessions.[0].nssai_sst 1 --uicc0.pdu_sessions.[0].nssai_sd 0x010203 \
--uicc0.pdu_sessions.[0].dnn internet"
The commands below write these flags out in full.
3. Placing the satellite
The ephemeris is a state vector, so "start at 30 degrees" means recomputing several numbers together. scripts/set_leo_pass.sh does that and stamps the epoch in one step:
cd ~/ntn/OCUDU-RAN
./scripts/set_leo_pass.sh --help
./scripts/set_leo_pass.sh --show # current geometry and epoch
./scripts/set_leo_pass.sh --elev horizon --single # single cell, rising at 0 deg
./scripts/set_leo_pass.sh --elev 30 --ho # handover pair, designed pass
./scripts/set_leo_pass.sh --elev 40 --elev2 10 --ho # the two satellites set independently
./scripts/set_leo_pass.sh --elev zenith --single --lead 30 # 30 s to start the binaries
| Flag | Meaning |
|---|---|
--elev <deg|horizon|zenith> | Where satellite 1 starts (rising) |
--elev2 <deg|horizon|zenith> | Where satellite 2 starts; default is 18.3958 deg of orbit behind satellite 1 |
--single / --ho / --both | Which configs to write (default --both) |
--lead <s> | Stamp the epoch this many seconds in the future (default 12) |
--no-epoch | Change geometry only |
The epoch lives only in configs/ntn/leo_rfsim_gnb.yml; the handover overlays inherit it, which keeps both satellites on one clock. The script also writes the epoch to /tmp/leo_epoch, which the commands below use to wait for a moment in the pass:
Tplus(){ while [ "$(date +%s)" -lt $(( $(cat /tmp/leo_epoch) + $1 )) ]; do sleep 1; done; }
Run scripts re-stamp the epoch themselves unless you pass KEEP_EPOCH=1.
4. The LEO tracker
scripts/leo_tracker.py follows the gNB and UE logs and serves a live page:
- to-scale side view of every satellite, with the switch elevation mask
- countdown to the next satellite switch
- satellite cards: elevation, range, round trip, drift, link state
- elevation-over-time chart with the serving satellite highlighted
- event timeline with pop-up notifications for satellite switches, handovers, path switch, UE state
cd ~/ntn/OCUDU-RAN
python3 scripts/leo_tracker.py # newest run found in /tmp
python3 scripts/leo_tracker.py /tmp/gnb_leo_sattrain.log --ue /tmp/ue_leo_sattrain.log
python3 scripts/leo_tracker.py /tmp/gnb_leo_sat1.log /tmp/gnb_leo_sat2.log --ue /tmp/ue_leo_ho.log
python3 scripts/leo_tracker.py --host 127.0.0.1 --port 8500 # local access only
Open http://<host>:8500. By default it binds all interfaces without authentication; use --host 127.0.0.1 on shared networks. If the page shows "not written for N min, this is a finished run", the gNB is not writing the log it is following.
Scenario 1: single-cell LEO pass
The satellite rises at the horizon when the epoch is reached and the UE attaches at low elevation, then stays connected until the satellite sets (~12.8 min), when the emulated link is muted.
sudo pkill -9 -f nr-uesoftmodem; sudo pkill -9 -f 'build_ntn/apps/gnb/gnb'
sudo rm -f /tmp/gnb_leo.log
cd ~/ntn/OCUDU-RAN
./scripts/set_leo_pass.sh --elev horizon --single --lead 20
Terminal 1, gNB
cd ~/ntn/OCUDU-RAN
sudo ./build_ntn/apps/gnb/gnb -c configs/ntn/leo_rfsim_gnb.yml -c configs/ntn/sdcore.yml
Terminal 2, UE (start within the 20 s lead)
sudo ~/ntn/OAI_RAN/cmake_targets/ran_build/build/nr-uesoftmodem \
-O ~/ntn/OAI_RAN/targets/PROJECTS/GENERIC-NR-5GC/CONF/ue.ntn.leo.rfsim.conf \
--band 256 -C 2185000000 --CO -190000000 -r 52 --numerology 0 --ssb 62 \
--rfsim --rfsimulator.[0].serveraddr 127.0.0.1 \
--time-sync-I 0.1 --ntn-initial-time-drift -40 \
--uicc0.imsi 001010100000001 \
--uicc0.key 5122250214c33e723a5dd523fc145fc0 --uicc0.opc 981d464c7c52eb6e5036234984ad0bcf \
--uicc0.nssai_sst 1 --uicc0.nssai_sd 0x010203 \
--uicc0.pdu_sessions.[0].nssai_sst 1 --uicc0.pdu_sessions.[0].nssai_sd 0x010203 \
--uicc0.pdu_sessions.[0].dnn internet
--ntn-initial-time-drift -40 seeds the UE's downlink timing tracker with the drift at the rising horizon, before it has read SIB19; --time-sync-I 0.1 keeps the tracking loop gentle.
Terminal 3, traffic and tracker
ping -I oaitun_ue1 8.8.8.8
python3 ~/ntn/OCUDU-RAN/scripts/leo_tracker.py /tmp/gnb_leo.log
Verify
gNB log (/tmp/gnb_leo.log):
[NGAP ] [I] "NG Setup Procedure" finished successfully
[NTN ] [I] Emulated NTN channel updated, cell=0x66c000 rx_delay=19331us drift=-46.141us/s doppler=off sat=0
[RRC ] [I] ue=0 c-rnti=0x4601: DCCH UL rrcSetupComplete
rx_delay falls to ~4000 us at zenith and rises again; drift changes sign at zenith. When the satellite sets, the gNB logs NTN: satellite set below the horizon at the reference location ... emulated link down.
Measured on this testbed, attaching at the rising horizon (19.7 ms round trip): the UE attached at the epoch and 10 of 10 pings to 8.8.8.8 returned in 122 to 174 ms. Echo times are dominated by the K_offset scheduling delay and uplink grant timing, not by the few milliseconds of propagation delay.
Scenario 2: inter-cell handover over Xn
Satellite 1 carries cell PCI 1 (gNB 411), satellite 2 carries cell PCI 2 (gNB 412), trailing satellite 1 by 18.4 degrees of orbit. The UE attaches through satellite 1; while satellite 1 descends and satellite 2 climbs through 30 to 40 degrees, the handover is commanded from the gNB console. The gNBs run an Xn handover, the target completes NGAP Path Switch with SD-Core, and the PDU session survives.
How it differs from the single-cell setup
- rfsimulator roles are inverted. One rfsimulator server can have many clients, so for the UE to hear both cells, the UE is the server and both gNBs are clients (
serveraddr=127.0.0.1). The UE starts first; the gNBs printconnect() faileduntil they reach it. - Satellite 2's gNB starts late, once the UE is attached through satellite 1. Both cells transmit into one sample stream, so a second cell coming up during initial access is interference.
The designed pass
Placed with ./scripts/set_leo_pass.sh --elev 30 --ho:
| Time from epoch | Satellite 1 | Satellite 2 | Action |
|---|---|---|---|
| T+0 s | 30.0 deg, 7.17 ms | -2.0 deg, below horizon (muted) | UE attaches via satellite 1 |
| T+34 s | 40.0 deg | 0.0 deg, 18.85 ms | Satellite 2 rises |
| T+120 s | 87.6 deg | 6.1 deg | Start gNB 2 |
| T+123 s | 89.8 deg (zenith) | 6.3 deg | |
| T+295 s | 20.4 deg | 30.0 deg | Handover window opens |
| T+310 s | 18.0 deg | 34.0 deg | Command the handover |
| T+329 s | 15.4 deg | 40.0 deg | Window closes |
| T+508 s | sets | 40.0 deg | Satellite 1 link muted |
| T+803 s | sets |
One command
cd ~/ntn/OCUDU-RAN
./scripts/set_leo_pass.sh --elev 30 --ho --no-epoch
./scripts/run_leo_ho_rfsim.sh
The script starts the UE as server with the SD-Core subscriber, starts gNB 1, waits for the PDU session, starts a continuous ping to 8.8.8.8, starts gNB 2 at T+120, sends ho to gNB 1's console at T+310 and reports the result. Tunables: SAT2_AT, HO_AT, DUR, EPOCH_LEAD.
Step by step
sudo pkill -9 -f nr-uesoftmodem; sudo pkill -9 -f 'build_ntn/apps/gnb/gnb'
sudo rm -f /tmp/gnb_leo_sat1.log /tmp/gnb_leo_sat2.log
cd ~/ntn/OCUDU-RAN
./scripts/set_leo_pass.sh --elev 30 --ho --lead 30
Terminal 1, UE (rfsimulator server), start first
sudo ~/ntn/OAI_RAN/cmake_targets/ran_build/build/nr-uesoftmodem \
-O ~/ntn/OAI_RAN/targets/PROJECTS/GENERIC-NR-5GC/CONF/ue.ntn.leo.rfsim.conf \
--band 256 -C 2185000000 --CO -190000000 -r 52 --numerology 0 --ssb 62 \
--rfsim --rfsimulator.[0].serveraddr server \
--time-sync-I 0.1 --ntn-initial-time-drift -40 \
--uicc0.imsi 001010100000001 \
--uicc0.key 5122250214c33e723a5dd523fc145fc0 --uicc0.opc 981d464c7c52eb6e5036234984ad0bcf \
--uicc0.nssai_sst 1 --uicc0.nssai_sd 0x010203 \
--uicc0.pdu_sessions.[0].nssai_sst 1 --uicc0.pdu_sessions.[0].nssai_sd 0x010203 \
--uicc0.pdu_sessions.[0].dnn internet
Terminal 2, gNB 1, satellite 1 (keep this terminal: it is the console used for ho)
cd ~/ntn/OCUDU-RAN
sudo ./build_ntn/apps/gnb/gnb -c configs/ntn/leo_rfsim_gnb.yml -c configs/ntn/leo_ho_sat1.yml
Wait until the UE reports TUN Interface oaitun_ue1 successfully configured.
Terminal 3, traffic
ping -I oaitun_ue1 -D -i 1 8.8.8.8
Terminal 4, gNB 2, satellite 2, at T+120
cd ~/ntn/OCUDU-RAN
Tplus(){ while [ "$(date +%s)" -lt $(( $(cat /tmp/leo_epoch) + $1 )) ]; do sleep 1; done; }
Tplus 120
sudo ./build_ntn/apps/gnb/gnb -c configs/ntn/leo_rfsim_gnb.yml -c configs/ntn/leo_ho_sat2.yml
Wait for Xn setup between the gNBs:
sudo grep -a 'XnSetup' /tmp/gnb_leo_sat1.log /tmp/gnb_leo_sat2.log
Command the handover at T+310. Get the UE's C-RNTI on gNB 1:
Tplus 310; sudo grep -ao 'c-rnti=0x[0-9a-f]*' /tmp/gnb_leo_sat1.log | tail -1
Then type into terminal 2 (gNB 1 console), using the RNTI in hex without 0x:
ho 1 4601 2 00101 1
Syntax: ho <serving pci> <rnti> <target pci> <target plmn> <target tac>.
Verify
Source gNB (/tmp/gnb_leo_sat1.log):
[XNAP ] [I] Tx PDU: HandoverRequest
[XNAP ] [I] Rx PDU: HandoverRequestAcknowledge
Target gNB (/tmp/gnb_leo_sat2.log):
[XNAP ] [I] Rx PDU: HandoverRequest
[XNAP ] [I] Tx PDU: HandoverRequestAcknowledge
[RRC ] [I] ue=0 c-rnti=0x4601: DCCH UL rrcReconfigurationComplete
[NGAP ] [I] ue=0 ran_ue=0 amf_ue=...: "Path Switch Procedure" finished successfully
[XNAP ] [I] Tx PDU: UEContextRelease
UE: Starting re-sync detection for target Nid_cell 2, then random access on the target.
Measured on this testbed with the designed pass:
| Metric | Result |
|---|---|
| Handover command | T+310 s, satellite 1 at 18 deg, satellite 2 at 34 deg and rising |
HandoverRequest to rrcReconfigurationComplete on the target | 227 ms |
| Path Switch with SD-Core | finished successfully 82 ms later |
| Ping to 8.8.8.8, attach to end of run | 391 sent, 391 received, no sequence gaps |
The target may log Decoding failure from the ASN.1 layer followed by Could not decode Path Switch Request Ack Transfer for psi=1. Keeping the uplink tunnel in use. It is harmless with SD-Core: the handover completes and the existing uplink tunnel stays valid.
Scenario 3: intra-cell satellite switch, one-shot
Release 17/18 NTN lets a cell keep its identity (NCI, PCI, SSB) while the satellite carrying it changes: SIB19 advertises the next satellite (satSwitchWithReSync-r18) and the time it takes over (t-ServiceStart). There is no target cell, no Xn, no RACH and no core signalling. The UE re-syncs in-cell at t-ServiceStart.
configs/ntn/leo_satswitch.yml adds a second satellite to the single cell and switches once, at the crossover where both satellites are at ~28.8 degrees (T+252 s), so the delay is continuous and only its drift reverses.
| Time from epoch | Satellite 1 | Satellite 2 |
|---|---|---|
| T+0 s | 30.0 deg, 7.17 ms | 0.3 deg, 18.65 ms |
| T+124 s | zenith | 9.8 deg |
| T+252 s | 28.7 deg | 28.9 deg, switch |
| T+384 s | zenith | |
| T+764 s | sets |
Run
Place satellite 1 at 30 degrees, then stamp the epoch of both satellites and the switch time together (epoch 20 s ahead, switch 252 s after it):
sudo pkill -9 -f nr-uesoftmodem; sudo pkill -9 -f 'build_ntn/apps/gnb/gnb'
sudo rm -f /tmp/gnb_leo_satswitch.log
cd ~/ntn/OCUDU-RAN
./scripts/set_leo_pass.sh --elev 30 --single --no-epoch
EPOCH=$(date -u -d '+20 seconds' '+%Y-%m-%dT%H:%M:%S')
SWITCH=$(date -u -d '+272 seconds' '+%Y-%m-%dT%H:%M:%S')
sed -i "s/epoch_timestamp: '[^']*'/epoch_timestamp: '$EPOCH'/" configs/ntn/leo_rfsim_gnb.yml configs/ntn/leo_satswitch.yml
sed -i "s/t_service: '[^']*'/t_service: '$SWITCH'/; s/t_service_start: '[^']*'/t_service_start: '$SWITCH'/" configs/ntn/leo_satswitch.yml
Terminal 1, gNB
cd ~/ntn/OCUDU-RAN
sudo ./build_ntn/apps/gnb/gnb -c configs/ntn/leo_rfsim_gnb.yml -c configs/ntn/leo_satswitch.yml -c configs/ntn/sdcore.yml
Terminal 2, UE (start within the 20 s lead), the same UE command as scenario 1 with 2>&1 | tee /tmp/ue_leo_satswitch.log appended.
Terminal 3, traffic and tracker
ping -I oaitun_ue1 -D -i 1 8.8.8.8
python3 ~/ntn/OCUDU-RAN/scripts/leo_tracker.py /tmp/gnb_leo_satswitch.log --ue /tmp/ue_leo_satswitch.log
Verify
gNB (/tmp/gnb_leo_satswitch.log):
[NTN ] [I] Sat-switch promotion scheduled, cell=0x66c000 serving satellite 0 -> 1 at t_service=...
[NTN ] [I] Sat-switch promotion applied, cell=0x66c000 serving satellite 0 -> 1
UE:
[NR_MAC] satSwitchWithReSync-r18 armed: switching satellite in ... ms (at UTC deadline), ssb-TimeOffset 0 s
[NR_MAC] satSwitchWithReSync-r18: t-ServiceStart reached, taking the target satellite into service in-cell
Measured on this testbed:
| Metric | Result |
|---|---|
| Promotion | scheduled at start-up for t_service, applied 34 ms after it |
| UE | armed from SIB19 268 s ahead, switched once at the UTC deadline |
| Sync failures / RRC releases | 0 / 0 |
| Ping to 8.8.8.8 across the switch | 299 sent, 298 received (the last one cut off at shutdown), no sequence gaps |
Scenario 4: intra-cell satellite switch loop (satellite train)
A one-shot switch only moves the problem to the second satellite's horizon. The satellite train keeps the cell on the air indefinitely: the gNB generates num_satellites satellites evenly spaced around the configured satellite's orbit, and every time the serving satellite sets through switch_elevation_deg it hands the cell to the satellite rising behind it, then immediately arms the next switch. The list wraps, so this repeats for as long as the gNB runs. The UE keeps its RRC connection and PDU session throughout.
# configs/ntn/leo_sattrain.yml
cell_cfg:
ntn:
ntn_ul_sync_validity_dur: 5
sat_train:
num_satellites: 12
switch_elevation_deg: 4.5
min_lead_s: 10
| Key | Range | Meaning |
|---|---|---|
num_satellites | 2-64 | Satellites spread evenly round the orbit, the configured one included |
switch_elevation_deg | 0-60 | The serving satellite hands over as it sets through this elevation |
min_lead_s | 1-600 | Minimum notice given in SIB19 before a switch |
Coverage is continuous only while the spacing is below twice the central angle at the switch elevation: 39.7 degrees for this 600 km orbit at 4.5 degrees, so at least 10 satellites.
| Satellites | Spacing | Time between switches | Next satellite's elevation at the switch |
|---|---|---|---|
| 10 | 36 deg | 9.6 min | ~9.6 deg |
| 12 | 30 deg | 8.0 min | ~22 deg |
| 18 | 20 deg | 5.3 min | ~88 deg |
| 36 | 10 deg | 2.7 min | ~23 deg |
ntn_ul_sync_validity_dur: 5 makes the UE re-read SIB19 about every 2.5 s, so it learns of each switch well before min_lead_s. The switch moment is computed from the orbit (the serving satellite's 4.5-degree setting time, found by bisection to 20 ms) and advertised as t-ServiceStart; the UE acts on it against its own UTC clock. If the gNB starts while the configured satellite is already below the switch elevation, it starts on the highest satellite of the train instead.
Run
sudo pkill -9 -f nr-uesoftmodem; sudo pkill -9 -f 'build_ntn/apps/gnb/gnb'
sudo rm -f /tmp/gnb_leo_sattrain.log
cd ~/ntn/OCUDU-RAN
./scripts/set_leo_pass.sh --elev zenith --single --lead 20
Starting at zenith puts the first switch about 5.5 minutes into the run; with --elev horizon it comes after the full ~12 min pass.
Terminal 1, gNB
cd ~/ntn/OCUDU-RAN
sudo ./build_ntn/apps/gnb/gnb -c configs/ntn/leo_rfsim_gnb.yml -c configs/ntn/leo_sattrain.yml -c configs/ntn/sdcore.yml
Terminal 2, UE (start within the 20 s lead), the same UE command as scenario 1 with 2>&1 | tee /tmp/ue_leo_sattrain.log appended.
Terminal 3, traffic and tracker
ping -I oaitun_ue1 -D -i 1 8.8.8.8
python3 ~/ntn/OCUDU-RAN/scripts/leo_tracker.py /tmp/gnb_leo_sattrain.log --ue /tmp/ue_leo_sattrain.log
Verify
gNB (/tmp/gnb_leo_sattrain.log). Every switch is followed at once by the next one being armed:
[NTN ] [I] Sat-train armed, cell=0x66c000 satellite 0 -> 1 at 20:25:34.645635951 (in 337 s): 0 sets through 4.5 deg, 1 will be at 21.9 deg
[NTN ] [I] Sat-switch promotion applied, cell=0x66c000 serving satellite 0 -> 1 time=20:25:34.662690833
[NTN ] [I] Sat-train armed, cell=0x66c000 satellite 1 -> 2 at 20:33:35.873628333 (in 481 s): 1 sets through 4.5 deg, 2 will be at 21.9 deg
Quick checks:
sudo grep -aE 'Sat-train armed|promotion applied' /tmp/gnb_leo_sattrain.log
grep -ac 'synch Failed' /tmp/ue_leo_sattrain.log # expect 0
grep -ac 'RRC moved into IDLE' /tmp/ue_leo_sattrain.log # expect 0
Measured on this testbed with 12 satellites, starting at zenith:
| Metric | Result |
|---|---|
| First switch | armed 337 s ahead, applied on time; satellite 0 released at 4.52 deg |
| Next switch | armed immediately, 481 s later; satellite 2 at 21.9 deg at the switch |
| UE | switched once at the UTC deadline and re-armed from the next SIB19 |
| Sync failures / RRC releases | 0 / 0 |
| Ping to 8.8.8.8 across the switch | 373 sent, 373 received, no sequence gaps |
Files
| File | Purpose |
|---|---|
configs/ntn/leo_rfsim_gnb.yml | Base LEO cell: ephemeris, epoch, K_offset 40 |
configs/ntn/sdcore.yml | SD-Core overlay for single-cell runs (AMF, bind addresses, slice) |
configs/ntn/leo_ho_sat1.yml / leo_ho_sat2.yml | Handover overlays: gNB IDs, PCIs, Xn, neighbour relations, SD-Core addresses, satellite ephemerides |
configs/ntn/leo_satswitch.yml | One-shot satellite switch overlay |
configs/ntn/leo_sattrain.yml | Satellite train overlay |
scripts/set_leo_pass.sh | Place satellites at a chosen elevation and stamp the epoch |
scripts/run_leo_ho_rfsim.sh | Scenario 2 in one command |
scripts/leo_tracker.py | Live dashboard |
OAI_RAN/targets/PROJECTS/GENERIC-NR-5GC/CONF/ue.ntn.leo.rfsim.conf | UE GNSS position (north pole) |
References
| Document | Relevance |
|---|---|
| TS 38.300 section 16.14 | NTN overview, transparent payload, Doppler responsibilities |
| TS 38.331 section 5.7.19, SIB19 | NTN-Config, satSwitchWithReSync-r18, t-ServiceStart-r17 |
| TS 38.213 section 4.2 | UE timing advance pre-compensation, K_offset |
| TS 38.423 | XnAP handover preparation |
| TS 38.413 | NGAP Path Switch |
| TR 38.821 | NR NTN solutions |
| TR 38.811 | NTN channel and deployment scenarios |
Source repositories
| Repository | Branch | Contents |
|---|---|---|
| OCUDU-RAN | ntn | gNB: rfsimulator NTN channel emulation, satellite train, configs, run scripts, tracker |
| OAI-RAN | rfsim_ocudu | nrUE: SIB19 Rel-18/19, satSwitchWithReSync-r18, UTC t-ServiceStart, NTN UE configs |
See also the GEO NTN rfsimulator testbed.