ci: simulate freq drift and verify freq correction

A new test case 5g_zmq_radio_trs is created by copying the folder
5g_zmq_radio_1x1. A gnuradio python script (rotate_zmq.py) generated by
grc is used to read the samples from gNB and apply a freq drift and send
them to a ZMQ sink. The UE reads samples from the ZMQ port on gnuradio.
The gnuradio script is modified to receive message containing freq
offsets from another script (send_freq_offset_zmq.py).

Dockerfile.gnuradio builds an ubuntu 24.04 image and copy both python
scripts.

Modified the docker-compose.yaml to include a new service gnuradio-zmq
that starts by running rotate_zmq.py.

Created container_5g_zmq_ocudu_trs.xml by copying
container_5g_zmq_ocudu_1x1.xml. Included a Custom_Command to run
send_freq_offset_zmq.py and another custom_command to grep the UE stdout
for TRS freq correction.

Added a PUSH socket in zmq_radio.cpp to send carrier frequency to
gnuradio and remove the offset applied so that the test can continue
without a freq offset.

Signed-off-by: Sakthivel Velumani <s.velumani@northeastern.edu>
This commit is contained in:
Sakthivel Velumani
2026-05-19 00:52:46 +00:00
parent bfe0b95df5
commit 12ef1ccf85
7 changed files with 684 additions and 1 deletions

View File

@@ -0,0 +1,123 @@
<!-- SPDX-License-Identifier: LicenseRef-CSSL-1.0 -->
<testCaseList>
<htmlTabRef>ocudu-5gnr-tdd-trs</htmlTabRef>
<htmlTabName>OCUDU gnb split8 with OAI UE</htmlTabName>
<htmlTabIcon>wrench</htmlTabIcon>
<testCase>
<class>Pull_Local_Registry</class>
<desc>Pull Images from Local Registry</desc>
<node>localhost</node>
<images>oai-gnb-asan oai-nr-ue-asan</images>
</testCase>
<testCase>
<class>Create_Workspace</class>
<desc>Create new Workspace</desc>
<node>localhost</node>
</testCase>
<testCase>
<class>Deploy_Object</class>
<desc>Deploy OAI 5G CoreNetwork</desc>
<node>localhost</node>
<yaml_path>ci-scripts/yaml_files/5g_zmq_radio_trs</yaml_path>
<services>mysql oai-amf oai-smf oai-upf oai-ext-dn</services>
</testCase>
<testCase>
<class>Deploy_Object</class>
<desc>Deploy OCUDU Split 8 gNB + GNURadio + OAI UE</desc>
<node>localhost</node>
<yaml_path>ci-scripts/yaml_files/5g_zmq_radio_trs</yaml_path>
<services>ocudu-gnb gnuradio-zmq oai-nr-ue</services>
</testCase>
<testCase>
<class>Attach_UE</class>
<desc>Attach OAI UE (Wait for IP)</desc>
<id>rfsim5g_ue</id>
<node>localhost</node>
</testCase>
<testCase>
<class>Custom_Command</class>
<desc>Simulate frequency drift of 50 Hz per second for 13 seconds</desc>
<node>localhost</node>
<command>docker exec gnuradio-zmq python3 /app/send_freq_offset_zmq.py --num-msg 13</command>
</testCase>
<testCase>
<class>Custom_Command</class>
<desc>Verify TRS frequency correction</desc>
<node>localhost</node>
<command>timeout 3 docker logs -f oai-nr-ue 2>&1 | grep -m1 'CFO estimated \([5-6][0-9][0-9]\) from TRS exceeded threshold'</command>
</testCase>
<testCase>
<class>Ping</class>
<desc>Ping ext-dn from NR-UE</desc>
<id>rfsim5g_ue</id>
<node>localhost</node>
<svr_id>rfsim5g_ext_dn</svr_id>
<svr_node>localhost</svr_node>
<ping_args>-c 20 -i 0.25</ping_args>
<ping_packetloss_threshold>5</ping_packetloss_threshold>
</testCase>
<testCase>
<class>Ping</class>
<desc>Ping NR-UE from ext-dn</desc>
<id>rfsim5g_ext_dn</id>
<node>localhost</node>
<svr_id>rfsim5g_ue</svr_id>
<svr_node>localhost</svr_node>
<ping_args>-c 20 -i 0.25</ping_args>
<ping_packetloss_threshold>5</ping_packetloss_threshold>
</testCase>
<testCase>
<class>Iperf</class>
<desc>Iperf (DL/3Mbps/UDP)(20 sec)</desc>
<iperf_args>-u -b 3M -t 20 -R</iperf_args>
<id>rfsim5g_ue</id>
<node>localhost</node>
<svr_id>rfsim5g_ext_dn</svr_id>
<svr_node>localhost</svr_node>
<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
<iperf_bitrate_threshold>90</iperf_bitrate_threshold>
</testCase>
<testCase>
<class>Iperf</class>
<desc>Iperf (UL/3Mbps/UDP)(20 sec)</desc>
<iperf_args>-u -b 3M -t 20</iperf_args>
<id>rfsim5g_ue</id>
<node>localhost</node>
<svr_id>rfsim5g_ext_dn</svr_id>
<svr_node>localhost</svr_node>
<iperf_packetloss_threshold>5</iperf_packetloss_threshold>
<iperf_bitrate_threshold>90</iperf_bitrate_threshold>
</testCase>
<testCase>
<class>Undeploy_Object</class>
<always_exec>true</always_exec>
<desc>Undeploy all OAI 5G stack</desc>
<node>localhost</node>
<yaml_path>ci-scripts/yaml_files/5g_zmq_radio_trs</yaml_path>
<analysis>
<services>oai-nr-ue</services>
</analysis>
</testCase>
<testCase>
<class>Clean_Test_Server_Images</class>
<always_exec>true</always_exec>
<desc>Clean Test Images on Test Server</desc>
<node>localhost</node>
<images>oai-gnb-asan oai-nr-ue-asan</images>
</testCase>
</testCaseList>

View File

@@ -0,0 +1,11 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
gnuradio \
python3-zmq \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY rotate_zmq.py send_freq_offset_zmq.py .

View File

@@ -0,0 +1,178 @@
# SPDX-License-Identifier: MIT
services:
mysql:
container_name: "rfsim5g-mysql"
image: mysql:9.6
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.2.1
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.2.1
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.2.1
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:latest
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
gnuradio-zmq:
build:
context: .
dockerfile: Dockerfile.gnuradio
cap_drop:
- ALL
cap_add:
- SYS_NICE
depends_on:
- ocudu-gnb
networks:
public_net:
ipv4_address: 192.168.71.141
command:
bash -c "cd /app/ & python3 rotate_zmq.py --zmq-req-src tcp://192.168.71.140:4558 --zmq-req-snk tcp://0.0.0.0:4559"
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.141:4559
depends_on:
- gnuradio-zmq
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"

View File

@@ -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

View File

@@ -0,0 +1,216 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# SPDX-License-Identifier: GPL-3.0
#
# GNU Radio Python Flow Graph
# Title: Simple doppler injector
# GNU Radio version: 3.10.9.2
import threading
import struct
import sys
import signal
from argparse import ArgumentParser
import zmq
from gnuradio import analog, blocks, gr
from gnuradio.filter import firdes
from gnuradio.fft import window
from gnuradio import zeromq
class untitled(gr.top_block):
def __init__(self, samp_rate=23040000, rx_freq=0, freq_off=0, zmq_req_src='tcp://127.0.0.1:4445', zmq_req_snk='tcp://127.0.0.1:4446', zmq_pull_addr='tcp://*:5555', zmq_pull_oai_addr='tcp://*5556'):
gr.top_block.__init__(self, "Simple doppler injector", catch_exceptions=True)
##################################################
# Variables
##################################################
self.samp_rate = samp_rate
self.freq_off = freq_off
self.rx_freq = rx_freq
##################################################
# Blocks
##################################################
self.zeromq_req_source_0 = zeromq.req_source(gr.sizeof_gr_complex, 1, zmq_req_src, 100, False, (-1), False)
self.zeromq_rep_sink_0 = zeromq.rep_sink(gr.sizeof_gr_complex, 1, zmq_req_snk, 100, False, (-1), True)
self.blocks_throttle2_0_0 = blocks.throttle(
gr.sizeof_gr_complex*1, samp_rate, True,
0 if "auto" == "auto" else max(
int(float(0.1) * samp_rate) if "auto" == "time" else int(0.1), 1)
)
self.blocks_multiply_xx_0 = blocks.multiply_vcc(1)
self.analog_sig_source_x_0_1_0 = analog.sig_source_c(samp_rate, analog.GR_COS_WAVE, freq_off, 1, 0, 0)
##################################################
# Connections
##################################################
self.connect((self.analog_sig_source_x_0_1_0, 0), (self.blocks_multiply_xx_0, 0))
self.connect((self.blocks_multiply_xx_0, 0), (self.zeromq_rep_sink_0, 0))
self.connect((self.blocks_throttle2_0_0, 0), (self.blocks_multiply_xx_0, 1))
self.connect((self.zeromq_req_source_0, 0), (self.blocks_throttle2_0_0, 0))
##################################################
# ZMQ sockets
##################################################
self._zmq_ctx = zmq.Context()
self._zmq_ctx_oai = zmq.Context()
# PULL - receive freq offset updates from Python script
self._pull_sock = self._zmq_ctx.socket(zmq.PULL)
self._pull_sock.setsockopt(zmq.LINGER, 0)
self._pull_sock.bind(zmq_pull_addr)
print(f"[zmq] PULL bound to {zmq_pull_addr}")
# PULL - receive carrier freq updates from OAI
self._pull_sock_oai = self._zmq_ctx_oai.socket(zmq.PULL)
self._pull_sock_oai.setsockopt(zmq.LINGER, 0)
self._pull_sock_oai.bind(zmq_pull_oai_addr)
print(f"[zmq] PULL bound to {zmq_pull_oai_addr}")
self._zmq_thread = threading.Thread(target=self._zmq_listener, daemon=True)
self._zmq_thread_oai = threading.Thread(target=self._zmq_listener_oai, daemon=True)
# ------------------------------------------------------------------
# Getters / setters
# ------------------------------------------------------------------
def get_samp_rate(self):
return self.samp_rate
def get_rx_freq(self):
return self.rx_freq
def set_rx_freq(self, rx_freq):
self.rx_freq = rx_freq
print(f"[rx_freq] -> {self.rx_freq}")
def set_samp_rate(self, samp_rate):
self.samp_rate = samp_rate
self.analog_sig_source_x_0_1_0.set_sampling_freq(self.samp_rate)
self.blocks_throttle2_0_0.set_sample_rate(self.samp_rate)
print(f"[samp_rate] -> {self.samp_rate}")
def get_freq_off(self):
return self.freq_off
def set_freq_off(self, freq_off):
self.freq_off = freq_off
self.analog_sig_source_x_0_1_0.set_frequency(self.freq_off)
print(f"[freq_off] -> {self.freq_off}")
# ------------------------------------------------------------------
# ZMQ PULL listener
# ------------------------------------------------------------------
def _zmq_listener(self):
while True:
try:
# Accept either a raw 8-byte double or a UTF-8 string
raw = self._pull_sock.recv()
if len(raw) == 8:
val = struct.unpack('d', raw)[0] # double from C/numpy sender
else:
val = float(raw.decode('utf-8').strip())
self.set_freq_off(self.freq_off + val)
except (ValueError, struct.error) as e:
print(f"[zmq] invalid message: {e}")
except zmq.ZMQError as e:
print(f"[zmq] error: {e}")
break
def _zmq_listener_oai(self):
while True:
try:
# Accept either a raw 8-byte double or a UTF-8 string
raw = self._pull_sock_oai.recv()
if len(raw) == 8:
val = struct.unpack('d', raw)[0] # double from C/numpy sender
else:
val = float(raw.decode('utf-8').strip())
self.set_rx_freq(val)
self.set_freq_off(0)
except (ValueError, struct.error) as e:
print(f"[zmq] invalid message: {e}")
except zmq.ZMQError as e:
print(f"[zmq] error: {e}")
break
def start(self):
self._zmq_thread.start()
self._zmq_thread_oai.start()
super().start()
def main(top_block_cls=untitled, options=None):
parser = ArgumentParser(description="Simple doppler injector")
parser.add_argument(
"--samp-rate", type=int, default=23040000,
help="Sample rate in Hz (default: 23040000)"
)
parser.add_argument(
"--rx-freq", type=int, default=3619200000,
help="Carrier freq in Hz (default: 3619200000)"
)
parser.add_argument(
"--freq-off", type=float, default=0.0,
help="Initial frequency offset in Hz (default: 0.0)"
)
parser.add_argument(
"--zmq-req-src", type=str, default="tcp://127.0.0.1:4445",
help="ZMQ REQ source address (default: tcp://127.0.0.1:4445)"
)
parser.add_argument(
"--zmq-req-snk", type=str, default="tcp://127.0.0.1:4446",
help="ZMQ REQ sink address (default: tcp://127.0.0.1:4446)"
)
parser.add_argument(
"--zmq-pull-addr", type=str, default="tcp://*:5555",
help="ZMQ PULL bind address for incoming freq_off updates (default: tcp://*:5555)"
)
parser.add_argument(
"--zmq-pull-oai-addr", type=str, default="tcp://*:5556",
help="ZMQ PULL bind address for incoming carrier_freq updates from OAI UE (default: tcp://*:5556)"
)
args = parser.parse_args()
tb = top_block_cls(
samp_rate=args.samp_rate,
rx_freq=args.rx_freq,
freq_off=args.freq_off,
zmq_req_src=args.zmq_req_src,
zmq_req_snk=args.zmq_req_snk,
zmq_pull_addr=args.zmq_pull_addr,
zmq_pull_oai_addr=args.zmq_pull_oai_addr,
)
def sig_handler(sig=None, frame=None):
tb.stop()
tb.wait()
sys.exit(0)
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGTERM, sig_handler)
tb.start()
print(f"Flowgraph running: samp_rate={args.samp_rate}, rx_freq={args.rx_freq}")
print(f"PULL (freq_off in): {args.zmq_pull_addr}")
print(f"PULL (carrier_freq in): {args.zmq_pull_oai_addr}")
try:
input('Press Enter to quit: ')
except EOFError:
pass
tb.stop()
tb.wait()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""
Send a freq offset to the GNURadio doppler injector every second via ZMQ PUSH.
Usage:
python3 send_freq_offset_zmq.py --carrier-freq 3489420000 --freq-off 50.0 --interval 0.5 --addr tcp://127.0.0.1:5555
"""
import sys
import time
import struct
import signal
import threading
import argparse
import zmq
def main():
parser = argparse.ArgumentParser(description="Send freq offset to GNURadio via ZMQ PUSH")
parser.add_argument(
"--freq-off", type=float, default=50.0,
help="Frequency offset in Hz to send (e.g. 50.0)"
)
parser.add_argument(
"--addr", type=str, default="tcp://127.0.0.1:5555",
help="GNURadio PULL address to send freq_off to (default: tcp://127.0.0.1:5555)"
)
parser.add_argument(
"--interval", type=float, default=1.0,
help="Send interval in seconds (default: 1.0)"
)
parser.add_argument(
"--num-msg", type=int, default=15,
help="Number of freq offsets to send (default: 15)"
)
args = parser.parse_args()
ctx = zmq.Context()
# PUSH - send freq_off to GNURadio PULL
push_sock = ctx.socket(zmq.PUSH)
push_sock.setsockopt(zmq.LINGER, 0)
push_sock.connect(args.addr)
print(f"[zmq] PUSH connected to {args.addr}")
def sig_handler(sig, frame):
print("\nStopping.")
push_sock.close()
ctx.term()
sys.exit(0)
signal.signal(signal.SIGINT, sig_handler)
signal.signal(signal.SIGTERM, sig_handler)
print(f"Sending freq_off = {args.freq_off} Hz every {args.interval}s - Ctrl+C to stop")
msg_count = 0
while msg_count < args.num_msg:
push_sock.send(struct.pack('d', args.freq_off))
print(f"[sent] freq_offset = {args.freq_off} Hz")
time.sleep(args.interval)
msg_count += 1
print("\nStopping.")
push_sock.close()
ctx.term()
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -44,11 +44,13 @@
#define ZMQ_SECTION "zmq"
#define ZMQ_TX_CHANNELS "tx_channels"
#define ZMQ_RX_CHANNELS "rx_channels"
#define ZMQ_CTRL_CHANNELS "ctrl_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), \
STRINGLISTPARAM(ZMQ_CTRL_CHANNELS, "list of zmq addresses represeting control channels_\n", PARAMFLAG_MANDATORY, nullptr, nullptr), \
};
const size_t sample_size = sizeof(cf_t);
@@ -56,6 +58,8 @@ const size_t rx_buffer_size = sample_size * 300000;
typedef struct {
void *context;
void *ctrl_context;
void *ctrl_socket;
zmq_tx_stream tx_stream;
zmq_rx_stream rx_stream;
std::thread poll_thread;
@@ -224,6 +228,10 @@ static void zmq_end(openair0_device_t *device)
if (s->context)
zmq_ctx_destroy(s->context);
if (s->ctrl_socket)
zmq_close(s->ctrl_socket);
if (s->ctrl_context)
zmq_ctx_destroy(s->ctrl_context);
delete s;
}
}
@@ -255,8 +263,18 @@ static int zmq_stop(openair0_device_t *device)
static int zmq_set_freq(openair0_device_t *device, openair0_config_t *openair0_cfg)
{
return 0;
/* Send raw 8-byte little-endian double - matches struct.unpack('d') in Python */
double freq = openair0_cfg[0].rx_freq[0];
zmq_state_t *s = static_cast<zmq_state_t *>(device->priv);
int rc = zmq_send(s->ctrl_socket, &freq, sizeof(double), 0);
if (rc != sizeof(double)) {
LOG_E(HW, "[ZMQ] send failed: %s\n", zmq_strerror(zmq_errno()));
} else {
LOG_I(HW, "[ZMQ] sent rx freq = %.4f Hz\n", freq);
}
return (rc == sizeof(double)) ? 0 : -1;
}
static int zmq_set_gains(openair0_device_t *device, openair0_config_t *openair0_cfg)
{
return 0;
@@ -325,6 +343,21 @@ extern "C" __attribute__((__visibility__("default"))) int device_init(openair0_d
zmq_state->rx_stream.tx_stream_ = &zmq_state->tx_stream;
}
// Setup control channel
int num_ctrl_channels = gpd(param_desc, sizeofArray(param_desc), ZMQ_CTRL_CHANNELS)->numelt;
if (num_ctrl_channels > 0) {
AssertFatal(num_ctrl_channels == 1, "Only one ZMQ control channel is supported at the moment.\n");
char **ctrl_channels = gpd(param_desc, sizeofArray(param_desc), ZMQ_CTRL_CHANNELS)->strlistptr;
zmq_state->ctrl_context = zmq_ctx_new();
zmq_state->ctrl_socket = zmq_socket(zmq_state->ctrl_context, ZMQ_PUSH);
AssertFatal(zmq_state->ctrl_socket != NULL, "zmq_socket(ZMQ_PUSH) for control channel failed");
int linger = 0;
zmq_setsockopt(zmq_state->ctrl_socket, ZMQ_LINGER, &linger, sizeof(linger));
AssertFatal(zmq_connect(zmq_state->ctrl_socket, ctrl_channels[0]) == 0,
"zmq_bind for control channel failed on %s",
ctrl_channels[0]);
}
device->trx_start_func = zmq_start;
device->trx_get_stats_func = zmq_get_stats;
device->trx_reset_stats_func = zmq_reset_stats;