diff --git a/NOTICE b/NOTICE
index ff1b993a0b..a388a3a9b5 100644
--- a/NOTICE
+++ b/NOTICE
@@ -84,3 +84,11 @@ Nuand: https://github.com/Nuand/bladeRF/tree/master?tab=License-1-ov-file
Credits for https://github.com/pothosware/SoapySDR
Pothosware: BSL 1.0 License
+
+Credits for https://github.com/zeromq/libzmq
+ZeroMQ authors: Mozilla Public License Version 2.0
+
+Credits for source code:
+ - radio/zmq/zmq_imported.cpp
+ - radio/zmq/zmq_imported.h
+Software Radio Systems Limited: BSD-3-Clause-Open-MPI
diff --git a/ci-scripts/docker/Dockerfile.unittest.ubuntu b/ci-scripts/docker/Dockerfile.unittest.ubuntu
index 3c95c678e8..722da5f0db 100644
--- a/ci-scripts/docker/Dockerfile.unittest.ubuntu
+++ b/ci-scripts/docker/Dockerfile.unittest.ubuntu
@@ -13,11 +13,12 @@ RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade --yes && \
DEBIAN_FRONTEND=noninteractive apt-get install --yes \
libgtest-dev \
- libyaml-cpp-dev
+ libyaml-cpp-dev \
+ libzmq3-dev
RUN rm -Rf /oai-ran
WORKDIR /oai-ran
COPY . .
WORKDIR /oai-ran/build
-RUN cmake -GNinja -DENABLE_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -DSANITIZE_ADDRESS=True .. && ninja tests
+RUN cmake -GNinja -DENABLE_TESTS=ON -DOAI_ZMQ=ON -DCMAKE_BUILD_TYPE=Debug -DSANITIZE_ADDRESS=True .. && ninja tests
diff --git a/ci-scripts/xml_files/container_5g_zmq_2x2.xml b/ci-scripts/xml_files/container_5g_zmq_2x2.xml
new file mode 100644
index 0000000000..40babfeeb9
--- /dev/null
+++ b/ci-scripts/xml_files/container_5g_zmq_2x2.xml
@@ -0,0 +1,109 @@
+
+
+
+ zmq-5gnr-tdd-2x2
+ Monolithic SA TDD 2x2 gNB with zmq radio
+ wrench
+
+
+ Pull_Local_Registry
+ Pull Images from Local Registry
+ localhost
+ oai-gnb-asan oai-nr-ue-asan
+
+
+
+ Create_Workspace
+ Create new Workspace
+ localhost
+
+
+
+ Deploy_Object
+ Deploy OAI 5G CoreNetwork
+ localhost
+ ci-scripts/yaml_files/5g_zmq_radio_2x2/
+ mysql oai-amf oai-smf oai-upf oai-ext-dn
+
+
+
+ Deploy_Object
+ Deploy OAI 5G gNB+UE RF sim SA
+ localhost
+ ci-scripts/yaml_files/5g_zmq_radio_2x2
+ oai-gnb oai-nr-ue
+
+
+
+ Attach_UE
+ Attach OAI UE (Wait for IP)
+ rfsim5g_ue
+ localhost
+
+
+
+ Ping
+ Ping ext-dn from NR-UE
+ rfsim5g_ue
+ localhost
+ rfsim5g_ext_dn
+ localhost
+ -c 20 -i 0.25
+ 5
+
+
+
+ Ping
+ Ping NR-UE from ext-dn
+ rfsim5g_ext_dn
+ localhost
+ rfsim5g_ue
+ localhost
+ -c 20 -i 0.25
+ 5
+
+
+
+ Iperf
+ Iperf (DL/10Mbps/UDP)(20 sec)
+ -u -b 10M -t 20 -R
+ rfsim5g_ue
+ localhost
+ rfsim5g_ext_dn
+ localhost
+ 5
+ 90
+
+
+
+ Iperf
+ Iperf (UL/3Mbps/UDP)(20 sec)
+ -u -b 3M -t 20
+ rfsim5g_ue
+ localhost
+ rfsim5g_ext_dn
+ localhost
+ 5
+ 90
+
+
+
+ Undeploy_Object
+ true
+ Undeploy all OAI 5G stack
+ localhost
+ ci-scripts/yaml_files/5g_zmq_radio_2x2
+
+ oai-gnb=RetxCheck=1,0,0,0 oai-gnb=EndsWithBye oai-nr-ue
+
+
+
+
+ Clean_Test_Server_Images
+ true
+ Clean Test Images on Test Server
+ localhost
+ oai-gnb-asan oai-nr-ue-asan
+
+
+
diff --git a/ci-scripts/xml_files/container_5g_zmq_ocudu_1x1.xml b/ci-scripts/xml_files/container_5g_zmq_ocudu_1x1.xml
new file mode 100644
index 0000000000..c53d6f850a
--- /dev/null
+++ b/ci-scripts/xml_files/container_5g_zmq_ocudu_1x1.xml
@@ -0,0 +1,109 @@
+
+
+
+ ocudu-5gnr-tdd-1x1
+ OCUDU gnb split8 with OAI UE
+ wrench
+
+
+ Pull_Local_Registry
+ Pull Images from Local Registry
+ localhost
+ oai-gnb-asan oai-nr-ue-asan
+
+
+
+ Create_Workspace
+ Create new Workspace
+ localhost
+
+
+
+ Deploy_Object
+ Deploy OAI 5G CoreNetwork
+ localhost
+ ci-scripts/yaml_files/5g_zmq_radio_1x1_ocudu
+ mysql oai-amf oai-smf oai-upf oai-ext-dn
+
+
+
+ Deploy_Object
+ Deploy OCUDU Split 8 gNB + OAI UE
+ localhost
+ ci-scripts/yaml_files/5g_zmq_radio_1x1_ocudu
+ ocudu-gnb oai-nr-ue
+
+
+
+ Attach_UE
+ Attach OAI UE (Wait for IP)
+ rfsim5g_ue
+ localhost
+
+
+
+ Ping
+ Ping ext-dn from NR-UE
+ rfsim5g_ue
+ localhost
+ rfsim5g_ext_dn
+ localhost
+ -c 20 -i 0.25
+ 5
+
+
+
+ Ping
+ Ping NR-UE from ext-dn
+ rfsim5g_ext_dn
+ localhost
+ rfsim5g_ue
+ localhost
+ -c 20 -i 0.25
+ 5
+
+
+
+ Iperf
+ Iperf (DL/3Mbps/UDP)(20 sec)
+ -u -b 3M -t 20 -R
+ rfsim5g_ue
+ localhost
+ rfsim5g_ext_dn
+ localhost
+ 5
+ 90
+
+
+
+ Iperf
+ Iperf (UL/3Mbps/UDP)(20 sec)
+ -u -b 3M -t 20
+ rfsim5g_ue
+ localhost
+ rfsim5g_ext_dn
+ localhost
+ 5
+ 90
+
+
+
+ Undeploy_Object
+ true
+ Undeploy all OAI 5G stack
+ localhost
+ ci-scripts/yaml_files/5g_zmq_radio_1x1_ocudu
+
+ oai-nr-ue
+
+
+
+
+ Clean_Test_Server_Images
+ true
+ Clean Test Images on Test Server
+ localhost
+ oai-gnb-asan oai-nr-ue-asan
+
+
+
diff --git a/ci-scripts/xml_files/container_5g_zmq_ocudu_2x2.xml b/ci-scripts/xml_files/container_5g_zmq_ocudu_2x2.xml
new file mode 100644
index 0000000000..2e54fc849e
--- /dev/null
+++ b/ci-scripts/xml_files/container_5g_zmq_ocudu_2x2.xml
@@ -0,0 +1,109 @@
+
+
+
+ ocudu-5gnr-tdd-2x2
+ OCUDU gnb split8 with OAI UE
+ wrench
+
+
+ Pull_Local_Registry
+ Pull Images from Local Registry
+ localhost
+ oai-gnb-asan oai-nr-ue-asan
+
+
+
+ Create_Workspace
+ Create new Workspace
+ localhost
+
+
+
+ Deploy_Object
+ Deploy OAI 5G CoreNetwork
+ localhost
+ ci-scripts/yaml_files/5g_zmq_radio_2x2_ocudu
+ mysql oai-amf oai-smf oai-upf oai-ext-dn
+
+
+
+ Deploy_Object
+ Deploy OCUDU Split 8 gNB + OAI UE
+ localhost
+ ci-scripts/yaml_files/5g_zmq_radio_2x2_ocudu
+ ocudu-gnb oai-nr-ue
+
+
+
+ Attach_UE
+ Attach OAI UE (Wait for IP)
+ rfsim5g_ue
+ localhost
+
+
+
+ Ping
+ Ping ext-dn from NR-UE
+ rfsim5g_ue
+ localhost
+ rfsim5g_ext_dn
+ localhost
+ -c 20 -i 0.25
+ 5
+
+
+
+ Ping
+ Ping NR-UE from ext-dn
+ rfsim5g_ext_dn
+ localhost
+ rfsim5g_ue
+ localhost
+ -c 20 -i 0.25
+ 5
+
+
+
+ Iperf
+ Iperf (DL/10Mbps/UDP)(20 sec)
+ -u -b 1M -t 20 -R
+ rfsim5g_ue
+ localhost
+ rfsim5g_ext_dn
+ localhost
+ 5
+ 90
+
+
+
+ Iperf
+ Iperf (UL/3Mbps/UDP)(20 sec)
+ -u -b 1M -t 20
+ rfsim5g_ue
+ localhost
+ rfsim5g_ext_dn
+ localhost
+ 5
+ 90
+
+
+
+ Undeploy_Object
+ true
+ Undeploy all OAI 5G stack
+ localhost
+ ci-scripts/yaml_files/5g_zmq_radio_2x2_ocudu
+
+ oai-nr-ue
+
+
+
+
+ Clean_Test_Server_Images
+ true
+ Clean Test Images on Test Server
+ localhost
+ oai-gnb-asan oai-nr-ue-asan
+
+
+
diff --git a/ci-scripts/yaml_files/5g_zmq_radio_1x1_ocudu/docker-compose.yaml b/ci-scripts/yaml_files/5g_zmq_radio_1x1_ocudu/docker-compose.yaml
new file mode 100644
index 0000000000..6866b6a0b1
--- /dev/null
+++ b/ci-scripts/yaml_files/5g_zmq_radio_1x1_ocudu/docker-compose.yaml
@@ -0,0 +1,162 @@
+# SPDX-License-Identifier: MIT
+
+services:
+ mysql:
+ container_name: "rfsim5g-mysql"
+ image: mysql:8.0
+ init: true
+ volumes:
+ - ../5g_rfsimulator/oai_db.sql:/docker-entrypoint-initdb.d/oai_db.sql
+ - ../5g_rfsimulator/mysql-healthcheck.sh:/tmp/mysql-healthcheck.sh
+ environment:
+ - TZ=Europe/Paris
+ - MYSQL_DATABASE=oai_db
+ - MYSQL_USER=test
+ - MYSQL_PASSWORD=test
+ - MYSQL_ROOT_PASSWORD=linux
+ healthcheck:
+ test: /bin/bash -c "/tmp/mysql-healthcheck.sh"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 30
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.131
+ oai-amf:
+ container_name: "rfsim5g-oai-amf"
+ image: oaisoftwarealliance/oai-amf:v2.1.10
+ environment:
+ - TZ=Europe/paris
+ volumes:
+ - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-amf/etc/config.yaml
+ depends_on:
+ - mysql
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.132
+ oai-smf:
+ container_name: "rfsim5g-oai-smf"
+ image: oaisoftwarealliance/oai-smf:v2.1.10
+ environment:
+ - TZ=Europe/Paris
+ volumes:
+ - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-smf/etc/config.yaml
+ depends_on:
+ - oai-amf
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.133
+ oai-upf:
+ container_name: "rfsim5g-oai-upf"
+ image: oaisoftwarealliance/oai-upf:v2.1.10
+ init: true
+ environment:
+ - TZ=Europe/Paris
+ volumes:
+ - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-upf/etc/config.yaml
+ depends_on:
+ - oai-smf
+ cap_add:
+ - NET_ADMIN
+ - SYS_ADMIN
+ cap_drop:
+ - ALL
+ privileged: true
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.134
+ interface_name: eth0
+ traffic_net:
+ ipv4_address: 192.168.72.134
+ interface_name: eth1
+ oai-ext-dn:
+ privileged: true
+ container_name: rfsim5g-oai-ext-dn
+ image: oaisoftwarealliance/trf-gen-cn5g:focal
+ init: true
+ entrypoint: /bin/bash -c \
+ "iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;"\
+ "ip route add 12.1.1.0/24 via 192.168.72.134 dev eth0; sleep infinity"
+ depends_on:
+ - oai-upf
+ networks:
+ traffic_net:
+ ipv4_address: 192.168.72.135
+ healthcheck:
+ test: /bin/bash -c "ping -c 2 192.168.72.134"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ ocudu-gnb:
+ image: ghcr.io/bpodrygajlo/ocudu/ocudu-avx2:7cecca5
+ container_name: ocudu-gnb
+ cap_drop:
+ - ALL
+ cap_add:
+ - SYS_NICE
+ depends_on:
+ - oai-ext-dn
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.140
+ volumes:
+ - ./ocudu.yml:/ocudu.yml
+ healthcheck:
+ test: /bin/bash -c "pgrep gnb_split_8"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ command: ./usr/local/bin/gnb_split_8 -c /ocudu.yml
+
+ oai-nr-ue:
+ image: ${REGISTRY-oaisoftwarealliance/}${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
+ container_name: rfsim5g-oai-nr-ue
+ cap_drop:
+ - ALL
+ cap_add:
+ - NET_ADMIN # for interface bringup
+ - NET_RAW # for ping
+ - SYS_NICE
+ environment:
+ USE_ADDITIONAL_OPTIONS: -r 51 --numerology 1 --band 78 -C 3489420000 --ssb 0 -E --uicc0.imsi 208990100001100 --uecap_file /opt/oai-nr-ue/etc/uecap.xml --log_config.global_log_options level,nocolor,time --device.name oai_zmqdevif --zmq.[0].tx_channels tcp://0.0.0.0:4556 --zmq.[0].rx_channels tcp://192.168.71.140:4558
+ depends_on:
+ - ocudu-gnb
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.150
+ devices:
+ - /dev/net/tun:/dev/net/tun
+ volumes:
+ - ../../../targets/PROJECTS/GENERIC-NR-5GC/CONF/uecap_ports1.xml:/opt/oai-nr-ue/etc/uecap.xml
+ - ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
+ healthcheck:
+ test: /bin/bash -c "pgrep nr-uesoftmodem"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+networks:
+ public_net:
+ driver: bridge
+ name: rfsim5g-oai-public-net
+ ipam:
+ config:
+ - subnet: 192.168.71.128/26
+ driver_opts:
+ com.docker.network.bridge.name: "rfsim5g-public"
+ traffic_net:
+ driver: bridge
+ name: rfsim5g-oai-traffic-net
+ ipam:
+ config:
+ - subnet: 192.168.72.128/26
+ driver_opts:
+ com.docker.network.bridge.name: "rfsim5g-traffic"
diff --git a/ci-scripts/yaml_files/5g_zmq_radio_1x1_ocudu/ocudu.yml b/ci-scripts/yaml_files/5g_zmq_radio_1x1_ocudu/ocudu.yml
new file mode 100644
index 0000000000..da62a52e1f
--- /dev/null
+++ b/ci-scripts/yaml_files/5g_zmq_radio_1x1_ocudu/ocudu.yml
@@ -0,0 +1,52 @@
+# SPDX-FileCopyrightText: Copyright (C) 2021-2026 Software Radio Systems Limited
+# SPDX-License-Identifier: BSD-3-Clause-Open-MPI
+# Refer to https://gitlab.com/ocudu/ocudu/-/raw/dev/LICENSE?ref_type=heads
+
+cu_cp:
+ amf:
+ addrs: 192.168.71.132
+ port: 38412
+ bind_addrs: 192.168.71.140
+ supported_tracking_areas:
+ - tac: 1
+ plmn_list:
+ - plmn: "20899"
+ tai_slice_support_list:
+ - sst: 1
+
+ru_sdr:
+ device_driver: zmq
+ device_args: tx_port=tcp://0.0.0.0:4558,rx_port=tcp://192.168.71.150:4556
+ srate: 23.04
+ tx_gain: 25
+ rx_gain: 25
+
+cell_cfg:
+ dl_arfcn: 632628
+ band: 78
+ channel_bandwidth_MHz: 20
+ common_scs: 30
+ plmn: "20899"
+ tac: 1
+ pci: 1
+ tdd_ul_dl_cfg:
+ dl_ul_tx_period: 5
+ nof_dl_slots: 3
+ nof_dl_symbols: 10
+ nof_ul_slots: 1
+ nof_ul_symbols: 2
+ csi:
+ csi_rs_enabled: false
+ pucch:
+ formats: f0_and_f2
+ nof_cell_csi_res: 0
+
+log:
+ filename: gnb.log
+ all_level: debug
+
+pcap:
+ mac_enable: false
+ mac_filename: /tmp/gnb_mac.pcap
+ ngap_enable: false
+ ngap_filename: /tmp/gnb_ngap.pcap
diff --git a/ci-scripts/yaml_files/5g_zmq_radio_2x2/docker-compose.yaml b/ci-scripts/yaml_files/5g_zmq_radio_2x2/docker-compose.yaml
new file mode 100644
index 0000000000..45f6a8bedd
--- /dev/null
+++ b/ci-scripts/yaml_files/5g_zmq_radio_2x2/docker-compose.yaml
@@ -0,0 +1,164 @@
+# SPDX-License-Identifier: MIT
+
+services:
+ mysql:
+ container_name: "rfsim5g-mysql"
+ image: mysql:8.0
+ init: true
+ volumes:
+ - ../5g_rfsimulator/oai_db.sql:/docker-entrypoint-initdb.d/oai_db.sql
+ - ../5g_rfsimulator/mysql-healthcheck.sh:/tmp/mysql-healthcheck.sh
+ environment:
+ - TZ=Europe/Paris
+ - MYSQL_DATABASE=oai_db
+ - MYSQL_USER=test
+ - MYSQL_PASSWORD=test
+ - MYSQL_ROOT_PASSWORD=linux
+ healthcheck:
+ test: /bin/bash -c "/tmp/mysql-healthcheck.sh"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 30
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.131
+ oai-amf:
+ container_name: "rfsim5g-oai-amf"
+ image: oaisoftwarealliance/oai-amf:v2.1.10
+ environment:
+ - TZ=Europe/paris
+ volumes:
+ - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-amf/etc/config.yaml
+ depends_on:
+ - mysql
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.132
+ oai-smf:
+ container_name: "rfsim5g-oai-smf"
+ image: oaisoftwarealliance/oai-smf:v2.1.10
+ environment:
+ - TZ=Europe/Paris
+ volumes:
+ - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-smf/etc/config.yaml
+ depends_on:
+ - oai-amf
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.133
+ oai-upf:
+ container_name: "rfsim5g-oai-upf"
+ image: oaisoftwarealliance/oai-upf:v2.1.10
+ init: true
+ environment:
+ - TZ=Europe/Paris
+ volumes:
+ - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-upf/etc/config.yaml
+ depends_on:
+ - oai-smf
+ cap_add:
+ - NET_ADMIN
+ - SYS_ADMIN
+ cap_drop:
+ - ALL
+ privileged: true
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.134
+ interface_name: eth0
+ traffic_net:
+ ipv4_address: 192.168.72.134
+ interface_name: eth1
+ oai-ext-dn:
+ privileged: true
+ container_name: rfsim5g-oai-ext-dn
+ image: oaisoftwarealliance/trf-gen-cn5g:focal
+ init: true
+ entrypoint: /bin/bash -c \
+ "iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;"\
+ "ip route add 12.1.1.0/24 via 192.168.72.134 dev eth0; sleep infinity"
+ depends_on:
+ - oai-upf
+ networks:
+ traffic_net:
+ ipv4_address: 192.168.72.135
+ healthcheck:
+ test: /bin/bash -c "ping -c 2 192.168.72.134"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ oai-gnb:
+ image: ${REGISTRY-oaisoftwarealliance/}${GNB_IMG:-oai-gnb}:${TAG:-develop}
+ container_name: rfsim5g-oai-gnb
+ cap_drop:
+ - ALL
+ cap_add:
+ - SYS_NICE
+ environment:
+ USE_ADDITIONAL_OPTIONS: --log_config.global_log_options level,nocolor,time --device.name oai_zmqdevif --zmq.[0].tx_channels tcp://0.0.0.0:4558,tcp://0.0.0.0:4559 --zmq.[0].rx_channels tcp://oai-nr-ue:4556,tcp://oai-nr-ue:4557
+ ASAN_OPTIONS: detect_leaks=0
+ depends_on:
+ - oai-ext-dn
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.140
+ volumes:
+ - ../../conf_files/gnb.sa.band78.273prb.rfsim.2x2.conf:/opt/oai-gnb/etc/gnb.conf
+ healthcheck:
+ test: /bin/bash -c "pgrep nr-softmodem"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ oai-nr-ue:
+ image: ${REGISTRY-oaisoftwarealliance/}${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
+ container_name: rfsim5g-oai-nr-ue
+ cap_drop:
+ - ALL
+ cap_add:
+ - NET_ADMIN # for interface bringup
+ - NET_RAW # for ping
+ - SYS_NICE
+ environment:
+ USE_ADDITIONAL_OPTIONS: -r 273 --numerology 1 --band 78 -C 3450720000 --ssb 1518 --ue-nb-ant-tx 2 --uicc0.imsi 208990100001100 --ue-nb-ant-rx 2 --uecap_file /opt/oai-nr-ue/etc/uecap.xml --log_config.global_log_options level,nocolor,time --device.name oai_zmqdevif --zmq.[0].tx_channels tcp://0.0.0.0:4556,tcp://0.0.0.0:4557 --zmq.[0].rx_channels tcp://oai-gnb:4558,tcp://oai-gnb:4559
+ depends_on:
+ - oai-gnb
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.150
+ devices:
+ - /dev/net/tun:/dev/net/tun
+ volumes:
+ - ../../../targets/PROJECTS/GENERIC-NR-5GC/CONF/uecap_ports2.xml:/opt/oai-nr-ue/etc/uecap.xml
+ - ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
+ healthcheck:
+ test: /bin/bash -c "pgrep nr-uesoftmodem"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+networks:
+ public_net:
+ driver: bridge
+ name: rfsim5g-oai-public-net
+ ipam:
+ config:
+ - subnet: 192.168.71.128/26
+ driver_opts:
+ com.docker.network.bridge.name: "rfsim5g-public"
+ traffic_net:
+ driver: bridge
+ name: rfsim5g-oai-traffic-net
+ ipam:
+ config:
+ - subnet: 192.168.72.128/26
+ driver_opts:
+ com.docker.network.bridge.name: "rfsim5g-traffic"
diff --git a/ci-scripts/yaml_files/5g_zmq_radio_2x2_ocudu/docker-compose.yaml b/ci-scripts/yaml_files/5g_zmq_radio_2x2_ocudu/docker-compose.yaml
new file mode 100644
index 0000000000..2d6c240e6d
--- /dev/null
+++ b/ci-scripts/yaml_files/5g_zmq_radio_2x2_ocudu/docker-compose.yaml
@@ -0,0 +1,162 @@
+# SPDX-License-Identifier: MIT
+
+services:
+ mysql:
+ container_name: "rfsim5g-mysql"
+ image: mysql:8.0
+ init: true
+ volumes:
+ - ../5g_rfsimulator/oai_db.sql:/docker-entrypoint-initdb.d/oai_db.sql
+ - ../5g_rfsimulator/mysql-healthcheck.sh:/tmp/mysql-healthcheck.sh
+ environment:
+ - TZ=Europe/Paris
+ - MYSQL_DATABASE=oai_db
+ - MYSQL_USER=test
+ - MYSQL_PASSWORD=test
+ - MYSQL_ROOT_PASSWORD=linux
+ healthcheck:
+ test: /bin/bash -c "/tmp/mysql-healthcheck.sh"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 30
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.131
+ oai-amf:
+ container_name: "rfsim5g-oai-amf"
+ image: oaisoftwarealliance/oai-amf:v2.1.10
+ environment:
+ - TZ=Europe/paris
+ volumes:
+ - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-amf/etc/config.yaml
+ depends_on:
+ - mysql
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.132
+ oai-smf:
+ container_name: "rfsim5g-oai-smf"
+ image: oaisoftwarealliance/oai-smf:v2.1.10
+ environment:
+ - TZ=Europe/Paris
+ volumes:
+ - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-smf/etc/config.yaml
+ depends_on:
+ - oai-amf
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.133
+ oai-upf:
+ container_name: "rfsim5g-oai-upf"
+ image: oaisoftwarealliance/oai-upf:v2.1.10
+ init: true
+ environment:
+ - TZ=Europe/Paris
+ volumes:
+ - ../5g_rfsimulator/mini_nonrf_config.yaml:/openair-upf/etc/config.yaml
+ depends_on:
+ - oai-smf
+ cap_add:
+ - NET_ADMIN
+ - SYS_ADMIN
+ cap_drop:
+ - ALL
+ privileged: true
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.134
+ interface_name: eth0
+ traffic_net:
+ ipv4_address: 192.168.72.134
+ interface_name: eth1
+ oai-ext-dn:
+ privileged: true
+ container_name: rfsim5g-oai-ext-dn
+ image: oaisoftwarealliance/trf-gen-cn5g:focal
+ init: true
+ entrypoint: /bin/bash -c \
+ "iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE;"\
+ "ip route add 12.1.1.0/24 via 192.168.72.134 dev eth0; sleep infinity"
+ depends_on:
+ - oai-upf
+ networks:
+ traffic_net:
+ ipv4_address: 192.168.72.135
+ healthcheck:
+ test: /bin/bash -c "ping -c 2 192.168.72.134"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ ocudu-gnb:
+ image: ghcr.io/bpodrygajlo/ocudu/ocudu-avx2:7cecca5
+ container_name: ocudu-gnb
+ cap_drop:
+ - ALL
+ cap_add:
+ - SYS_NICE
+ depends_on:
+ - oai-ext-dn
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.140
+ volumes:
+ - ./ocudu.yml:/ocudu.yml
+ healthcheck:
+ test: /bin/bash -c "pgrep gnb_split_8"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 5
+ command: ./usr/local/bin/gnb_split_8 -c /ocudu.yml
+
+ oai-nr-ue:
+ image: ${REGISTRY-oaisoftwarealliance/}${NRUE_IMG:-oai-nr-ue}:${TAG:-develop}
+ container_name: rfsim5g-oai-nr-ue
+ cap_drop:
+ - ALL
+ cap_add:
+ - NET_ADMIN # for interface bringup
+ - NET_RAW # for ping
+ - SYS_NICE
+ environment:
+ USE_ADDITIONAL_OPTIONS: -r 106 --numerology 1 --band 78 -C 3489420000 --ssb 42 --ue-nb-ant-tx 2 --uicc0.imsi 208990100001100 --ue-nb-ant-rx 2 --uecap_file /opt/oai-nr-ue/etc/uecap.xml --log_config.global_log_options level,nocolor,time --device.name oai_zmqdevif --zmq.[0].tx_channels tcp://0.0.0.0:4556,tcp://0.0.0.0:4557 --zmq.[0].rx_channels tcp://192.168.71.140:4558,tcp://192.168.71.140:4559
+ depends_on:
+ - ocudu-gnb
+ networks:
+ public_net:
+ ipv4_address: 192.168.71.150
+ devices:
+ - /dev/net/tun:/dev/net/tun
+ volumes:
+ - ../../../targets/PROJECTS/GENERIC-NR-5GC/CONF/uecap_ports2.xml:/opt/oai-nr-ue/etc/uecap.xml
+ - ../../conf_files/nrue.uicc.conf:/opt/oai-nr-ue/etc/nr-ue.conf
+ healthcheck:
+ test: /bin/bash -c "pgrep nr-uesoftmodem"
+ start_period: 10s
+ start_interval: 500ms
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+networks:
+ public_net:
+ driver: bridge
+ name: rfsim5g-oai-public-net
+ ipam:
+ config:
+ - subnet: 192.168.71.128/26
+ driver_opts:
+ com.docker.network.bridge.name: "rfsim5g-public"
+ traffic_net:
+ driver: bridge
+ name: rfsim5g-oai-traffic-net
+ ipam:
+ config:
+ - subnet: 192.168.72.128/26
+ driver_opts:
+ com.docker.network.bridge.name: "rfsim5g-traffic"
diff --git a/ci-scripts/yaml_files/5g_zmq_radio_2x2_ocudu/ocudu.yml b/ci-scripts/yaml_files/5g_zmq_radio_2x2_ocudu/ocudu.yml
new file mode 100644
index 0000000000..fdef405af7
--- /dev/null
+++ b/ci-scripts/yaml_files/5g_zmq_radio_2x2_ocudu/ocudu.yml
@@ -0,0 +1,54 @@
+# SPDX-FileCopyrightText: Copyright (C) 2021-2026 Software Radio Systems Limited
+# SPDX-License-Identifier: BSD-3-Clause-Open-MPI
+# Refer to https://gitlab.com/ocudu/ocudu/-/raw/dev/LICENSE?ref_type=heads
+
+cu_cp:
+ amf:
+ addrs: 192.168.71.132
+ port: 38412
+ bind_addrs: 192.168.71.140
+ supported_tracking_areas:
+ - tac: 1
+ plmn_list:
+ - plmn: "20899"
+ tai_slice_support_list:
+ - sst: 1
+
+ru_sdr:
+ device_driver: zmq
+ device_args: tx_port=tcp://0.0.0.0:4558,tx_port=tcp://0.0.0.0:4559,rx_port=tcp://192.168.71.150:4556,rx_port=tcp://192.168.71.150:4557
+ srate: 61.44
+ tx_gain: 25
+ rx_gain: 25
+
+cell_cfg:
+ dl_arfcn: 632628
+ band: 78
+ channel_bandwidth_MHz: 40
+ common_scs: 30
+ plmn: "20899"
+ tac: 1
+ pci: 1
+ nof_antennas_ul: 2
+ nof_antennas_dl: 2
+ tdd_ul_dl_cfg:
+ dl_ul_tx_period: 5
+ nof_dl_slots: 3
+ nof_dl_symbols: 10
+ nof_ul_slots: 1
+ nof_ul_symbols: 2
+ csi:
+ csi_rs_enabled: false
+ pucch:
+ formats: f0_and_f2
+ nof_cell_csi_res: 0
+
+log:
+ filename: gnb.log
+ all_level: debug
+
+pcap:
+ mac_enable: false
+ mac_filename: /tmp/gnb_mac.pcap
+ ngap_enable: false
+ ngap_filename: /tmp/gnb_ngap.pcap
diff --git a/cmake_targets/build_oai b/cmake_targets/build_oai
index ca387cda32..95793206bf 100755
--- a/cmake_targets/build_oai
+++ b/cmake_targets/build_oai
@@ -68,7 +68,7 @@ Options:
--UE-conf-nvram [configuration file]
Specify conf_nvram_path (default \"$conf_nvram_path\")
-w | --hardware
- USRP, BLADERF, LMSSDR, IRIS, SIMU, AW2SORI, AERIAL, None (Default)
+ USRP, BLADERF, LMSSDR, IRIS, SIMU, AW2SORI, AERIAL, ZMQ, None (Default)
Adds this RF board support (in external packages installation and in compilation)
-t | --transport
Selects the transport protocol type, options: None, Ethernet, oran_fhlib_5g, oran_fhlib_5g_mplane, WLS
@@ -223,7 +223,7 @@ function main() {
shift 2;;
-w | --hardware)
case "$2" in
- "USRP" | "BLADERF" | "LMSSDR" | "IRIS")
+ "USRP" | "BLADERF" | "LMSSDR" | "IRIS" | "ZMQ")
HWs+=" OAI_"$2
TARGET_LIST="$TARGET_LIST oai_${2,,}devif" # ,, makes lowercase
CMAKE_CMD="$CMAKE_CMD -DOAI_$2=ON"
diff --git a/common/config/config_load_configmodule.c b/common/config/config_load_configmodule.c
index 087c8f622d..a47e1958ae 100644
--- a/common/config/config_load_configmodule.c
+++ b/common/config/config_load_configmodule.c
@@ -282,12 +282,9 @@ configmodule_interface_t *load_configmodule(int argc,
cfgmode = strdup(CONFIG_LIBCONFIGFILE);
}
}
- static configmodule_interface_t *cfgptr = NULL;
- if (cfgptr)
- fprintf(stderr, "ERROR: Call load_configmodule more than one time\n");
// The macros are not thread safe print_params and similar
- cfgptr = calloc(sizeof(configmodule_interface_t), 1);
+ configmodule_interface_t *cfgptr = calloc(sizeof(configmodule_interface_t), 1);
if (!cfgptr) {
fprintf(stderr, "ERROR: cannot allocate a memory for configuration\n");
return NULL;
diff --git a/common/config/config_load_configmodule.h b/common/config/config_load_configmodule.h
index 5e535f67f7..a47b6b1801 100644
--- a/common/config/config_load_configmodule.h
+++ b/common/config/config_load_configmodule.h
@@ -10,6 +10,9 @@
#ifndef INCLUDE_CONFIG_LOADCONFIGMODULE_H
#define INCLUDE_CONFIG_LOADCONFIGMODULE_H
+#ifdef __cplusplus
+extern "C" {
+#endif
#include
#include
@@ -117,5 +120,8 @@ void write_parsedcfg(configmodule_interface_t *cfg);
extern void free_configmodule(void);
#define CONFIG_PRINTF_ERROR(f, x... ) if (isLogInitDone ()) { LOG_E(ENB_APP,f,x);} else {printf(f,x);}; if ( !CONFIG_ISFLAGSET(CONFIG_NOABORTONCHKF) ) exit_fun("exit because configuration failed\n");
+#ifdef __cplusplus
+}
+#endif
#endif /* INCLUDE_CONFIG_LOADCONFIGMODULE_H */
diff --git a/common/config/config_paramdesc.h b/common/config/config_paramdesc.h
index efb5c956d2..ccca1dbd51 100644
--- a/common/config/config_paramdesc.h
+++ b/common/config/config_paramdesc.h
@@ -181,6 +181,11 @@ typedef struct paramdef {
{ \
OPTNAME(name), HELPSTR(help), PARAMFLAG(flags), .u64ptr = ptr, .defuintval = defval, PARAMTYPE(TYPE_UINT64), .numelt = 0 \
}
+#define STRINGLISTPARAM(name, help, flags, ptr, defval) \
+ { \
+ OPTNAME(name), HELPSTR(help), PARAMFLAG(flags), .strptr = ptr, .defstrlistval = defval, PARAMTYPE(TYPE_STRINGLIST), \
+ .numelt = 0 \
+ }
typedef struct paramlist_def {
char listname[MAX_OPTNAME_SIZE];
diff --git a/doc/README.md b/doc/README.md
index d1dfc2dc4d..02be593a7e 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -108,6 +108,7 @@ Some directories under `radio` contain READMEs:
- [fhi_72](../radio/fhi_72/README.md)
- [vrtsim](../radio/vrtsim/README.md)
- [rf_emulator](../radio/emulator/README.md)
+- [zmq](../radio/zmq/README.md)
The other SDRs (AW2S, LimeSDR, ...) have no READMEs.
diff --git a/docker/Dockerfile.base.ubuntu b/docker/Dockerfile.base.ubuntu
index fc258b226c..0fa2112047 100644
--- a/docker/Dockerfile.base.ubuntu
+++ b/docker/Dockerfile.base.ubuntu
@@ -40,7 +40,8 @@ RUN apt-get update && \
wget \
flatbuffers-compiler \
libflatbuffers-dev \
- libnanomsg-dev && \
+ libnanomsg-dev \
+ libzmq3-dev && \
# --break-system-packages allows externally managed environment in pip
pip3 install --ignore-installed pyyaml --break-system-packages
diff --git a/docker/Dockerfile.build.ubuntu b/docker/Dockerfile.build.ubuntu
index 3050a5c51e..29c1278025 100644
--- a/docker/Dockerfile.build.ubuntu
+++ b/docker/Dockerfile.build.ubuntu
@@ -32,6 +32,7 @@ RUN \
-w USRP -t Ethernet -w BLADERF \
--build-e2 --cmake-opt -DKPM_VERSION=$KPM_VERSION --cmake-opt -DE2AP_VERSION=$E2AP_VERSION \
--cmake-opt -DOAI_VRTSIM_TAPS_CLIENT=ON \
+ -w ZMQ \
--noavx512 \
--build-tool-opt -k10 \
--cmake-opt -DT_RECORD_DB=ON \
diff --git a/docker/Dockerfile.gNB.ubuntu b/docker/Dockerfile.gNB.ubuntu
index d10e476127..d76cbcbd39 100644
--- a/docker/Dockerfile.gNB.ubuntu
+++ b/docker/Dockerfile.gNB.ubuntu
@@ -47,7 +47,8 @@ RUN apt-get update && \
python3-requests \
libusb-1.0-0 \
libyaml-cpp-dev \
- libnanomsg5 && \
+ libnanomsg5 \
+ libzmq5 && \
# if the --sanitize option was used to build, additional packages are required
/bin/bash -c 'if [[ "$BUILD_OPTION" = "--sanitize" ]]; then DEBIAN_FRONTEND=noninteractive apt-get install --yes \
libasan8 \
@@ -78,6 +79,7 @@ COPY --from=gnb-build \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_rrc.so \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv_o1.so \
/oai-ran/cmake_targets/ran_build/build/libvrtsim.so \
+ /oai-ran/cmake_targets/ran_build/build/liboai_zmqdevif.so \
/usr/local/lib/
# Now we are copying from builder-image the UHD files.
@@ -105,7 +107,8 @@ RUN /bin/bash -c "ln -s /usr/local/lib/liboai_usrpdevif.so /usr/local/lib/liboai
/usr/local/lib/libldpc*.so \
/usr/local/lib/libtelnetsrv.so \
/usr/local/lib/libtelnetsrv_ci.so \
- /usr/local/lib/libuhd.so.4.8.0
+ /usr/local/lib/libuhd.so.4.8.0 \
+ /usr/local/lib/liboai_zmqdevif.so
WORKDIR /opt/oai-gnb
#EXPOSE 2152/udp # S1U, GTP/UDP
diff --git a/docker/Dockerfile.nrUE.ubuntu b/docker/Dockerfile.nrUE.ubuntu
index c38104f3bc..69aa85d550 100644
--- a/docker/Dockerfile.nrUE.ubuntu
+++ b/docker/Dockerfile.nrUE.ubuntu
@@ -50,7 +50,8 @@ RUN apt-get update && \
iperf \
libyaml-cpp-dev \
libzstd1 \
- libnanomsg5 && \
+ libnanomsg5 \
+ libzmq5 && \
# if the --sanitize option was used to build, additional packages are required
/bin/bash -c 'if [[ "$BUILD_OPTION" = "--sanitize" ]]; then DEBIAN_FRONTEND=noninteractive apt-get install --yes \
libasan8 \
@@ -83,6 +84,7 @@ COPY --from=nr-ue-build \
/oai-ran/cmake_targets/ran_build/build/libtelnetsrv.so \
/oai-ran/cmake_targets/ran_build/build/libparams_yaml.so \
/oai-ran/cmake_targets/ran_build/build/libvrtsim.so \
+ /oai-ran/cmake_targets/ran_build/build/liboai_zmqdevif.so \
# Wildcards so it doesn't fail if not present
/oai-ran/cmake_targets/ran_build/build/_deps*/fmtlog-build*/libfmtlog-shared*.so \
/oai-ran/cmake_targets/ran_build/build/_deps*/clickhouse-cpp-build*/clickhouse/libclickhouse*.so \
@@ -114,7 +116,8 @@ RUN /bin/bash -c "ln -s /usr/local/lib/liboai_usrpdevif.so /usr/local/lib/liboai
/usr/local/lib/libtelnetsrv.so \
/usr/local/lib/libtelnetsrv_ciUE.so \
/usr/local/lib/libtelnetsrv_5Gue.so \
- /usr/local/lib/libuhd.so.4.8.0
+ /usr/local/lib/libuhd.so.4.8.0 \
+ /usr/local/lib/liboai_zmqdevif.so
WORKDIR /opt/oai-nr-ue
diff --git a/executables/nr-softmodem.c b/executables/nr-softmodem.c
index 59f8b96194..aa33fc5f5d 100644
--- a/executables/nr-softmodem.c
+++ b/executables/nr-softmodem.c
@@ -131,13 +131,18 @@ void exit_function(const char *file, const char *function, const int line, const
printf("%s:%d %s() Exiting OAI softmodem: %s\n",file,line, function, s);
}
- oai_exit = 1;
-
if (RC.ru == NULL)
exit(-1); // likely init not completed, prevent crash or hang, exit now...
for (ru_id=0; ru_idrfdevice.trx_end_func) {
+ if (RC.ru[ru_id] == NULL) {
+ continue;
+ }
+ if (RC.ru[ru_id]->ifdevice.trx_stop_func) {
+ RC.ru[ru_id]->ifdevice.trx_stop_func(&RC.ru[ru_id]->ifdevice);
+ RC.ru[ru_id]->ifdevice.trx_stop_func = NULL;
+ }
+ if (RC.ru[ru_id]->rfdevice.trx_end_func) {
if (RC.ru[ru_id]->rfdevice.trx_get_stats_func) {
RC.ru[ru_id]->rfdevice.trx_get_stats_func(&RC.ru[ru_id]->rfdevice);
RC.ru[ru_id]->rfdevice.trx_get_stats_func = NULL;
@@ -146,6 +151,10 @@ void exit_function(const char *file, const char *function, const int line, const
RC.ru[ru_id]->rfdevice.trx_end_func = NULL;
}
+ if (RC.ru[ru_id]->ifdevice.trx_stop_func) {
+ RC.ru[ru_id]->ifdevice.trx_stop_func(&RC.ru[ru_id]->ifdevice);
+ RC.ru[ru_id]->ifdevice.trx_stop_func = NULL;
+ }
if (RC.ru[ru_id] && RC.ru[ru_id]->ifdevice.trx_end_func) {
if (RC.ru[ru_id]->ifdevice.trx_get_stats_func) {
RC.ru[ru_id]->ifdevice.trx_get_stats_func(&RC.ru[ru_id]->ifdevice);
@@ -156,6 +165,8 @@ void exit_function(const char *file, const char *function, const int line, const
}
}
+ oai_exit = 1;
+
if (assert) {
abort();
} else {
diff --git a/executables/nr-ue-ru.c b/executables/nr-ue-ru.c
index 52b234a137..1b0817478e 100644
--- a/executables/nr-ue-ru.c
+++ b/executables/nr-ue-ru.c
@@ -352,6 +352,14 @@ void nrue_ru_start(void)
}
}
+void nrue_ru_stop(void)
+{
+ for (int ru_id = 0; ru_id < nrue_ru_count; ru_id++) {
+ if (openair0_dev[ru_id].trx_stop_func)
+ openair0_dev[ru_id].trx_stop_func(&openair0_dev[ru_id]);
+ }
+}
+
void nrue_ru_end(void)
{
for (int ru_id = 0; ru_id < nrue_ru_count; ru_id++) {
diff --git a/executables/nr-ue-ru.h b/executables/nr-ue-ru.h
index 2bbd2dea89..7d0836c050 100644
--- a/executables/nr-ue-ru.h
+++ b/executables/nr-ue-ru.h
@@ -22,6 +22,7 @@ void nrue_set_ru_params(configmodule_interface_t *cfg);
void nrue_init_openair0(void);
void nrue_ru_start(void);
+void nrue_ru_stop(void);
void nrue_ru_end(void);
void nrue_ru_set_freq(PHY_VARS_NR_UE *UE, uint64_t ul_carrier, uint64_t dl_carrier, int freq_offset);
int nrue_ru_adjust_rx_gain(PHY_VARS_NR_UE *UE, int gain_change);
diff --git a/executables/nr-uesoftmodem.c b/executables/nr-uesoftmodem.c
index 2069da4c00..7997f72ef5 100644
--- a/executables/nr-uesoftmodem.c
+++ b/executables/nr-uesoftmodem.c
@@ -112,6 +112,7 @@ void exit_function(const char *file, const char *function, const int line, const
oai_exit = 1;
+ nrue_ru_stop();
nrue_ru_end();
if (assert) {
@@ -457,6 +458,8 @@ int main(int argc, char **argv)
LOG_W(NR_PHY, "Returned from ITTI signal handler\n");
oai_exit = 1;
+ nrue_ru_stop();
+
if (PHY_vars_UE_g && PHY_vars_UE_g[0]) {
for (int CC_id = 0; CC_id < MAX_NUM_CCs; CC_id++) {
PHY_VARS_NR_UE *phy_vars = PHY_vars_UE_g[0][CC_id];
diff --git a/openair2/LAYER2/NR_MAC_UE/tests/test_nr_ue_power_procedures.cpp b/openair2/LAYER2/NR_MAC_UE/tests/test_nr_ue_power_procedures.cpp
index c6ac903a4c..6fcc239228 100644
--- a/openair2/LAYER2/NR_MAC_UE/tests/test_nr_ue_power_procedures.cpp
+++ b/openair2/LAYER2/NR_MAC_UE/tests/test_nr_ue_power_procedures.cpp
@@ -691,9 +691,11 @@ TEST(test_srs_power, tpc_accumulation)
int main(int argc, char** argv)
{
logInit();
- load_configmodule(argc, argv, CONFIG_ENABLECMDLINEONLY);
+ configmodule_interface_t *uniqCfg = load_configmodule(argc, argv, CONFIG_ENABLECMDLINEONLY);
g_log->log_component[MAC].level = OAILOG_DEBUG;
g_log->log_component[NR_MAC].level = OAILOG_DEBUG;
testing::InitGoogleTest(&argc, argv);
- return RUN_ALL_TESTS();
+ int ret = RUN_ALL_TESTS();
+ end_configmodule(uniqCfg);
+ return ret;
}
diff --git a/openair2/LAYER2/NR_MAC_UE/tests/test_nr_ue_ra_procedures.cpp b/openair2/LAYER2/NR_MAC_UE/tests/test_nr_ue_ra_procedures.cpp
index 0fb34a9a37..29dcba5a7a 100644
--- a/openair2/LAYER2/NR_MAC_UE/tests/test_nr_ue_ra_procedures.cpp
+++ b/openair2/LAYER2/NR_MAC_UE/tests/test_nr_ue_ra_procedures.cpp
@@ -171,9 +171,11 @@ TEST(test_init_ra, four_step_cfra)
int main(int argc, char **argv)
{
logInit();
- load_configmodule(argc, argv, CONFIG_ENABLECMDLINEONLY);
+ configmodule_interface_t *uniqCfg = load_configmodule(argc, argv, CONFIG_ENABLECMDLINEONLY);
g_log->log_component[MAC].level = OAILOG_DEBUG;
g_log->log_component[NR_MAC].level = OAILOG_DEBUG;
testing::InitGoogleTest(&argc, argv);
- return RUN_ALL_TESTS();
+ int ret = RUN_ALL_TESTS();
+ end_configmodule(uniqCfg);
+ return ret;
}
diff --git a/radio/CMakeLists.txt b/radio/CMakeLists.txt
index 5a8028253d..285224cd32 100644
--- a/radio/CMakeLists.txt
+++ b/radio/CMakeLists.txt
@@ -51,3 +51,8 @@ add_boolean_option(OAI_RF_EMULATOR ON "Activate OAI's RF emulator" OFF)
if(OAI_RF_EMULATOR)
add_subdirectory(emulator)
endif()
+
+add_boolean_option(OAI_ZMQ OFF "Activate OAI's ZMQ radio" OFF)
+if(OAI_ZMQ)
+ add_subdirectory(zmq)
+endif()
diff --git a/radio/zmq/CMakeLists.txt b/radio/zmq/CMakeLists.txt
new file mode 100644
index 0000000000..ce23cb5c9d
--- /dev/null
+++ b/radio/zmq/CMakeLists.txt
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: LicenseRef-CSSL-1.0
+
+pkg_check_modules(zmq REQUIRED libzmq)
+add_library(ring_buffer ring_buffer.cpp)
+add_library(zmq_imported zmq_imported.cpp)
+target_include_directories(zmq_imported PUBLIC ./)
+target_link_libraries(zmq_imported PUBLIC ${zmq_LIBRARIES} log_headers)
+
+target_include_directories(ring_buffer PUBLIC ./)
+add_library(oai_zmqdevif SHARED zmq_radio.cpp)
+target_link_libraries(oai_zmqdevif PUBLIC ${zmq_LIBRARIES} ring_buffer log_headers zmq_imported)
+target_include_directories(oai_zmqdevif PUBLIC ${zmq_INCLUDE_DIRS})
+set_target_properties(oai_zmqdevif PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
+
+add_library(zmq_radio_static STATIC zmq_radio.cpp)
+target_link_libraries(zmq_radio_static PUBLIC ${zmq_LIBRARIES} ring_buffer log_headers zmq_imported)
+target_include_directories(zmq_radio_static PUBLIC ${zmq_INCLUDE_DIRS})
+
+if (ENABLE_TESTS)
+ add_subdirectory(tests)
+endif()
diff --git a/radio/zmq/README.md b/radio/zmq/README.md
new file mode 100644
index 0000000000..48c66d6d82
--- /dev/null
+++ b/radio/zmq/README.md
@@ -0,0 +1,40 @@
+
+
+# Overview
+
+This library implements ZMQ-based radio driver
+
+# Architecture
+
+The radio simulates RX/TX pairs between different processes over network or on the same machine
+as ZMQ REQ/REP socket pairs. All the antennas have to be configured and connected before the
+simulation can start.
+
+# Limitations
+
+You cannot reconnect a device once the simulation has started. The simulation has to be restarted
+from scratch in order to add devices or restart a process.
+
+# Requirements
+
+Depends on zmq library being installed in the system. On ubuntu: `libzmq3-dev`, on RHEL/Fedora: `zeromq-devel`
+
+# Usage
+
+## Simple 1-to-1 antenna mapping
+
+Add `--device.name oai_zmqdevif` to load the library in UE / gNB process. Add `--zmq.[0].tx_channels ` and
+`--zmq.[0].rx_channels ` to define ZMQ REQ/REP pairs.
+
+On the opposite side, load the library like specified above but invert the `rx` and `tx` channels. This way all
+antennas of UE will be directly mapped to all antennas of the gNB.
+
+### Example:
+
+```
+sudo ./nr-uesoftmodem -r 106 --numerology 1 --band 78 -C 3619200000 --device.name oai_zmqdevif --zmq.[0].tx_channels tcp://127.0.0.1:4557 --zmq.[0].rx_channels tcp://127.0.0.1:4556 --ssb 516
+```
+
+```
+sudo ./nr-softmodem -O ../targets/PROJECTS/GENERIC-NR-5GC/CONF/gnb.sa.band78.fr1.106PRB.usrpb210.conf --gNBs.[0].min_rxtxtime 6 --device.name oai_zmqdevif --zmq.[0].tx_channels tcp://127.0.0.1:4556 --zmq.[0].rx_channels tcp://127.0.0.1:4557
+```
diff --git a/radio/zmq/ring_buffer.cpp b/radio/zmq/ring_buffer.cpp
new file mode 100644
index 0000000000..c4fdd94b29
--- /dev/null
+++ b/radio/zmq/ring_buffer.cpp
@@ -0,0 +1,167 @@
+/*
+ * SPDX-License-Identifier: LicenseRef-CSSL-1.0
+ */
+
+#include "ring_buffer.h"
+#include
+#include
+#include
+
+ring_buffer::ring_buffer(size_t max_size) : max_size_(max_size)
+{
+ buffer_ = std::make_unique(max_size);
+}
+
+size_t ring_buffer::push_samples(const cf_t *samples, const size_t nsamps)
+{
+ size_t overflow = 0;
+ // if nsamps > max_size skip nsamps - max_size samples
+ size_t nsamps_left = nsamps;
+ if (nsamps > max_size_) {
+ samples += nsamps - max_size_;
+ nsamps_left = max_size_;
+ overflow += nsamps - max_size_;
+ }
+
+ // Detect overflow
+ if (size_ + nsamps_left > max_size_) {
+ size_t newtail__pos = (head_ + nsamps_left) % max_size_;
+ overflow += (size_ + nsamps_left) - max_size_;
+ tail_ = newtail__pos;
+ }
+
+ size_t first_chunk = std::min(nsamps_left, max_size_ - head_);
+ memcpy(&buffer_[head_], samples, first_chunk * sizeof(cf_t));
+ head_ = (head_ + first_chunk) % max_size_;
+ samples += first_chunk;
+ nsamps_left -= first_chunk;
+ if (nsamps_left > 0) {
+ memcpy(&buffer_[0], samples, nsamps_left * sizeof(cf_t));
+ head_ = nsamps_left;
+ }
+
+ size_ = std::min(size_ + nsamps, max_size_);
+
+ return overflow;
+}
+
+size_t ring_buffer::push_zeros(const size_t num_zeros)
+{
+ size_t overflow = 0;
+ // if nsamps > max_size skip nsamps - max_size samples
+ size_t nsamps_left = num_zeros;
+ if (num_zeros > max_size_) {
+ nsamps_left = max_size_;
+ overflow += num_zeros - max_size_;
+ }
+
+ // Detect overflow
+ if (size_ + nsamps_left > max_size_) {
+ size_t new_tail_pos = (head_ + nsamps_left) % max_size_;
+ overflow += (size_ + nsamps_left) - max_size_;
+ tail_ = new_tail_pos;
+ }
+
+ size_t first_chunk = std::min(nsamps_left, max_size_ - head_);
+ memset(&buffer_[head_], 0, first_chunk * sizeof(cf_t));
+ head_ = (head_ + first_chunk) % max_size_;
+ nsamps_left -= first_chunk;
+ if (nsamps_left > 0) {
+ memset(&buffer_[0], 0, nsamps_left * sizeof(cf_t));
+ head_ = nsamps_left;
+ }
+
+ size_ = std::min(size_ + num_zeros, max_size_);
+
+ return overflow;
+}
+
+size_t ring_buffer::pop_samples(cf_t *samples, size_t num_samples)
+{
+ size_t samples_to_pop = std::min(size_, num_samples);
+ if (samples_to_pop > 0) {
+ if (tail_ + samples_to_pop > max_size_) {
+ size_t first_chunk = max_size_ - tail_;
+ memcpy(samples, &buffer_[tail_], first_chunk * sizeof(cf_t));
+ memcpy(samples + first_chunk, &buffer_[0], (samples_to_pop - first_chunk) * sizeof(cf_t));
+ } else {
+ memcpy(samples, &buffer_[tail_], samples_to_pop * sizeof(cf_t));
+ }
+ tail_ = (tail_ + samples_to_pop) % max_size_;
+ size_ -= samples_to_pop;
+ return samples_to_pop;
+ }
+ return 0;
+}
+
+void ring_buffer::clear_samples()
+{
+ head_ = 0;
+ tail_ = 0;
+ size_ = 0;
+}
+
+void ring_buffer::reset()
+{
+ clear_samples();
+}
+
+size_t ring_buffer::size() const
+{
+ return size_;
+}
+
+size_t overflow_buffer::push_samples(const cf_t *samples, size_t nsamps)
+{
+ std::lock_guard lock(mutex_);
+ size_t overflow = buffer_.push_samples(samples, nsamps);
+ zeros_to_send_ += overflow;
+ return overflow;
+}
+
+size_t overflow_buffer::push_zeros(size_t num_zeros)
+{
+ std::lock_guard lock(mutex_);
+ size_t overflow = buffer_.push_zeros(num_zeros);
+ zeros_to_send_ += overflow;
+ return overflow;
+}
+
+size_t overflow_buffer::pop_samples(cf_t *samples, size_t num_samples)
+{
+ std::lock_guard lock(mutex_);
+ size_t samples_popped = 0;
+ if (zeros_to_send_ > 0) {
+ size_t num_zeros = std::min(zeros_to_send_, num_samples);
+ memset(samples, 0, num_zeros * sizeof(cf_t));
+ zeros_to_send_ -= num_zeros;
+ samples += num_zeros;
+ num_samples -= num_zeros;
+ samples_popped += num_zeros;
+ }
+
+ if (num_samples > 0) {
+ samples_popped += buffer_.pop_samples(samples, num_samples);
+ }
+ return samples_popped;
+}
+
+void overflow_buffer::reset()
+{
+ std::lock_guard lock(mutex_);
+ buffer_.reset();
+ zeros_to_send_ = buffer_.size() / 2;
+}
+
+void overflow_buffer::clear_samples()
+{
+ std::lock_guard lock(mutex_);
+ buffer_.clear_samples();
+ zeros_to_send_ = 0;
+}
+
+size_t overflow_buffer::size()
+{
+ std::lock_guard lock(mutex_);
+ return buffer_.size() + zeros_to_send_;
+}
diff --git a/radio/zmq/ring_buffer.h b/radio/zmq/ring_buffer.h
new file mode 100644
index 0000000000..40a877427e
--- /dev/null
+++ b/radio/zmq/ring_buffer.h
@@ -0,0 +1,46 @@
+/*
+ * SPDX-License-Identifier: LicenseRef-CSSL-1.0
+ */
+
+#pragma once
+#include
+#include
+#include
+#include "common/platform_types.h"
+
+// A basic cirular sample buffer class
+class ring_buffer {
+ private:
+ std::unique_ptr buffer_;
+ size_t head_ = 0;
+ size_t tail_ = 0;
+ size_t size_ = 0;
+ size_t max_size_;
+
+ public:
+ ring_buffer(size_t max_size = 614400);
+ size_t push_samples(const cf_t *samples, size_t nsamps);
+ size_t push_zeros(size_t num_zeros);
+ size_t pop_samples(cf_t *samples, size_t num_samples);
+ void reset();
+ void clear_samples();
+ size_t size() const;
+};
+
+// A thread-safe wrapper around ring_buffer that counts overflows
+class overflow_buffer {
+ ring_buffer buffer_;
+ size_t zeros_to_send_;
+ std::mutex mutex_;
+
+ public:
+ overflow_buffer(size_t max_size = 614400) : buffer_(max_size), zeros_to_send_(0)
+ {
+ }
+ size_t push_samples(const cf_t *samples, size_t nsamps);
+ size_t push_zeros(size_t num_zeros);
+ size_t pop_samples(cf_t *samples, size_t num_samples);
+ void reset();
+ void clear_samples();
+ size_t size();
+};
diff --git a/radio/zmq/tests/CMakeLists.txt b/radio/zmq/tests/CMakeLists.txt
new file mode 100644
index 0000000000..aa46be194b
--- /dev/null
+++ b/radio/zmq/tests/CMakeLists.txt
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: LicenseRef-CSSL-1.0
+
+add_executable(test_ring_buffer test_ring_buffer.cpp)
+target_link_libraries(test_ring_buffer PRIVATE ring_buffer GTest::gtest)
+add_dependencies(tests test_ring_buffer)
+add_test(NAME test_ring_buffer
+ COMMAND ./test_ring_buffer)
+
+add_executable(test_zmq_radio test_zmq_radio.cpp)
+target_link_libraries(test_zmq_radio PRIVATE zmq_radio_static UTIL CONFIG_LIB GTest::gtest zmq)
+add_dependencies(tests test_zmq_radio)
+add_test(NAME test_zmq_radio
+ COMMAND ./test_zmq_radio)
diff --git a/radio/zmq/tests/test_ring_buffer.cpp b/radio/zmq/tests/test_ring_buffer.cpp
new file mode 100644
index 0000000000..8687139b13
--- /dev/null
+++ b/radio/zmq/tests/test_ring_buffer.cpp
@@ -0,0 +1,221 @@
+/*
+ * SPDX-License-Identifier: LicenseRef-CSSL-1.0
+ */
+
+#include
+#include "ring_buffer.h"
+
+TEST(CircularBuffer, simplePushPop)
+{
+ ring_buffer cb(10);
+ cf_t data[10];
+ for (int i = 0; i < 10; i++) {
+ data[i] = { (float)i, (float)(i + 1)};
+ }
+ ASSERT_EQ(cb.push_samples(data, 10), 0);
+
+ cf_t read_data[10];
+ ASSERT_EQ(cb.pop_samples(read_data, 10), 10);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(read_data[i].r, data[i].r);
+ ASSERT_EQ(read_data[i].i, data[i].i);
+ }
+
+ ASSERT_EQ(cb.size(), 0);
+ ASSERT_EQ(cb.pop_samples(read_data, 10), 0);
+}
+
+TEST(CircularBuffer, overflow)
+{
+ ring_buffer cb(10);
+ cf_t data[15];
+ for (int i = 0; i < 15; i++) {
+ data[i] = {(float)i, (float)(i + 1)};
+ }
+ ASSERT_EQ(cb.push_samples(data, 15), 5);
+ cf_t read_data[10];
+ ASSERT_EQ(cb.pop_samples(read_data, 10), 10);
+ // The first 5 samples are lost due to overflow
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(read_data[i].r, i + 5);
+ ASSERT_EQ(read_data[i].i, i + 6);
+ }
+}
+
+TEST(CircularBuffer, partialPop)
+{
+ ring_buffer cb(10);
+ cf_t data[10];
+ for (int i = 0; i < 10; i++) {
+ data[i] = {(float)i, (float)(i + 1)};
+ }
+ ASSERT_EQ(cb.push_samples(data, 10), 0);
+ cf_t read_data[5];
+ ASSERT_EQ(cb.pop_samples(read_data, 5), 5);
+ for (int i = 0; i < 5; i++) {
+ ASSERT_EQ(read_data[i].r, i);
+ ASSERT_EQ(read_data[i].i, i + 1);
+ }
+ ASSERT_EQ(cb.size(), 5);
+ ASSERT_EQ(cb.pop_samples(read_data, 5), 5);
+ for (int i = 0; i < 5; i++) {
+ ASSERT_EQ(read_data[i].r, i + 5);
+ ASSERT_EQ(read_data[i].i, i + 6);
+ }
+}
+
+TEST(CircularBuffer, popMore)
+{
+ ring_buffer cb(10);
+ cf_t data[10];
+ for (int i = 0; i < 10; i++) {
+ data[i] = {(float)i, (float)(i + 1)};
+ }
+ cb.push_samples(data, 10);
+ cf_t read_data[15];
+ ASSERT_EQ(cb.pop_samples(read_data, 15), 10);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(read_data[i].r, i);
+ ASSERT_EQ(read_data[i].i, i + 1);
+ }
+}
+
+TEST(CircularBuffer, newOverflow)
+{
+ ring_buffer cb(10);
+ cf_t data1[8];
+ for (int i = 0; i < 8; i++) {
+ data1[i] = {(float)i, (float)(i + 1)};
+ }
+ ASSERT_EQ(cb.push_samples(data1, 8), 0);
+
+ cf_t data2[4];
+ for (int i = 0; i < 4; i++) {
+ data2[i] = {(float)(i + 8), (float)(i + 9)};
+ }
+ ASSERT_EQ(cb.push_samples(data2, 4), 2);
+
+ cf_t read_data[10];
+ ASSERT_EQ(cb.pop_samples(read_data, 10), 10);
+ // The first 2 samples are lost due to overflow
+ for (int i = 0; i < 8; i++) {
+ ASSERT_EQ(read_data[i].r, i + 2);
+ ASSERT_EQ(read_data[i].i, i + 3);
+ }
+ ASSERT_EQ(read_data[8].r, 10);
+ ASSERT_EQ(read_data[8].i, 11);
+ ASSERT_EQ(read_data[9].r, 11);
+ ASSERT_EQ(read_data[9].i, 12);
+}
+
+TEST(overflow_buffer, simplePushPop)
+{
+ overflow_buffer pb(10);
+ cf_t data[10];
+ for (int i = 0; i < 10; i++) {
+ data[i] = {(float)i, (float)(i + 1)};
+ }
+ pb.push_samples(data, 10);
+ ASSERT_EQ(pb.size(), 10);
+ cf_t read_data[10];
+ ASSERT_EQ(pb.pop_samples(read_data, 10), 10);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(read_data[i].r, i);
+ ASSERT_EQ(read_data[i].i, i + 1);
+ }
+}
+
+TEST(overflow_buffer, zeros_to_send)
+{
+ overflow_buffer pb(10);
+ pb.push_zeros(10);
+ cf_t data[5];
+ for (int i = 0; i < 5; i++) {
+ data[i] = {(float)i, (float)(i + 1)};
+ }
+ pb.push_samples(data, 5);
+ ASSERT_EQ(pb.size(), 15);
+ cf_t read_data[10];
+ ASSERT_EQ(pb.pop_samples(read_data, 10), 10);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(read_data[i].r, 0);
+ ASSERT_EQ(read_data[i].i, 0);
+ }
+ ASSERT_EQ(pb.pop_samples(read_data, 10), 5);
+ for (int i = 0; i < 5; i++) {
+ ASSERT_EQ(read_data[i].r, i);
+ ASSERT_EQ(read_data[i].i, i + 1);
+ }
+}
+
+TEST(overflow_buffer, no_zeros_to_send)
+{
+ overflow_buffer pb(10);
+ cf_t data[10];
+ for (int i = 0; i < 10; i++) {
+ data[i] = {(float)i, (float)(i + 1)};
+ }
+ pb.push_samples(data, 10);
+ ASSERT_EQ(pb.size(), 10);
+ cf_t read_data[10];
+ ASSERT_EQ(pb.pop_samples(read_data, 10), 10);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(read_data[i].r, i);
+ ASSERT_EQ(read_data[i].i, i + 1);
+ }
+}
+
+TEST(overflow_buffer, overflow)
+{
+ overflow_buffer pb(10);
+ cf_t data[15];
+ for (int i = 0; i < 15; i++) {
+ data[i] = {(float)i, (float)(i + 1)};
+ }
+ pb.push_samples(data, 15);
+ ASSERT_EQ(pb.size(), 15);
+ cf_t read_data[15];
+ ASSERT_EQ(pb.pop_samples(read_data, 15), 15);
+ // The first 5 samples were lost due to overflow.
+ int i = 0;
+ for (; i < 5; i++) {
+ ASSERT_EQ(read_data[i].r, 0);
+ ASSERT_EQ(read_data[i].i, 0);
+ }
+ for (; i < 15; i++) {
+ ASSERT_EQ(read_data[i].r, i);
+ ASSERT_EQ(read_data[i].i, i + 1);
+ }
+ ASSERT_EQ(pb.size(), 0);
+}
+
+TEST(overflow_buffer, push_zeros)
+{
+ overflow_buffer pb(10);
+ pb.push_zeros(5);
+ ASSERT_EQ(pb.size(), 5);
+ cf_t data[5];
+ for (int i = 0; i < 5; i++) {
+ data[i] = {(float)i, (float)(i + 1)};
+ }
+ pb.push_samples(data, 5);
+ ASSERT_EQ(pb.size(), 10);
+ cf_t read_data[10];
+ ASSERT_EQ(pb.pop_samples(read_data, 10), 10);
+ for (int i = 0; i < 10; i++) {
+ if (i < 5) {
+ ASSERT_EQ(read_data[i].r, 0);
+ ASSERT_EQ(read_data[i].i, 0);
+ } else {
+ ASSERT_EQ(read_data[i].r, data[i - 5].r);
+ ASSERT_EQ(read_data[i].i, data[i - 5].i);
+ }
+ }
+ ASSERT_EQ(pb.size(), 0);
+}
+
+int main(int argc, char **argv)
+{
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/radio/zmq/tests/test_zmq_radio.cpp b/radio/zmq/tests/test_zmq_radio.cpp
new file mode 100644
index 0000000000..78e8e6b85c
--- /dev/null
+++ b/radio/zmq/tests/test_zmq_radio.cpp
@@ -0,0 +1,162 @@
+/*
+ * SPDX-License-Identifier: LicenseRef-CSSL-1.0
+ */
+
+#include "common_lib.h"
+#include
+#include "common/config/config_userapi.h"
+#include
+extern "C" {
+#include "common/config/config_userapi.h"
+#include "openair1/SIMULATION/TOOLS/sim.h"
+extern int device_init(openair0_device_t *device, openair0_config_t *openair0_cfg);
+static softmodem_params_t softmodem_params;
+softmodem_params_t *get_softmodem_params(void)
+{
+ return &softmodem_params;
+}
+}
+#include "common/platform_types.h"
+#include
+
+configmodule_interface_t *uniqCfg = NULL;
+
+extern "C" void exit_function(const char *file, const char *function, const int line, const char *s, const int assert)
+{
+ fprintf(stderr, "FATAL: %s at %s:%s:%d\n", s, file, function, line);
+ exit(EXIT_FAILURE);
+}
+
+class ZMQTest : public ::testing::Test {
+ protected:
+ configmodule_interface_t *cfg1 = nullptr;
+ configmodule_interface_t *cfg2 = nullptr;
+ openair0_device_t device1 = {0};
+ openair0_device_t device2 = {0};
+ openair0_config_t config1 = {0};
+ openair0_config_t config2 = {0};
+ char *argv[4];
+
+ void SetUp() override
+ {
+ argv[0] = strdup("--zmq.[0].tx_channels");
+ argv[1] = strdup("tcp://127.0.0.1:5555");
+ argv[2] = strdup("--zmq.[0].rx_channels");
+ argv[3] = strdup("tcp://127.0.0.1:5556");
+
+ cfg1 = load_configmodule(sizeofArray(argv), argv, CONFIG_ENABLECMDLINEONLY);
+ uniqCfg = cfg1;
+ // 2. Initialize the ZMQ device
+ config1.tx_num_channels = 1;
+ config1.rx_num_channels = 1;
+ config1.sample_rate = 30;
+ ASSERT_EQ(device_init(&device1, &config1), 0);
+ ASSERT_EQ(device1.trx_start_func(&device1), 0);
+
+ // Swap the RX with TX for second device
+ char *tmp = argv[0];
+ argv[0] = argv[2];
+ argv[2] = tmp;
+ cfg2 = load_configmodule(sizeofArray(argv), argv, CONFIG_ENABLECMDLINEONLY);
+ uniqCfg = cfg2;
+ config2.tx_num_channels = 1;
+ config2.rx_num_channels = 1;
+ config2.sample_rate = 1500;
+ ASSERT_EQ(device_init(&device2, &config2), 0);
+ ASSERT_EQ(device2.trx_start_func(&device2), 0);
+ }
+
+ void TearDown() override
+ {
+ if (device1.trx_end_func) {
+ device1.trx_end_func(&device1);
+ }
+ if (device2.trx_end_func) {
+ device2.trx_end_func(&device2);
+ }
+ if (cfg1) {
+ end_configmodule(cfg1);
+ }
+ if (cfg2) {
+ end_configmodule(cfg2);
+ }
+ for (auto i = 0U; i < sizeofArray(argv); i++) {
+ free(argv[i]);
+ }
+ }
+};
+
+TEST_F(ZMQTest, RXSamples)
+{
+ std::thread t1([this]() {
+ c16_t rx_samples[10];
+ openair0_timestamp_t rx_timestamp;
+ void *samples[1] = {rx_samples};
+ ASSERT_EQ(device1.trx_read_func(&device1, &rx_timestamp, samples, 10, 1), 10);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(rx_samples[i].r, 0);
+ ASSERT_EQ(rx_samples[i].i, 0);
+ }
+ });
+ std::thread t2([this]() {
+ c16_t rx_samples[10];
+ openair0_timestamp_t rx_timestamp;
+ void *samples[1] = {rx_samples};
+ ASSERT_EQ(device2.trx_read_func(&device2, &rx_timestamp, samples, 10, 1), 10);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(rx_samples[i].r, 0);
+ ASSERT_EQ(rx_samples[i].i, 0);
+ }
+ });
+ t1.join();
+ t2.join();
+}
+
+TEST_F(ZMQTest, TxRxSamples)
+{
+ std::thread t1([this]() {
+ c16_t rx_samples[10];
+ openair0_timestamp_t rx_timestamp;
+ void *samples[1] = {rx_samples};
+ ASSERT_EQ(device1.trx_read_func(&device1, &rx_timestamp, samples, 10, 1), 10);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(rx_samples[i].r, 0);
+ ASSERT_EQ(rx_samples[i].i, 0);
+ }
+ c16_t tx_samples[10];
+ openair0_timestamp_t tx_timestamp = rx_timestamp + 10;
+ for (int i = 0; i < 10; i++) {
+ tx_samples[i].r = i;
+ tx_samples[i].i = i + 1;
+ }
+ samples[0] = tx_samples;
+ ASSERT_EQ(device1.trx_write_func(&device1, tx_timestamp, samples, 10, 1, 0), 10);
+ });
+ std::thread t2([this]() {
+ c16_t rx_samples[10];
+ openair0_timestamp_t rx_timestamp;
+ void *samples[1] = {rx_samples};
+ ASSERT_EQ(device2.trx_read_func(&device2, &rx_timestamp, samples, 10, 1), 10);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(rx_samples[i].r, 0);
+ ASSERT_EQ(rx_samples[i].i, 0);
+ }
+ openair0_timestamp_t rx_timestamp2;
+ ASSERT_EQ(device2.trx_read_func(&device2, &rx_timestamp2, samples, 10, 1), 10);
+ for (int i = 0; i < 10; i++) {
+ ASSERT_EQ(rx_samples[i].r, i);
+ ASSERT_EQ(rx_samples[i].i, i + 1);
+ }
+ ASSERT_EQ(rx_timestamp + 10, rx_timestamp2);
+ });
+ t1.join();
+ t2.join();
+}
+
+int main(int argc, char **argv)
+{
+ logInit();
+ g_log->log_component[HW].level = OAILOG_DEBUG;
+ testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/radio/zmq/zmq_imported.cpp b/radio/zmq/zmq_imported.cpp
new file mode 100644
index 0000000000..19bf542098
--- /dev/null
+++ b/radio/zmq/zmq_imported.cpp
@@ -0,0 +1,131 @@
+/*
+ * SPDX-License-Identifier: BSD-3-Clause-Open-MPI
+ * Based on zmq library in OCUDU project: ocudu/lib/radio/zmq
+ * Refer to https://gitlab.com/ocudu/ocudu/-/raw/dev/LICENSE?ref_type=heads
+ */
+
+#include "zmq_imported.h"
+#include "log.h"
+
+const float c16_t_to_cf_t_factor = std::numeric_limits::max();
+static constexpr std::chrono::milliseconds TRANSMIT_TS_ALIGN_TIMEOUT = std::chrono::milliseconds(0);
+static constexpr std::chrono::milliseconds RECEIVE_TS_ALIGN_TIMEOUT = std::chrono::milliseconds(100);
+
+void zmq_tx_channel::transmit(c16_t *samples, size_t nsamps, uint64_t timestamp)
+{
+ std::scoped_lock lock(transmit_alignment_mutex_);
+ size_t overflow = 0;
+ if (timestamp > sample_count_) {
+ overflow += buffer_.push_zeros(timestamp - sample_count_);
+ sample_count_ = timestamp;
+ }
+ cf_t samples_float[nsamps];
+ for (size_t i = 0; i < nsamps; i++) {
+ samples_float[i].r = samples[i].r / c16_t_to_cf_t_factor;
+ samples_float[i].i = samples[i].i / c16_t_to_cf_t_factor;
+ }
+ overflow += buffer_.push_samples(samples_float, nsamps);
+ sample_count_ += nsamps;
+ if (overflow) {
+ LOG_W(HW, "Overflow on ZMQ channel by %lu samples\n", overflow);
+ }
+ is_tx_enabled_ = true;
+ transmit_alignment_cvar_.notify_all();
+}
+
+void zmq_tx_channel::start(uint64_t init_time)
+{
+ sample_count_ = init_time;
+}
+
+bool zmq_tx_channel::align(uint64_t timestamp, std::chrono::milliseconds timeout)
+{
+ if (sample_count_ >= timestamp) {
+ return sample_count_ > timestamp;
+ }
+ std::unique_lock lock(transmit_alignment_mutex_);
+ if (is_tx_enabled_ && (timeout.count() != 0)) {
+ bool is_not_timeout =
+ transmit_alignment_cvar_.wait_for(lock, timeout, [this, timestamp]() { return sample_count_ >= timestamp; });
+ if (is_not_timeout) {
+ return sample_count_ > timestamp;
+ }
+ LOG_W(HW, "Timeout waiting for TX path to align samples\n");
+ is_tx_enabled_ = false;
+ }
+ if (sample_count_ < timestamp) {
+ buffer_.push_zeros(timestamp - sample_count_);
+ sample_count_ = timestamp;
+ }
+ return false;
+}
+
+void zmq_rx_channel::receive(c16_t *samples, size_t nsamps)
+{
+ size_t samples_popped = 0;
+ cf_t samples_float[nsamps];
+ while (samples_popped < (size_t)nsamps && !stopped_) {
+ size_t popped_now = buffer_.pop_samples(samples_float + samples_popped, nsamps - samples_popped);
+ samples_popped += popped_now;
+ if (popped_now == 0) {
+ usleep(100); // wait for more samples to arrive
+ }
+ }
+ for (size_t i = 0; i < nsamps; i++) {
+ samples[i].r = samples_float[i].r * c16_t_to_cf_t_factor + 0.5;
+ samples[i].i = samples_float[i].i * c16_t_to_cf_t_factor + 0.5;
+ }
+}
+void zmq_rx_channel::stop()
+{
+ stopped_ = true;
+}
+
+void zmq_tx_stream::start(uint64_t init_time)
+{
+ for (auto &chan : channels_) {
+ chan->start(init_time);
+ }
+}
+bool zmq_tx_stream::align(uint64_t timestamp, std::chrono::milliseconds timeout)
+{
+ bool timestamp_passed = false;
+ for (auto &chan : channels_) {
+ timestamp_passed = timestamp_passed || chan->align(timestamp, timeout);
+ }
+ return timestamp_passed;
+}
+void zmq_tx_stream::transmit(c16_t **samples, size_t nsamps, uint64_t timestamp)
+{
+ bool timestamp_passed = align(timestamp, TRANSMIT_TS_ALIGN_TIMEOUT);
+ if (timestamp_passed) {
+ LOG_W(HW, "Error, channel timeout\n");
+ return;
+ }
+ int i = 0;
+ for (auto chan : channels_) {
+ chan->transmit(samples[i++], nsamps, timestamp);
+ }
+}
+
+void zmq_rx_stream::start(uint64_t init_time)
+{
+ sample_count_ = init_time;
+}
+void zmq_rx_stream::stop()
+{
+ for (auto &chan : channels_) {
+ chan->stop();
+ }
+}
+void zmq_rx_stream::receive(c16_t **samples, size_t nsamps, uint64_t *timestamp)
+{
+ *timestamp = sample_count_;
+ uint64_t passed_timestamp = sample_count_ + nsamps;
+ tx_stream_->align(passed_timestamp, RECEIVE_TS_ALIGN_TIMEOUT);
+ int i = 0;
+ for (auto chan : channels_) {
+ chan->receive(samples[i++], nsamps);
+ }
+ sample_count_ += nsamps;
+}
diff --git a/radio/zmq/zmq_imported.h b/radio/zmq/zmq_imported.h
new file mode 100644
index 0000000000..7262ea0605
--- /dev/null
+++ b/radio/zmq/zmq_imported.h
@@ -0,0 +1,71 @@
+/*
+ * SPDX-License-Identifier: BSD-3-Clause-Open-MPI
+ * Based on zmq library in OCUDU project: ocudu/lib/radio/zmq
+ * Refer to https://gitlab.com/ocudu/ocudu/-/raw/dev/LICENSE?ref_type=heads
+ */
+
+#ifndef ZMQ_IMPORTED_H
+#define ZMQ_IMPORTED_H
+
+#include
+#include "ring_buffer.h"
+#include
+#include
+#include
+#include
+
+class zmq_tx_channel {
+ public:
+ void *socket_;
+ overflow_buffer buffer_;
+ std::atomic sample_count_ = 0;
+ std::atomic is_tx_enabled_ = false;
+ std::mutex transmit_alignment_mutex_;
+ std::condition_variable transmit_alignment_cvar_;
+
+ zmq_tx_channel(void *s, uint64_t buffer_size) : socket_(s), buffer_(buffer_size)
+ {
+ }
+
+ void transmit(c16_t *samples, size_t nsamps, uint64_t timestamp);
+
+ void start(uint64_t init_time);
+
+ bool align(uint64_t timestamp, std::chrono::milliseconds timeout);
+};
+
+class zmq_rx_channel {
+ public:
+ void *socket_;
+ overflow_buffer buffer_;
+ bool request_sent_;
+ std::atomic stopped_;
+ zmq_rx_channel(void *s, uint64_t buffer_size) : socket_(s), buffer_(buffer_size), stopped_(false)
+ {
+ }
+ void receive(c16_t *samples, size_t nsamps);
+ void stop();
+};
+
+class zmq_tx_stream {
+ public:
+ std::vector channels_;
+ void start(uint64_t init_time);
+ bool align(uint64_t timestamp, std::chrono::milliseconds timeout);
+ void transmit(c16_t **samples, size_t nsamps, uint64_t timestamp);
+};
+
+class zmq_rx_stream {
+ public:
+ std::vector channels_;
+ zmq_tx_stream *tx_stream_;
+ uint64_t sample_count_ = 0;
+ zmq_rx_stream() : sample_count_(0)
+ {
+ }
+ void start(uint64_t init_time);
+ void stop();
+ void receive(c16_t **samples, size_t nsamps, uint64_t *timestamp);
+};
+
+#endif
diff --git a/radio/zmq/zmq_radio.cpp b/radio/zmq/zmq_radio.cpp
new file mode 100644
index 0000000000..93774bb2c3
--- /dev/null
+++ b/radio/zmq/zmq_radio.cpp
@@ -0,0 +1,345 @@
+/*
+ * SPDX-License-Identifier: LicenseRef-CSSL-1.0
+ */
+
+#include "PHY/TOOLS/tools_defs.h"
+#include "PHY/defs_common.h"
+#include "common/platform_types.h"
+#include "softmodem-common.h"
+#include "utils.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include "common_lib.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "zmq_imported.h"
+
+#define ZMQ_SECTION "zmq"
+#define ZMQ_TX_CHANNELS "tx_channels"
+#define ZMQ_RX_CHANNELS "rx_channels"
+
+#define ZMQ_PARAMS_DESC \
+ { \
+ STRINGLISTPARAM(ZMQ_TX_CHANNELS, "list of zmq addresses represeting tx channels_\n", PARAMFLAG_MANDATORY, nullptr, nullptr), \
+ STRINGLISTPARAM(ZMQ_RX_CHANNELS, "list of zmq addresses represeting rx channels_\n", PARAMFLAG_MANDATORY, nullptr, nullptr), \
+ };
+
+const size_t sample_size = sizeof(cf_t);
+const size_t rx_buffer_size = sample_size * 300000;
+
+typedef struct {
+ void *context;
+ zmq_tx_stream tx_stream;
+ zmq_rx_stream rx_stream;
+ std::thread poll_thread;
+ std::atomic poll_thread_running;
+ bool stopped = false;
+ double sample_rate;
+} zmq_state_t;
+
+static void poll_thread(zmq_state_t *s)
+{
+ s->poll_thread_running = true;
+ unsigned char *rx_buffer = static_cast(malloc(rx_buffer_size));
+ const auto num_tx_channels = s->tx_stream.channels_.size();
+ const auto num_rx_channels = s->rx_stream.channels_.size();
+ std::vector items(num_tx_channels + num_rx_channels);
+ std::vector reply_requested(num_tx_channels);
+ for (size_t i = 0; i < num_tx_channels; ++i) {
+ items[i] = {s->tx_stream.channels_[i]->socket_, 0, ZMQ_POLLIN, 0};
+ // wait for REQ
+ reply_requested[i] = false;
+ }
+ for (size_t i = 0; i < num_rx_channels; i++) {
+ items[i + num_tx_channels] = {s->rx_stream.channels_[i]->socket_, 0, ZMQ_POLLIN, 0};
+ }
+
+ const auto num_channels = num_tx_channels + num_rx_channels;
+ while (s->poll_thread_running) {
+ for (size_t i = 0; i < num_tx_channels; i++) {
+ auto chan = s->tx_stream.channels_[i];
+ if (!reply_requested[i]) {
+ continue;
+ }
+ std::vector samples(1024);
+ size_t num_popped = chan->buffer_.pop_samples(samples.data(), 1024);
+ if (num_popped == 0) {
+ continue;
+ }
+ int rc = zmq_send(chan->socket_, samples.data(), num_popped * sizeof(cf_t), 0);
+ if (rc < 0) {
+ LOG_E(HW, "[ZMQ] poll_thread zmq_send for TX antenna %d failed: %s\n", (int)i, zmq_strerror(errno));
+ }
+ reply_requested[i] = false;
+ }
+
+ int rc = zmq_poll(items.data(), num_channels, 10); // 10ms timeout
+ if (rc < 0) {
+ if (errno == EINTR)
+ continue;
+ LOG_E(HW, "[ZMQ] poll_thread zmq_poll failed: %s\n", zmq_strerror(errno));
+ break;
+ }
+ if (rc == 0) {
+ continue; // timeout
+ }
+
+ // --- TX Sockets (ZMQ_REP) ---
+ for (size_t i = 0; i < num_tx_channels; i++) {
+ if (items[i].revents & ZMQ_POLLIN) {
+ auto chan = s->tx_stream.channels_[i];
+ char dummy;
+ rc = zmq_recv(chan->socket_, &dummy, 1, 0);
+ if (rc < 0) {
+ LOG_E(HW, "[ZMQ] poll_thread zmq_recv for TX antenna %d failed: %s\n", (int)i, zmq_strerror(errno));
+ continue;
+ }
+ if (reply_requested[i]) {
+ LOG_E(HW, "[ZMQ] Error, unexpected REQ before REP on TX antenna %d\n", (int)i);
+ }
+ reply_requested[i] = true;
+ }
+ }
+
+ // --- RX Sockets (ZMQ_REQ) ---
+ for (size_t i = 0; i < num_rx_channels; i++) {
+ if (items[i + num_tx_channels].revents & ZMQ_POLLIN) {
+ auto chan = s->rx_stream.channels_[i];
+ rc = zmq_recv(chan->socket_, rx_buffer, rx_buffer_size, 0);
+ if (rc < 0) {
+ LOG_E(HW, "[ZMQ] poll_thread zmq_recv for RX antenna %d failed: %s\n", (int)i, zmq_strerror(errno));
+ } else {
+ size_t received_bytes = rc;
+ if (rx_buffer_size < received_bytes) {
+ LOG_W(HW,
+ "[ZMQ] the RX buffer is too small! The received message size is %lu while the buffer is %lu. Message truncated\n",
+ received_bytes,
+ rx_buffer_size);
+ }
+ size_t num_samples_received = std::min(received_bytes, rx_buffer_size) / sizeof(cf_t);
+ cf_t *samples = reinterpret_cast(rx_buffer);
+ size_t overflow = chan->buffer_.push_samples(samples, num_samples_received);
+ if (rx_buffer_size < received_bytes) {
+ overflow += chan->buffer_.push_zeros((received_bytes - rx_buffer_size) / sizeof(cf_t));
+ }
+ if (overflow) {
+ LOG_W(HW, "Overflow on receive\n");
+ }
+ // After receiving, send next request to keep the stream flowing
+ char dummy = 0;
+ if (zmq_send(chan->socket_, &dummy, 1, 0) != 1) {
+ LOG_E(HW, "[ZMQ] poll_thread zmq_send for RX antenna %d failed: %s\n", (int)i, zmq_strerror(errno));
+ }
+ }
+ }
+ }
+ }
+ free(rx_buffer);
+}
+
+static int zmq_write(openair0_device_t *device, openair0_timestamp_t timestamp, void **buff, int nsamps, int cc, int flags)
+{
+ zmq_state_t *s = static_cast(device->priv);
+ AssertFatal((uint)cc == s->tx_stream.channels_.size(),
+ "Request to write on more antennas (%d) than configured (%d)",
+ cc,
+ (int)s->tx_stream.channels_.size());
+
+ s->tx_stream.transmit((c16_t **)buff, nsamps, timestamp);
+
+ return nsamps;
+}
+
+static int zmq_read(openair0_device_t *device, openair0_timestamp_t *ptimestamp, void **samplesVoid, int nsamps, int nbAnt)
+{
+ zmq_state_t *s = static_cast(device->priv);
+ AssertFatal((uint)nbAnt == s->rx_stream.channels_.size(),
+ "Request to read on more antennas (%d) than configured (%d)",
+ nbAnt,
+ (int)s->rx_stream.channels_.size());
+ uint64_t timestamp;
+ s->rx_stream.receive((c16_t **)samplesVoid, nsamps, ×tamp);
+ *ptimestamp = timestamp;
+ return nsamps;
+}
+
+static int zmq_get_stats(openair0_device_t *device)
+{
+ return 0;
+}
+static int zmq_reset_stats(openair0_device_t *device)
+{
+ return 0;
+}
+static void zmq_end(openair0_device_t *device)
+{
+ zmq_state_t *s = static_cast(device->priv);
+ if (s) {
+ if (s->poll_thread_running) {
+ s->poll_thread_running = false;
+ if (s->poll_thread.joinable()) {
+ s->poll_thread.join();
+ }
+ }
+ for (auto &chan : s->tx_stream.channels_) {
+ if (chan->socket_)
+ zmq_close(chan->socket_);
+ delete chan;
+ }
+ s->tx_stream.channels_.clear();
+
+ for (auto &chan : s->rx_stream.channels_) {
+ if (chan->socket_)
+ zmq_close(chan->socket_);
+ delete chan;
+ }
+ s->rx_stream.channels_.clear();
+
+ if (s->context)
+ zmq_ctx_destroy(s->context);
+ delete s;
+ }
+}
+
+static int zmq_start(openair0_device_t *device)
+{
+ zmq_state_t *s = static_cast(device->priv);
+ s->rx_stream.start(s->sample_rate / 100);
+ s->tx_stream.start(s->sample_rate / 100);
+ for (size_t i = 0; i < s->rx_stream.channels_.size(); i++) {
+ auto channel = s->rx_stream.channels_[i];
+ // Send initial request to start data flow
+ char dummy = 0;
+ if (zmq_send(channel->socket_, &dummy, 1, 0) != 1) {
+ LOG_E(HW, "[ZMQ] zmq_send for initial RX request failed for antenna %lu: %s\n", i, zmq_strerror(errno));
+ return -1;
+ }
+ }
+ s->poll_thread = std::thread(poll_thread, s);
+ return 0;
+}
+
+static int zmq_stop(openair0_device_t *device)
+{
+ zmq_state_t *s = static_cast(device->priv);
+ s->rx_stream.stop();
+ return 0;
+}
+
+static int zmq_set_freq(openair0_device_t *device, openair0_config_t *openair0_cfg)
+{
+ return 0;
+}
+static int zmq_set_gains(openair0_device_t *device, openair0_config_t *openair0_cfg)
+{
+ return 0;
+}
+static int zmq_write_init(openair0_device_t *device)
+{
+ return 0;
+}
+
+extern "C" __attribute__((__visibility__("default"))) int device_init(openair0_device_t *device, openair0_config_t *openair0_cfg)
+{
+ auto *zmq_state = new zmq_state_t();
+ zmq_state->context = zmq_ctx_new();
+ AssertFatal(zmq_state->context != NULL, "zmq_ctx_new failed");
+
+ LOG_I(HW, "[ZMQ] tx_antennas: %d, rx_antennas: %d\n", openair0_cfg->tx_num_channels, openair0_cfg->rx_num_channels);
+ configmodule_interface_t *cfg = config_get_if();
+ paramdef_t param_desc[] = ZMQ_PARAMS_DESC;
+ std::string zmq_section = std::string(ZMQ_SECTION);
+ int ru_id = openair0_cfg->ru_id;
+ std::string zmq_array_section = std::string(ZMQ_SECTION) + ".[" + std::to_string(ru_id) + "]";
+ int ret = config_get(cfg, param_desc, sizeofArray(param_desc), zmq_array_section.c_str());
+ AssertFatal(ret >= 0, "configuration couldn't be performed\n");
+ int num_configured_tx_channels = gpd(param_desc, sizeofArray(param_desc), ZMQ_TX_CHANNELS)->numelt;
+ AssertFatal(num_configured_tx_channels == openair0_cfg->tx_num_channels,
+ "Incorrect configuration: Number of zmq tx channels (%d) != number of configured tx channels (%d)\n",
+ num_configured_tx_channels,
+ openair0_cfg->tx_num_channels);
+ int num_configured_rx_channels = gpd(param_desc, sizeofArray(param_desc), ZMQ_RX_CHANNELS)->numelt;
+ AssertFatal(num_configured_rx_channels == openair0_cfg->rx_num_channels,
+ "Incorrect configuration: Number of zmq rx channels (%d) != number of configured rx channels (%d)\n",
+ num_configured_rx_channels,
+ openair0_cfg->rx_num_channels);
+ char **tx_channels = gpd(param_desc, sizeofArray(param_desc), ZMQ_TX_CHANNELS)->strlistptr;
+ char **rx_channels = gpd(param_desc, sizeofArray(param_desc), ZMQ_RX_CHANNELS)->strlistptr;
+
+ // Setup TX sockets (one per antenna)
+ if (openair0_cfg->tx_num_channels > 0) {
+ zmq_state->tx_stream.channels_.resize(openair0_cfg->tx_num_channels);
+ for (int i = 0; i < openair0_cfg->tx_num_channels; i++) {
+ void *socket = zmq_socket(zmq_state->context, ZMQ_REP);
+ AssertFatal(socket != NULL, "zmq_socket(ZMQ_REP) for TX antenna %d failed", i);
+ int linger = 0;
+ zmq_setsockopt(socket, ZMQ_LINGER, &linger, sizeof(linger));
+ AssertFatal(zmq_bind(socket, tx_channels[i]) == 0, "zmq_bind for TX antenna %d failed on %s", i, tx_channels[i]);
+ auto channel = new zmq_tx_channel(socket, openair0_cfg->sample_rate);
+ LOG_I(HW, "[ZMQ] TX socket for antenna %d bound to %s\n", i, tx_channels[i]);
+ zmq_state->tx_stream.channels_[i] = channel;
+ }
+ }
+ zmq_state->sample_rate = openair0_cfg->sample_rate;
+
+ // Setup RX sockets (one per antenna)
+ if (openair0_cfg->rx_num_channels > 0) {
+ zmq_state->rx_stream.channels_.resize(openair0_cfg->rx_num_channels);
+ for (int i = 0; i < openair0_cfg->rx_num_channels; i++) {
+ void *socket = zmq_socket(zmq_state->context, ZMQ_REQ);
+ AssertFatal(socket != NULL, "zmq_socket(ZMQ_REQ) for RX antenna %d failed", i);
+ int linger = 0;
+ zmq_setsockopt(socket, ZMQ_LINGER, &linger, sizeof(linger));
+ AssertFatal(zmq_connect(socket, rx_channels[i]) == 0, "zmq_connect for RX antenna %d failed on %s", i, rx_channels[i]);
+ auto channel = new zmq_rx_channel(socket, openair0_cfg->sample_rate);
+ LOG_I(HW, "[ZMQ] RX socket for antenna %d connected to %s\n", i, rx_channels[i]);
+ zmq_state->rx_stream.channels_[i] = channel;
+ }
+ zmq_state->rx_stream.tx_stream_ = &zmq_state->tx_stream;
+ }
+
+ device->trx_start_func = zmq_start;
+ device->trx_get_stats_func = zmq_get_stats;
+ device->trx_reset_stats_func = zmq_reset_stats;
+ device->trx_end_func = zmq_end;
+ device->trx_stop_func = zmq_stop;
+ device->trx_set_freq_func = zmq_set_freq;
+ device->trx_set_gains_func = zmq_set_gains;
+ device->trx_write_func = zmq_write;
+ device->trx_read_func = zmq_read;
+ device->type = RFSIMULATOR;
+ IS_SOFTMODEM_RFSIM = 1U;
+ openair0_cfg->rx_gain[0] = 0;
+ device->openair0_cfg = openair0_cfg;
+ device->priv = zmq_state;
+ device->trx_write_init = zmq_write_init;
+
+ return 0;
+}