Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8178cede9 | ||
|
|
e1f315bee9 | ||
|
|
df26524d80 | ||
|
|
c5a3ef8b60 | ||
|
|
766d0a4bd3 | ||
|
|
d7d67277f3 | ||
|
|
da224f6ba1 | ||
|
|
7cd72323af | ||
|
|
bfb177c38e | ||
|
|
434cd8ca3d | ||
|
|
38167e50f8 | ||
|
|
d1129d1f90 | ||
|
|
6a6f200790 | ||
|
|
1c26f372aa | ||
|
|
0da440017e | ||
|
|
e0a2b4837c | ||
|
|
11dc86f59c | ||
|
|
547f683ebe |
@@ -919,6 +919,7 @@ set(PHY_SRC_RU
|
|||||||
${OPENAIR1_DIR}/PHY/MODULATION/gen_75KHz.cpp
|
${OPENAIR1_DIR}/PHY/MODULATION/gen_75KHz.cpp
|
||||||
${OPENAIR1_DIR}/PHY/MODULATION/beamforming.c
|
${OPENAIR1_DIR}/PHY/MODULATION/beamforming.c
|
||||||
${OPENAIR1_DIR}/PHY/MODULATION/compute_bf_weights.c
|
${OPENAIR1_DIR}/PHY/MODULATION/compute_bf_weights.c
|
||||||
|
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_antenna_state.c
|
||||||
${OPENAIR1_DIR}/PHY/INIT/lte_init_ru.c
|
${OPENAIR1_DIR}/PHY/INIT/lte_init_ru.c
|
||||||
${OPENAIR1_DIR}/PHY/LTE_TRANSPORT/prach.c
|
${OPENAIR1_DIR}/PHY/LTE_TRANSPORT/prach.c
|
||||||
)
|
)
|
||||||
@@ -998,6 +999,7 @@ set(PHY_SRC_UE
|
|||||||
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_prach.c
|
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_prach.c
|
||||||
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_ulsch_llr_computation.c
|
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_ulsch_llr_computation.c
|
||||||
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c
|
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_ulsch_demodulation.c
|
||||||
|
${OPENAIR1_DIR}/PHY/NR_TRANSPORT/nr_antenna_control.c
|
||||||
${OPENAIR1_DIR}/PHY/NR_REFSIG/ul_ref_seq_nr.c
|
${OPENAIR1_DIR}/PHY/NR_REFSIG/ul_ref_seq_nr.c
|
||||||
${OPENAIR1_DIR}/PHY/NR_REFSIG/nr_dmrs_rx.c
|
${OPENAIR1_DIR}/PHY/NR_REFSIG/nr_dmrs_rx.c
|
||||||
${OPENAIR1_DIR}/PHY/NR_REFSIG/nr_gen_mod_table.c
|
${OPENAIR1_DIR}/PHY/NR_REFSIG/nr_gen_mod_table.c
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ oc-cn5g:
|
|||||||
|
|
||||||
oc-cn5g-20897:
|
oc-cn5g-20897:
|
||||||
Host: cacofonix
|
Host: cacofonix
|
||||||
NetworkScript: echo "inet 172.21.6.105"
|
NetworkScript: echo "inet 172.21.6.115"
|
||||||
RunIperf3Server: False
|
RunIperf3Server: False
|
||||||
Deploy: "! scripts/oc-cn5g-deploy.sh /opt/oai-cn5g-fed-develop-2025-jan oaicicd-core-for-fhi72"
|
Deploy: "! scripts/oc-cn5g-deploy.sh /opt/oai-cn5g-fed-develop-2026-apr oaicicd-core-for-fhi72"
|
||||||
Undeploy: "! scripts/oc-cn5g-undeploy.sh /opt/oai-cn5g-fed-develop-2025-jan oaicicd-core-for-fhi72"
|
Undeploy: "! scripts/oc-cn5g-undeploy.sh /opt/oai-cn5g-fed-develop-2026-apr oaicicd-core-for-fhi72"
|
||||||
LogCollect: "! scripts/oc-cn5g-logcollect.sh /opt/oai-cn5g-fed-develop-2025-jan oaicicd-core-for-fhi72 %%log_dir%%"
|
LogCollect: "! scripts/oc-cn5g-logcollect.sh /opt/oai-cn5g-fed-develop-2026-apr oaicicd-core-for-fhi72 %%log_dir%%"
|
||||||
|
|
||||||
oc-cn5g-00103-ho:
|
oc-cn5g-00103-ho:
|
||||||
Host: groot
|
Host: groot
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ L1s =
|
|||||||
(
|
(
|
||||||
{
|
{
|
||||||
num_cc = 1;
|
num_cc = 1;
|
||||||
|
prach_dtx_threshold = 200;
|
||||||
tr_n_preference = "local_mac";
|
tr_n_preference = "local_mac";
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -188,8 +188,8 @@ MACRLCs = (
|
|||||||
tr_n_preference = "local_RRC";
|
tr_n_preference = "local_RRC";
|
||||||
pucch_RSSI_Threshold = -270;
|
pucch_RSSI_Threshold = -270;
|
||||||
pusch_RSSI_Threshold = -260;
|
pusch_RSSI_Threshold = -260;
|
||||||
pusch_TargetSNRx10 = 260; # 150;
|
pusch_TargetSNRx10 = 290;
|
||||||
pucch_TargetSNRx10 = 200; #200;
|
pucch_TargetSNRx10 = 200;
|
||||||
dl_max_mcs = 28;
|
dl_max_mcs = 28;
|
||||||
ul_max_mcs = 28;
|
ul_max_mcs = 28;
|
||||||
ul_min_mcs = 9;
|
ul_min_mcs = 9;
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ gNBs =
|
|||||||
|
|
||||||
|
|
||||||
////////// AMF parameters:
|
////////// AMF parameters:
|
||||||
amf_ip_address = ({ ipv4 = "172.21.6.103"; });
|
amf_ip_address = ({ ipv4 = "172.21.6.113"; });
|
||||||
|
|
||||||
NETWORK_INTERFACES :
|
NETWORK_INTERFACES :
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ RUs = (
|
|||||||
nb_tx = 2;
|
nb_tx = 2;
|
||||||
nb_rx = 2;
|
nb_rx = 2;
|
||||||
att_tx = 0;
|
att_tx = 0;
|
||||||
att_rx = 0;
|
att_rx = 6;
|
||||||
bands = [77];
|
bands = [77];
|
||||||
max_pdschReferenceSignalPower = -27;
|
max_pdschReferenceSignalPower = -27;
|
||||||
max_rxgain = 71;
|
max_rxgain = 71;
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ gNBs =
|
|||||||
nr_cellid = 12345678L;
|
nr_cellid = 12345678L;
|
||||||
|
|
||||||
////////// Physical parameters:
|
////////// Physical parameters:
|
||||||
|
use_deltaMCS = 1;
|
||||||
|
|
||||||
servingCellConfigCommon = (
|
servingCellConfigCommon = (
|
||||||
{
|
{
|
||||||
@@ -180,7 +180,7 @@ MACRLCs = (
|
|||||||
{
|
{
|
||||||
tr_s_preference = "local_L1";
|
tr_s_preference = "local_L1";
|
||||||
tr_n_preference = "local_RRC";
|
tr_n_preference = "local_RRC";
|
||||||
pusch_TargetSNRx10 = 200;
|
pusch_TargetSNRx10 = 100;
|
||||||
pucch_TargetSNRx10 = 200;
|
pucch_TargetSNRx10 = 200;
|
||||||
ul_prbblack_SNR_threshold = 10;
|
ul_prbblack_SNR_threshold = 10;
|
||||||
}
|
}
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RU_IP="10.10.0.111"
|
||||||
|
RU_HOST="root@10.10.0.111"
|
||||||
|
|
||||||
|
ssh_exec() {
|
||||||
|
ssh $RU_HOST $@ </dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "→ Checking for PTP fluctuations in the last 60 minutes..."
|
||||||
|
|
||||||
|
logs=$(journalctl -u ptp4l.service --since "60 minutes ago")
|
||||||
|
|
||||||
|
check_ptp=$(echo "$logs" | awk '
|
||||||
|
/rms/ {
|
||||||
|
rms=$8
|
||||||
|
if (rms > 100) {
|
||||||
|
print "✗ PTP FLUCTUATION DETECTED → " $0
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
')
|
||||||
|
|
||||||
|
if [ -n "$check_ptp" ]; then
|
||||||
|
echo "$check_ptp"
|
||||||
|
else
|
||||||
|
echo "✓ No PTP fluctuations detected."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ Checking if VVDN LPRU is PTP synchronized (timeout 1 min)..."
|
||||||
|
if ssh_exec 'timeout 1m sh -c "tail -F /var/log/synctimingptp2.log | grep -m 1 -F ,\ synchronized"'; then
|
||||||
|
echo "✓ VVDN LPRU synchronized"
|
||||||
|
else
|
||||||
|
echo "✗ VVDN LPRU not synchronized"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "→ Checking TX/RX carrier configuration..."
|
||||||
|
if ssh_exec 'sysrepocfg -X -d running -f xml | grep -q "<active>INACTIVE</active>"'; then
|
||||||
|
echo "→ Some carriers are not active, activating via sysrepocfg..."
|
||||||
|
ssh_exec 'sysrepocfg -X -d running -f xml | sed "s/<active>INACTIVE<\/active>/<active>ACTIVE<\/active>/g" | sysrepocfg -I -d running -f xml'
|
||||||
|
echo "✓ All TX/RX carriers activated."
|
||||||
|
else
|
||||||
|
echo "✓ All TX/RX carriers already activated."
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RU_HOST="root@10.10.0.111"
|
||||||
|
|
||||||
|
ssh_exec() {
|
||||||
|
ssh $RU_HOST $@ </dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "→ Inactivating TX/RX carriers on VVDN LPRU..."
|
||||||
|
ssh_exec 'sysrepocfg -X -d running -f xml | sed "s/ACTIVE/INACTIVE/g" | sysrepocfg -I -d running -f xml'
|
||||||
|
echo "✓ TX/RX carriers inactivated."
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
<node>gracehopper1-oai</node>
|
<node>gracehopper1-oai</node>
|
||||||
<yaml_path>ci-scripts/yaml_files/sa_gnb_aerial</yaml_path>
|
<yaml_path>ci-scripts/yaml_files/sa_gnb_aerial</yaml_path>
|
||||||
<analysis>
|
<analysis>
|
||||||
<services>oai-gnb-aerial=RetxCheck=20,100,100,100 oai-gnb-aerial=EndsWithBye</services>
|
<services>oai-gnb-aerial=EndsWithBye</services>
|
||||||
</analysis>
|
</analysis>
|
||||||
</testCase>
|
</testCase>
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
<node>gracehopper1-oai</node>
|
<node>gracehopper1-oai</node>
|
||||||
<yaml_path>ci-scripts/yaml_files/sa_gnb_aerial_ul</yaml_path>
|
<yaml_path>ci-scripts/yaml_files/sa_gnb_aerial_ul</yaml_path>
|
||||||
<analysis>
|
<analysis>
|
||||||
<services>oai-gnb-aerial=RetxCheck=20,100,100,100 oai-gnb-aerial=EndsWithBye</services>
|
<services>oai-gnb-aerial=EndsWithBye</services>
|
||||||
</analysis>
|
</analysis>
|
||||||
</testCase>
|
</testCase>
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,13 @@
|
|||||||
<script>yaml_files/sa_fhi_7.2_vvdn_gnb/setup_config.sh</script>
|
<script>yaml_files/sa_fhi_7.2_vvdn_gnb/setup_config.sh</script>
|
||||||
</testCase>
|
</testCase>
|
||||||
|
|
||||||
|
<testCase>
|
||||||
|
<class>Custom_Script</class>
|
||||||
|
<desc>Activate carriers on VVDN RU</desc>
|
||||||
|
<node>cacofonix</node>
|
||||||
|
<script>scripts/vvdn-activate-carriers.sh</script>
|
||||||
|
</testCase>
|
||||||
|
|
||||||
<testCase>
|
<testCase>
|
||||||
<class>Pull_Local_Registry</class>
|
<class>Pull_Local_Registry</class>
|
||||||
<desc>Pull Images from Local Registry</desc>
|
<desc>Pull Images from Local Registry</desc>
|
||||||
@@ -134,9 +141,9 @@
|
|||||||
<testCase>
|
<testCase>
|
||||||
<class>Custom_Script</class>
|
<class>Custom_Script</class>
|
||||||
<always_exec>true</always_exec>
|
<always_exec>true</always_exec>
|
||||||
<desc>Set CPU to idle state, set kernel parameters to default values</desc>
|
<desc>Inactivate carriers on VVDN RU</desc>
|
||||||
<node>cacofonix</node>
|
<node>cacofonix</node>
|
||||||
<script>yaml_files/sa_fhi_7.2_vvdn_gnb/setup_cleanup.sh</script>
|
<script>scripts/vvdn-inactivate-carriers.sh</script>
|
||||||
</testCase>
|
</testCase>
|
||||||
|
|
||||||
</testCaseList>
|
</testCaseList>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<node>matix</node>
|
<node>matix</node>
|
||||||
<yaml_path>ci-scripts/yaml_files/5g_sa_n310_2x2_100MHz</yaml_path>
|
<yaml_path>ci-scripts/yaml_files/5g_sa_n310_2x2_100MHz</yaml_path>
|
||||||
<analysis>
|
<analysis>
|
||||||
<services>oai-gnb=RetxCheck=dl=10,100,100,100;ul=25,100,100,100 oai-gnb=EndsWithBye</services>
|
<services>oai-gnb=EndsWithBye</services>
|
||||||
</analysis>
|
</analysis>
|
||||||
</testCase>
|
</testCase>
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<node>matix</node>
|
<node>matix</node>
|
||||||
<yaml_path>ci-scripts/yaml_files/5g_sa_n310_2x2_60MHz</yaml_path>
|
<yaml_path>ci-scripts/yaml_files/5g_sa_n310_2x2_60MHz</yaml_path>
|
||||||
<analysis>
|
<analysis>
|
||||||
<services>oai-gnb=RetxCheck=dl=15,100,100,100;ul=20,100,100,100 oai-gnb=EndsWithBye</services>
|
<services>oai-gnb=EndsWithBye</services>
|
||||||
</analysis>
|
</analysis>
|
||||||
</testCase>
|
</testCase>
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<node>matix</node>
|
<node>matix</node>
|
||||||
<yaml_path>ci-scripts/yaml_files/5g_sa_n310_4x4_60MHz</yaml_path>
|
<yaml_path>ci-scripts/yaml_files/5g_sa_n310_4x4_60MHz</yaml_path>
|
||||||
<analysis>
|
<analysis>
|
||||||
<services>oai-vnf=RetxCheck=10,100,100,100 oai-vnf=EndsWithBye oai-pnf=EndsWithBye</services>
|
<services>oai-vnf=EndsWithBye oai-pnf=EndsWithBye</services>
|
||||||
</analysis>
|
</analysis>
|
||||||
</testCase>
|
</testCase>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
set -e
|
|
||||||
sudo cpupower idle-set -E > /dev/null
|
|
||||||
sudo sysctl kernel.sched_rt_runtime_us=950000
|
|
||||||
sudo sysctl kernel.timer_migration=1
|
|
||||||
exit 0
|
|
||||||
@@ -17,7 +17,7 @@ services:
|
|||||||
devices:
|
devices:
|
||||||
- /dev/bus/usb/:/dev/bus/usb/
|
- /dev/bus/usb/:/dev/bus/usb/
|
||||||
volumes:
|
volumes:
|
||||||
- ../../conf_files/gnb.sa.band78.106prb.usrpb200.sc-fdma.conf:/opt/oai-gnb/etc/gnb.conf
|
- ../../conf_files/gnb.sa.band78.106prb.usrpb200.sc-fdma-deltaMCS.conf:/opt/oai-gnb/etc/gnb.conf
|
||||||
# for performance reasons, we use host mode: in bridge mode, we have
|
# for performance reasons, we use host mode: in bridge mode, we have
|
||||||
# unacceptable DL TCP performance. However, the whole point of
|
# unacceptable DL TCP performance. However, the whole point of
|
||||||
# containerization is to not be in host mode, so update this to macvlan
|
# containerization is to not be in host mode, so update this to macvlan
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
FROM ran-base:latest AS ran-build
|
||||||
|
ARG BUILD_OPTION
|
||||||
|
|
||||||
|
RUN rm -Rf /oai-ran
|
||||||
|
WORKDIR /oai-ran
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# build all targets so final targets can be created afterwards
|
||||||
|
RUN /bin/sh oaienv && \
|
||||||
|
cd cmake_targets && \
|
||||||
|
./build_oai --ninja \
|
||||||
|
--gNB --RU --nrUE -w SIMU -w USRP --build-lib telnetsrv \
|
||||||
|
--build-e2 --cmake-opt -DKPM_VERSION=KPM_V3_00 --cmake-opt -DE2AP_VERSION=E2AP_V3 \
|
||||||
|
--cmake-opt -DCMAKE_C_FLAGS="-Werror" --cmake-opt -DCMAKE_CXX_FLAGS="-Werror" $BUILD_OPTION && \
|
||||||
|
echo "---- ldd on executables ----" && \
|
||||||
|
ldd ran_build/build/*softmodem* ran_build/build/nr-cuup ran_build/build/oairu && \
|
||||||
|
echo "---- ldd on shared libraries ----" && \
|
||||||
|
ldd ran_build/build/*.so
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
apt-get update; \
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates curl wget iproute2 iputils-ping dnsutils jq \
|
||||||
|
iperf iperf3 ffmpeg gdb\
|
||||||
|
nghttp2-client || true; \
|
||||||
|
if ! command -v h2load >/dev/null 2>&1; then \
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends nghttp2 || true; \
|
||||||
|
fi; \
|
||||||
|
wget -O /usr/local/bin/sipp https://github.com/SIPp/sipp/releases/download/v3.7.7/sipp && chmod +x /usr/local/bin/sipp; \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# FlexRIC libraries (built by --build-e2) - only copy service model libraries (*_sm.so)
|
||||||
|
# Also rebuild xapp_nakaolab (not built by default in build_oai)
|
||||||
|
RUN mkdir -p /usr/local/etc/flexric /usr/local/lib/flexric && \
|
||||||
|
find /oai-ran/openair2/E2AP/flexric/build -name "*_sm.so" -exec cp {} /usr/local/lib/flexric/ \; && \
|
||||||
|
cp /oai-ran/openair2/E2AP/flexric/flexric.conf /usr/local/etc/flexric/ 2>/dev/null || true && \
|
||||||
|
echo "/usr/local/lib/flexric" > /etc/ld.so.conf.d/flexric.conf && ldconfig && \
|
||||||
|
cd /oai-ran/openair2/E2AP/flexric/build && \
|
||||||
|
sed -i 's|/home/[^/]*/openairinterface5g|/oai-ran|g' CMakeCache.txt && \
|
||||||
|
find . -name 'Makefile' -o -name '*.cmake' | xargs sed -i 's|/home/[^/]*/openairinterface5g|/oai-ran|g' 2>/dev/null || true && \
|
||||||
|
rm -f examples/xApp/c/monitor/CMakeFiles/xapp_nakaolab.dir/xapp_nakaolab.c.o && \
|
||||||
|
cmake --build . --target xapp_nakaolab 2>&1 | tail -5 && \
|
||||||
|
ls -la /usr/local/lib/flexric/
|
||||||
|
|
||||||
|
# Install netcat for antenna control testing + paramiko for M-Plane NETCONF server
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
netcat-openbsd python3-pip \
|
||||||
|
&& pip3 install --break-system-packages paramiko \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# O-RU M-Plane NETCONF server (o-ran-uplane-conf antenna mask control)
|
||||||
|
# Listens on port 831, translates NETCONF edit-config to internal antenna control
|
||||||
|
COPY mplane/mplane_server.py /opt/mplane/mplane_server.py
|
||||||
|
COPY mplane/wg4-capability.json /opt/mplane/wg4-capability.json
|
||||||
|
COPY mplane/gnb-mplane-entrypoint.sh /opt/mplane/gnb-mplane-entrypoint.sh
|
||||||
|
RUN chmod +x /opt/mplane/gnb-mplane-entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 831
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/bash"]
|
||||||
@@ -24,6 +24,8 @@
|
|||||||
#include "NR_PHY_INTERFACE/NR_IF_Module.h"
|
#include "NR_PHY_INTERFACE/NR_IF_Module.h"
|
||||||
#include "PHY/INIT/nr_phy_init.h"
|
#include "PHY/INIT/nr_phy_init.h"
|
||||||
#include "PHY/MODULATION/nr_modulation.h"
|
#include "PHY/MODULATION/nr_modulation.h"
|
||||||
|
#include "openair2/LAYER2/NR_MAC_gNB/nr_mac_gNB.h"
|
||||||
|
#include "PHY/NR_TRANSPORT/nr_antenna_control.h"
|
||||||
#include "PHY/NR_TRANSPORT/nr_transport_proto.h"
|
#include "PHY/NR_TRANSPORT/nr_transport_proto.h"
|
||||||
#include "PHY/TOOLS/tools_defs.h"
|
#include "PHY/TOOLS/tools_defs.h"
|
||||||
#include "PHY/defs_RU.h"
|
#include "PHY/defs_RU.h"
|
||||||
@@ -372,6 +374,14 @@ void init_eNB_afterRU(void)
|
|||||||
*/
|
*/
|
||||||
//init_precoding_weights(RC.gNB[inst]);
|
//init_precoding_weights(RC.gNB[inst]);
|
||||||
init_gNB_Tpool(inst);
|
init_gNB_Tpool(inst);
|
||||||
|
|
||||||
|
// antenna control
|
||||||
|
if (nr_antenna_control_start_test_server(&gNB->antenna_state, 9999))
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] Test control server started on port 9999\n");
|
||||||
|
if (inst < RC.nb_nr_macrlc_inst && RC.nrmac && RC.nrmac[inst]) {
|
||||||
|
RC.nrmac[inst]->antenna_state = &gNB->antenna_state;
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Connected MAC scheduler to PHY antenna state for dynamic layer adaptation\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# gNB entrypoint with M-Plane NETCONF server
|
||||||
|
#
|
||||||
|
# Starts the M-Plane server alongside the gNB process.
|
||||||
|
# The M-Plane server acts as an O-RU NETCONF endpoint,
|
||||||
|
# translating o-ran-uplane-conf edit-config to internal antenna control.
|
||||||
|
#
|
||||||
|
# Usage: gnb-mplane-entrypoint.sh <gnb-command-args...>
|
||||||
|
# Example: gnb-mplane-entrypoint.sh /oai-ran/.../nr-softmodem -O /opt/.../gnb.conf --rfsim
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[gnb-mplane] Starting M-Plane NETCONF server (port ${MPLANE_PORT:-831})..."
|
||||||
|
python3 /opt/mplane/mplane_server.py &
|
||||||
|
MPLANE_PID=$!
|
||||||
|
|
||||||
|
# Give the M-Plane server a moment to start
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if ! kill -0 $MPLANE_PID 2>/dev/null; then
|
||||||
|
echo "[gnb-mplane] WARNING: M-Plane server failed to start, continuing without it"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the gNB command (passed as arguments)
|
||||||
|
echo "[gnb-mplane] Starting gNB: $@"
|
||||||
|
exec "$@"
|
||||||
@@ -0,0 +1,602 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
O-RU M-Plane NETCONF Server for gNB Antenna Control
|
||||||
|
|
||||||
|
Lightweight NETCONF/SSH server implementing the O-RAN WG4 M-Plane interface
|
||||||
|
for antenna mask configuration (o-ran-uplane-conf.yang).
|
||||||
|
|
||||||
|
The gNB acts as an O-RU for antenna control purposes:
|
||||||
|
- Advertises mplane-supported-trx-control-masks (capability report)
|
||||||
|
- Accepts tx-array-antenna-mask-config via NETCONF edit-config
|
||||||
|
- Translates received mask to internal antenna control (localhost:9999)
|
||||||
|
- Sends mplane-trx-control-ant-mask-update notification (logged)
|
||||||
|
|
||||||
|
Protocol:
|
||||||
|
SSH transport (paramiko) -> NETCONF base:1.0 framing -> XML RPC processing
|
||||||
|
|
||||||
|
Architecture:
|
||||||
|
o1-adapter (O-DU) --NETCONF:831--> mplane_server.py (O-RU) --localhost:9999--> nr_antenna_control.c
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import signal
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Configuration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
MPLANE_PORT = 831
|
||||||
|
MPLANE_USER = MPLANE_PASS = "netconf"
|
||||||
|
GNB_CTRL_HOST = "127.0.0.1"
|
||||||
|
GNB_CTRL_PORT = 9999
|
||||||
|
WG4_CAPABILITY_FILE = "/opt/mplane/wg4-capability.json"
|
||||||
|
SSH_HOST_KEY = "/opt/mplane/ssh_host_rsa_key"
|
||||||
|
|
||||||
|
NS_NC = "urn:ietf:params:xml:ns:netconf:base:1.0"
|
||||||
|
NS_UPLANE = "urn:o-ran:uplane-conf:1.0"
|
||||||
|
NETCONF_END = "]]>]]>" # https://www.rfc-editor.org/rfc/rfc4742 NETCONF base:1.0 message delimiter
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
stream=sys.stdout,
|
||||||
|
)
|
||||||
|
log = logging.getLogger("mplane-server")
|
||||||
|
|
||||||
|
# WG4 Capability (O-RU supported masks)
|
||||||
|
def b64_to_mask(b64_str):
|
||||||
|
return int.from_bytes(base64.b64decode(b64_str), byteorder="big")
|
||||||
|
|
||||||
|
def mask_to_b64(mask_int):
|
||||||
|
byte_len = max(1, (mask_int.bit_length() + 7) // 8)
|
||||||
|
return base64.b64encode(mask_int.to_bytes(byte_len, byteorder="big")).decode()
|
||||||
|
|
||||||
|
|
||||||
|
class Capability:
|
||||||
|
"""O-RU capability report: tx-arrays with supported masks."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.tx_arrays = {} # name -> {"masks": [{index, b64, int}], ...}
|
||||||
|
self.current_config = {} # array-name -> {"index": i, "mask_int": m}
|
||||||
|
|
||||||
|
def load(self, path):
|
||||||
|
if not (path and os.path.exists(path)):
|
||||||
|
# Fallback: generate for 4 antennas
|
||||||
|
masks = []
|
||||||
|
for n in range(4, 0, -1):
|
||||||
|
m = (1 << n) - 1
|
||||||
|
masks.append({"index": 4 - n, "b64": mask_to_b64(m), "int": m})
|
||||||
|
self.tx_arrays["tx-array-0"] = {"masks": masks, "columns": 4, "layers": 4}
|
||||||
|
self.current_config["tx-array-0"] = {"index": 0, "mask_int": 0x0F}
|
||||||
|
log.info("Generated fallback capability (4 antennas)")
|
||||||
|
return
|
||||||
|
|
||||||
|
with open(path) as f:
|
||||||
|
data = json.load(f) # returns a dict
|
||||||
|
for arr in data.get("tx-arrays", []): # blank list for fallback
|
||||||
|
name = arr["name"]
|
||||||
|
cap = arr.get("mplane-trx-control-txarr-capability-info", {})
|
||||||
|
masks = []
|
||||||
|
for m in cap.get("mplane-supported-trx-control-masks", []):
|
||||||
|
masks.append({
|
||||||
|
"index": m["index"],
|
||||||
|
"b64": m["antenna-mask"],
|
||||||
|
"int": b64_to_mask(m["antenna-mask"]),
|
||||||
|
})
|
||||||
|
self.tx_arrays[name] = {
|
||||||
|
"masks": masks,
|
||||||
|
"columns": arr.get("number-of-columns", 4),
|
||||||
|
"layers": arr.get("number-of-array-layers", 4),
|
||||||
|
}
|
||||||
|
# Default: all active (index 0)
|
||||||
|
if masks:
|
||||||
|
self.current_config[name] = {
|
||||||
|
"index": masks[0]["index"],
|
||||||
|
"mask_int": masks[0]["int"],
|
||||||
|
}
|
||||||
|
log.info("Loaded capability: %d tx-arrays from %s", len(self.tx_arrays), path)
|
||||||
|
|
||||||
|
def resolve_index(self, array_name, index):
|
||||||
|
arr = self.tx_arrays.get(array_name)
|
||||||
|
if not arr:
|
||||||
|
return None, f"unknown tx-array '{array_name}'"
|
||||||
|
for m in arr["masks"]:
|
||||||
|
if m["index"] == index:
|
||||||
|
return m, ""
|
||||||
|
return None, f"index {index} not in supported masks"
|
||||||
|
|
||||||
|
def resolve_mask(self, array_name, mask_int):
|
||||||
|
arr = self.tx_arrays.get(array_name)
|
||||||
|
if not arr:
|
||||||
|
return None, f"unknown tx-array '{array_name}'"
|
||||||
|
for m in arr["masks"]:
|
||||||
|
if m["int"] == mask_int:
|
||||||
|
return m, ""
|
||||||
|
return None, f"mask 0x{mask_int:02X} not in supported masks"
|
||||||
|
|
||||||
|
def get_capability_xml(self):
|
||||||
|
"""Generate o-ran-uplane-conf operational data XML for <get> response."""
|
||||||
|
parts = [f'<user-plane-configuration xmlns="{NS_UPLANE}">']
|
||||||
|
for name, arr in self.tx_arrays.items():
|
||||||
|
parts.append(f"<tx-arrays><name>{name}</name>")
|
||||||
|
parts.append(f"<number-of-columns>{arr['columns']}</number-of-columns>")
|
||||||
|
parts.append(f"<number-of-array-layers>{arr['layers']}</number-of-array-layers>")
|
||||||
|
parts.append("<mplane-trx-control-txarr-capability-info>")
|
||||||
|
for m in arr["masks"]:
|
||||||
|
parts.append("<mplane-supported-trx-control-masks>")
|
||||||
|
parts.append(f"<index>{m['index']}</index>")
|
||||||
|
parts.append(f"<antenna-mask>{m['b64']}</antenna-mask>")
|
||||||
|
parts.append("</mplane-supported-trx-control-masks>")
|
||||||
|
parts.append("</mplane-trx-control-txarr-capability-info>")
|
||||||
|
parts.append("</tx-arrays>")
|
||||||
|
# Current config
|
||||||
|
for aname, cfg in self.current_config.items():
|
||||||
|
parts.append("<tx-array-antenna-mask-config>")
|
||||||
|
parts.append(f"<array-name>{aname}</array-name>")
|
||||||
|
parts.append(f"<antenna-bitmask-index>{cfg['index']}</antenna-bitmask-index>")
|
||||||
|
parts.append("</tx-array-antenna-mask-config>")
|
||||||
|
parts.append("</user-plane-configuration>")
|
||||||
|
return "".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# gNB internal antenna control client (localhost:9999)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class GnbCtrlClient:
|
||||||
|
"""Connects to the gNB internal test socket to apply antenna masks."""
|
||||||
|
|
||||||
|
def __init__(self, host, port):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
for attempt in range(30):
|
||||||
|
try:
|
||||||
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.sock.settimeout(5.0)
|
||||||
|
self.sock.connect((self.host, self.port))
|
||||||
|
self._recv() # consume welcome
|
||||||
|
log.info("Connected to gNB control at %s:%d", self.host, self.port)
|
||||||
|
return True
|
||||||
|
except (socket.error, ConnectionRefusedError) as e:
|
||||||
|
log.debug("gNB ctrl connect attempt %d/30: %s", attempt + 1, e)
|
||||||
|
if self.sock:
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
time.sleep(2)
|
||||||
|
log.warning("Cannot connect to gNB control at %s:%d", self.host, self.port)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def send_command(self, cmd):
|
||||||
|
if not self.sock:
|
||||||
|
if not self.connect():
|
||||||
|
return "ERROR: not connected"
|
||||||
|
try:
|
||||||
|
self.sock.sendall((cmd + "\n").encode())
|
||||||
|
time.sleep(0.1)
|
||||||
|
return self._recv()
|
||||||
|
except (socket.error, BrokenPipeError):
|
||||||
|
log.warning("gNB ctrl connection lost, reconnecting...")
|
||||||
|
self.sock = None
|
||||||
|
if not self.connect():
|
||||||
|
return "ERROR: reconnect failed"
|
||||||
|
self.sock.sendall((cmd + "\n").encode())
|
||||||
|
time.sleep(0.1)
|
||||||
|
return self._recv()
|
||||||
|
|
||||||
|
def _recv(self):
|
||||||
|
self.sock.settimeout(0.5)
|
||||||
|
data = b""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
chunk = self.sock.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
data += chunk
|
||||||
|
except socket.timeout:
|
||||||
|
pass
|
||||||
|
return data.decode("utf-8", errors="ignore")
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.sock:
|
||||||
|
try:
|
||||||
|
self.sock.sendall(b"QUIT\n")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.sock.close()
|
||||||
|
self.sock = None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# NETCONF Session Handler
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class NetconfSession:
|
||||||
|
"""Handle a single NETCONF session on an SSH channel."""
|
||||||
|
|
||||||
|
def __init__(self, channel, capability, gnb_ctrl):
|
||||||
|
self.channel = channel
|
||||||
|
self.cap = capability
|
||||||
|
self.gnb = gnb_ctrl
|
||||||
|
self.session_id = 1
|
||||||
|
self.running = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
self._send_hello()
|
||||||
|
hello_msg = self._recv_message()
|
||||||
|
if hello_msg is None:
|
||||||
|
log.warning("Client did not send hello")
|
||||||
|
return
|
||||||
|
log.info("NETCONF hello exchange complete")
|
||||||
|
|
||||||
|
while self.running:
|
||||||
|
msg = self._recv_message()
|
||||||
|
if msg is None:
|
||||||
|
break
|
||||||
|
self._process_rpc(msg)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Session error: %s", e)
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
self.channel.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
log.info("NETCONF session closed")
|
||||||
|
|
||||||
|
def _send_hello(self):
|
||||||
|
hello = (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>'
|
||||||
|
f'<hello xmlns="{NS_NC}">'
|
||||||
|
"<capabilities>"
|
||||||
|
"<capability>urn:ietf:params:netconf:base:1.0</capability>"
|
||||||
|
"<capability>urn:ietf:params:netconf:capability:writable-running:1.0</capability>"
|
||||||
|
"</capabilities>"
|
||||||
|
f"<session-id>{self.session_id}</session-id>"
|
||||||
|
"</hello>"
|
||||||
|
)
|
||||||
|
self._send_message(hello)
|
||||||
|
|
||||||
|
def _send_message(self, xml_str):
|
||||||
|
data = (xml_str + NETCONF_END).encode("utf-8")
|
||||||
|
self.channel.sendall(data)
|
||||||
|
|
||||||
|
def _recv_message(self):
|
||||||
|
buf = b""
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
self.channel.settimeout(300)
|
||||||
|
chunk = self.channel.recv(65536)
|
||||||
|
if not chunk:
|
||||||
|
return None
|
||||||
|
buf += chunk
|
||||||
|
text = buf.decode("utf-8", errors="ignore")
|
||||||
|
if NETCONF_END in text:
|
||||||
|
return text.split(NETCONF_END)[0].strip()
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _process_rpc(self, xml_str):
|
||||||
|
# Strip XML declaration if present
|
||||||
|
xml_str = re.sub(r"<\?xml[^?]*\?>", "", xml_str).strip()
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(xml_str)
|
||||||
|
except ET.ParseError as e:
|
||||||
|
log.warning("Malformed RPC: %s", e)
|
||||||
|
self._send_error("0", "malformed-message", "rpc")
|
||||||
|
return
|
||||||
|
|
||||||
|
msg_id = root.get("message-id", "0")
|
||||||
|
|
||||||
|
# Find the operation element
|
||||||
|
for child in root:
|
||||||
|
local_tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag
|
||||||
|
|
||||||
|
if local_tag == "edit-config":
|
||||||
|
self._handle_edit_config(msg_id, child)
|
||||||
|
elif local_tag == "get":
|
||||||
|
self._handle_get(msg_id, child)
|
||||||
|
elif local_tag == "get-config":
|
||||||
|
self._handle_get_config(msg_id, child)
|
||||||
|
elif local_tag == "close-session":
|
||||||
|
self._send_ok(msg_id)
|
||||||
|
self.running = False
|
||||||
|
elif local_tag == "lock" or local_tag == "unlock":
|
||||||
|
self._send_ok(msg_id)
|
||||||
|
else:
|
||||||
|
self._send_error(msg_id, "operation-not-supported", "protocol")
|
||||||
|
return # Only process first operation
|
||||||
|
|
||||||
|
def _handle_edit_config(self, msg_id, edit_elem):
|
||||||
|
"""Handle edit-config for tx-array-antenna-mask-config."""
|
||||||
|
# Find <config> element
|
||||||
|
config_elem = None
|
||||||
|
for child in edit_elem:
|
||||||
|
local = child.tag.split("}")[-1] if "}" in child.tag else child.tag
|
||||||
|
if local == "config":
|
||||||
|
config_elem = child
|
||||||
|
break
|
||||||
|
|
||||||
|
if config_elem is None:
|
||||||
|
self._send_error(msg_id, "missing-element", "protocol",
|
||||||
|
"Missing <config> element")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find tx-array-antenna-mask-config under user-plane-configuration
|
||||||
|
mask_config = None
|
||||||
|
for elem in config_elem.iter():
|
||||||
|
local = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
|
||||||
|
if local == "tx-array-antenna-mask-config":
|
||||||
|
mask_config = elem
|
||||||
|
break
|
||||||
|
|
||||||
|
if mask_config is None:
|
||||||
|
# No antenna mask config in this edit - accept silently
|
||||||
|
self._send_ok(msg_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Extract array-name, antenna-bitmask-index or antenna-bitmask
|
||||||
|
array_name = None
|
||||||
|
bitmask_index = None
|
||||||
|
bitmask_b64 = None
|
||||||
|
|
||||||
|
for child in mask_config:
|
||||||
|
local = child.tag.split("}")[-1] if "}" in child.tag else child.tag
|
||||||
|
if local == "array-name":
|
||||||
|
array_name = child.text.strip() if child.text else None
|
||||||
|
elif local == "antenna-bitmask-index":
|
||||||
|
bitmask_index = int(child.text.strip()) if child.text else None
|
||||||
|
elif local == "antenna-bitmask":
|
||||||
|
bitmask_b64 = child.text.strip() if child.text else None
|
||||||
|
|
||||||
|
if not array_name:
|
||||||
|
self._send_error(msg_id, "missing-element", "application",
|
||||||
|
"Missing array-name in tx-array-antenna-mask-config")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Resolve the mask
|
||||||
|
resolved = None
|
||||||
|
if bitmask_index is not None:
|
||||||
|
resolved, err = self.cap.resolve_index(array_name, bitmask_index)
|
||||||
|
if not resolved:
|
||||||
|
self._send_error(msg_id, "invalid-value", "application", err)
|
||||||
|
return
|
||||||
|
log.info("M-Plane edit-config: array=%s, antenna-bitmask-index=%d",
|
||||||
|
array_name, bitmask_index)
|
||||||
|
elif bitmask_b64 is not None:
|
||||||
|
mask_int = b64_to_mask(bitmask_b64)
|
||||||
|
resolved, err = self.cap.resolve_mask(array_name, mask_int)
|
||||||
|
if not resolved:
|
||||||
|
self._send_error(msg_id, "invalid-value", "application", err)
|
||||||
|
return
|
||||||
|
log.info("M-Plane edit-config: array=%s, antenna-bitmask=%s (0x%02X)",
|
||||||
|
array_name, bitmask_b64, mask_int)
|
||||||
|
else:
|
||||||
|
self._send_error(msg_id, "missing-element", "application",
|
||||||
|
"Need antenna-bitmask or antenna-bitmask-index")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Apply to gNB via internal control socket
|
||||||
|
mask_int = resolved["int"]
|
||||||
|
resp = self.gnb.send_command(f"SET TX MASK 0x{mask_int:02X}")
|
||||||
|
log.info("Applied mask 0x%02X to gNB: %s", mask_int, resp.strip())
|
||||||
|
|
||||||
|
# Update current config
|
||||||
|
self.cap.current_config[array_name] = {
|
||||||
|
"index": resolved["index"],
|
||||||
|
"mask_int": mask_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Log WG4 mplane-trx-control-ant-mask-update notification
|
||||||
|
log.info("WG4 mplane-trx-control-ant-mask-update: "
|
||||||
|
"array-name='%s', array-type=TX-ARRAY, "
|
||||||
|
"updated-antenna-mask-index=%d, updated-antenna-mask=%s",
|
||||||
|
array_name, resolved["index"], resolved["b64"])
|
||||||
|
|
||||||
|
self._send_ok(msg_id)
|
||||||
|
|
||||||
|
def _handle_get(self, msg_id, get_elem):
|
||||||
|
"""Handle <get> - return operational data (capability + current config)."""
|
||||||
|
data_xml = self.cap.get_capability_xml()
|
||||||
|
reply = (
|
||||||
|
f'<rpc-reply xmlns="{NS_NC}" message-id="{msg_id}">'
|
||||||
|
f"<data>{data_xml}</data>"
|
||||||
|
"</rpc-reply>"
|
||||||
|
)
|
||||||
|
self._send_message(reply)
|
||||||
|
|
||||||
|
def _handle_get_config(self, msg_id, get_elem):
|
||||||
|
"""Handle <get-config> - return running config (current mask config)."""
|
||||||
|
parts = [f'<user-plane-configuration xmlns="{NS_UPLANE}">']
|
||||||
|
for aname, cfg in self.cap.current_config.items():
|
||||||
|
parts.append("<tx-array-antenna-mask-config>")
|
||||||
|
parts.append(f"<array-name>{aname}</array-name>")
|
||||||
|
parts.append(f"<antenna-bitmask-index>{cfg['index']}</antenna-bitmask-index>")
|
||||||
|
parts.append("</tx-array-antenna-mask-config>")
|
||||||
|
parts.append("</user-plane-configuration>")
|
||||||
|
reply = (
|
||||||
|
f'<rpc-reply xmlns="{NS_NC}" message-id="{msg_id}">'
|
||||||
|
f'<data>{"".join(parts)}</data>'
|
||||||
|
"</rpc-reply>"
|
||||||
|
)
|
||||||
|
self._send_message(reply)
|
||||||
|
|
||||||
|
def _send_ok(self, msg_id):
|
||||||
|
reply = (
|
||||||
|
f'<rpc-reply xmlns="{NS_NC}" message-id="{msg_id}">'
|
||||||
|
"<ok/>"
|
||||||
|
"</rpc-reply>"
|
||||||
|
)
|
||||||
|
self._send_message(reply)
|
||||||
|
|
||||||
|
def _send_error(self, msg_id, error_tag, error_type, error_message=""):
|
||||||
|
msg_part = ""
|
||||||
|
if error_message:
|
||||||
|
msg_part = f"<error-message>{error_message}</error-message>"
|
||||||
|
reply = (
|
||||||
|
f'<rpc-reply xmlns="{NS_NC}" message-id="{msg_id}">'
|
||||||
|
"<rpc-error>"
|
||||||
|
f"<error-type>{error_type}</error-type>"
|
||||||
|
f"<error-tag>{error_tag}</error-tag>"
|
||||||
|
"<error-severity>error</error-severity>"
|
||||||
|
f"{msg_part}"
|
||||||
|
"</rpc-error>"
|
||||||
|
"</rpc-reply>"
|
||||||
|
)
|
||||||
|
self._send_message(reply)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SSH Server Interface
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class MplaneSSHServer(paramiko.ServerInterface):
|
||||||
|
def __init__(self):
|
||||||
|
self.subsystem_event = threading.Event()
|
||||||
|
|
||||||
|
def check_channel_request(self, kind, chanid):
|
||||||
|
if kind == "session":
|
||||||
|
return paramiko.OPEN_SUCCEEDED
|
||||||
|
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
|
||||||
|
|
||||||
|
def get_allowed_auths(self, username):
|
||||||
|
return "password"
|
||||||
|
|
||||||
|
def check_auth_password(self, username, password):
|
||||||
|
if username == MPLANE_USER and password == MPLANE_PASS:
|
||||||
|
return paramiko.AUTH_SUCCESSFUL
|
||||||
|
return paramiko.AUTH_FAILED
|
||||||
|
|
||||||
|
def check_channel_subsystem_request(self, channel, name):
|
||||||
|
if name == "netconf":
|
||||||
|
self.subsystem_event.set()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main Server
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def handle_connection(client_sock, addr, host_key, capability, gnb_ctrl):
|
||||||
|
"""Handle a single SSH/NETCONF client connection."""
|
||||||
|
log.info("SSH connection from %s:%d", addr[0], addr[1])
|
||||||
|
transport = None
|
||||||
|
try:
|
||||||
|
transport = paramiko.Transport(client_sock)
|
||||||
|
transport.add_server_key(host_key)
|
||||||
|
server = MplaneSSHServer()
|
||||||
|
transport.start_server(server=server)
|
||||||
|
|
||||||
|
channel = transport.accept(30)
|
||||||
|
if channel is None:
|
||||||
|
log.warning("No channel opened by client")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Wait for NETCONF subsystem request
|
||||||
|
if not server.subsystem_event.wait(10):
|
||||||
|
log.warning("Client did not request netconf subsystem")
|
||||||
|
channel.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
session = NetconfSession(channel, capability, gnb_ctrl)
|
||||||
|
session.run()
|
||||||
|
|
||||||
|
except paramiko.SSHException as e:
|
||||||
|
log.warning("SSH error from %s: %s", addr[0], e)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Connection error from %s: %s", addr[0], e)
|
||||||
|
finally:
|
||||||
|
if transport:
|
||||||
|
try:
|
||||||
|
transport.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log.info("O-RU M-Plane NETCONF server starting")
|
||||||
|
log.info(" Port: %d", MPLANE_PORT)
|
||||||
|
log.info(" gNB control: %s:%d (internal)", GNB_CTRL_HOST, GNB_CTRL_PORT)
|
||||||
|
|
||||||
|
# Load/generate SSH host key
|
||||||
|
if os.path.exists(SSH_HOST_KEY):
|
||||||
|
host_key = paramiko.RSAKey.from_private_key_file(SSH_HOST_KEY)
|
||||||
|
log.info("Loaded SSH host key from %s", SSH_HOST_KEY)
|
||||||
|
else:
|
||||||
|
host_key = paramiko.RSAKey.generate(2048)
|
||||||
|
os.makedirs(os.path.dirname(SSH_HOST_KEY), exist_ok=True)
|
||||||
|
host_key.write_private_key_file(SSH_HOST_KEY)
|
||||||
|
log.info("Generated SSH host key at %s", SSH_HOST_KEY)
|
||||||
|
|
||||||
|
# Load WG4 capability
|
||||||
|
capability = Capability()
|
||||||
|
capability.load(WG4_CAPABILITY_FILE)
|
||||||
|
for name, arr in capability.tx_arrays.items():
|
||||||
|
log.info(" tx-array '%s': %d supported masks", name, len(arr["masks"]))
|
||||||
|
for m in arr["masks"]:
|
||||||
|
log.info(" index=%d: %s (0x%02X, %d active)",
|
||||||
|
m["index"], m["b64"], m["int"], bin(m["int"]).count("1"))
|
||||||
|
|
||||||
|
# Connect to gNB internal control
|
||||||
|
gnb_ctrl = GnbCtrlClient(GNB_CTRL_HOST, GNB_CTRL_PORT)
|
||||||
|
gnb_ctrl.connect()
|
||||||
|
|
||||||
|
# Create server socket
|
||||||
|
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
server_sock.bind(("0.0.0.0", MPLANE_PORT))
|
||||||
|
server_sock.listen(5)
|
||||||
|
server_sock.settimeout(1.0)
|
||||||
|
|
||||||
|
shutdown = threading.Event()
|
||||||
|
|
||||||
|
def handle_signal(signum, frame):
|
||||||
|
log.info("Shutdown signal received")
|
||||||
|
shutdown.set()
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, handle_signal)
|
||||||
|
signal.signal(signal.SIGINT, handle_signal)
|
||||||
|
|
||||||
|
log.info("M-Plane NETCONF server ready on port %d", MPLANE_PORT)
|
||||||
|
|
||||||
|
while not shutdown.is_set():
|
||||||
|
try:
|
||||||
|
client_sock, addr = server_sock.accept()
|
||||||
|
t = threading.Thread(
|
||||||
|
target=handle_connection,
|
||||||
|
args=(client_sock, addr, host_key, capability, gnb_ctrl),
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
t.start()
|
||||||
|
except socket.timeout:
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
if not shutdown.is_set():
|
||||||
|
log.exception("Accept error: %s", e)
|
||||||
|
|
||||||
|
log.info("Shutting down...")
|
||||||
|
server_sock.close()
|
||||||
|
gnb_ctrl.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"tx-arrays": [
|
||||||
|
{
|
||||||
|
"name": "tx-array-0",
|
||||||
|
"number-of-columns": 4,
|
||||||
|
"number-of-array-layers": 4,
|
||||||
|
"mplane-trx-control-txarr-capability-info": {
|
||||||
|
"mplane-supported-trx-control-masks": [
|
||||||
|
{"index": 0, "antenna-mask": "Dw=="},
|
||||||
|
{"index": 1, "antenna-mask": "Bw=="},
|
||||||
|
{"index": 2, "antenna-mask": "Aw=="},
|
||||||
|
{"index": 3, "antenna-mask": "AQ=="}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -143,6 +143,9 @@ void phy_init_nr_gNB(PHY_VARS_gNB *gNB)
|
|||||||
|
|
||||||
init_DLSCH_struct(gNB);
|
init_DLSCH_struct(gNB);
|
||||||
|
|
||||||
|
nr_init_antenna_state(&gNB->antenna_state, Ptx, Prx, "tx-array-0", "rx-array-0");
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Initialized with %d TX, %d RX antennas\n", Ptx, Prx);
|
||||||
|
|
||||||
/* Do NOT allocate per-antenna rxdataF: the gNB gets a pointer to the
|
/* Do NOT allocate per-antenna rxdataF: the gNB gets a pointer to the
|
||||||
* RU to copy/recover freq-domain memory from there */
|
* RU to copy/recover freq-domain memory from there */
|
||||||
common_vars->rxdataF = (c16_t ***)malloc16(common_vars->num_beams_period * sizeof(c16_t**));
|
common_vars->rxdataF = (c16_t ***)malloc16(common_vars->num_beams_period * sizeof(c16_t**));
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "PHY/LTE_TRANSPORT/transport_eNB.h"
|
#include "PHY/LTE_TRANSPORT/transport_eNB.h"
|
||||||
#include "modulation_eNB.h"
|
#include "modulation_eNB.h"
|
||||||
#include "common/utils/LOG/vcd_signal_dumper.h"
|
#include "common/utils/LOG/vcd_signal_dumper.h"
|
||||||
|
#include "PHY/NR_TRANSPORT/nr_antenna_state.h"
|
||||||
|
|
||||||
int beam_precoding(int32_t **txdataF,
|
int beam_precoding(int32_t **txdataF,
|
||||||
int32_t **txdataF_BF,
|
int32_t **txdataF_BF,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
#include "nr_modulation.h"
|
#include "nr_modulation.h"
|
||||||
#include "openair1/PHY/TOOLS/tools_defs.h"
|
#include "openair1/PHY/TOOLS/tools_defs.h"
|
||||||
|
#include "PHY/NR_TRANSPORT/nr_antenna_state.h"
|
||||||
|
#include "common/utils/LOG/log.h"
|
||||||
|
|
||||||
void nr_beam_precoding(c16_t **txdataF,
|
void nr_beam_precoding(c16_t **txdataF,
|
||||||
c16_t **txdataF_BF,
|
c16_t **txdataF_BF,
|
||||||
@@ -25,3 +27,33 @@ void nr_beam_precoding(c16_t **txdataF,
|
|||||||
15);
|
15);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void nr_beam_precoding_with_muting(c16_t **txdataF,
|
||||||
|
c16_t **txdataF_BF,
|
||||||
|
NR_DL_FRAME_PARMS *frame_parms,
|
||||||
|
int32_t ***beam_weights,
|
||||||
|
int symbol,
|
||||||
|
int aa,
|
||||||
|
int nb_antenna_ports,
|
||||||
|
int offset,
|
||||||
|
const nr_antenna_state_t *ant_state)
|
||||||
|
{
|
||||||
|
// Clear output buffer first
|
||||||
|
memset(&txdataF_BF[aa][symbol * frame_parms->ofdm_symbol_size], 0,
|
||||||
|
sizeof(c16_t) * frame_parms->ofdm_symbol_size);
|
||||||
|
// O-RAN energy saving: skip precoding if antenna is muted
|
||||||
|
// This implements transmission blanking per WG4 spec
|
||||||
|
if (ant_state != NULL && !nr_is_tx_antenna_active(ant_state, aa)) {
|
||||||
|
LOG_D(PHY, "[ANTENNA_MUTE] Antenna %d muted, blanking transmission for symbol %d\n",
|
||||||
|
aa, symbol);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Normal precoding for active antennas
|
||||||
|
for (int p = 0; p < nb_antenna_ports; p++) {
|
||||||
|
multadd_cpx_vector((c16_t *)&txdataF[p][(symbol * frame_parms->ofdm_symbol_size) + offset],
|
||||||
|
(c16_t *)beam_weights[p][aa],
|
||||||
|
(c16_t *)&txdataF_BF[aa][symbol * frame_parms->ofdm_symbol_size],
|
||||||
|
frame_parms->ofdm_symbol_size,
|
||||||
|
15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,6 +84,33 @@ void nr_beam_precoding(c16_t **txdataF,
|
|||||||
int nb_antenna_ports,
|
int nb_antenna_ports,
|
||||||
int offset);
|
int offset);
|
||||||
|
|
||||||
|
|
||||||
|
/*! \brief NR beam precoding with O-RAN compliant antenna muting
|
||||||
|
*
|
||||||
|
* Extends nr_beam_precoding to support antenna muting for energy saving
|
||||||
|
* per O-RAN WG4 MPLANE-TRX-CONTROL specification (o-ran-uplane-conf.yang).
|
||||||
|
* When an antenna is muted, transmission is blanked (output zeroed).
|
||||||
|
*
|
||||||
|
* @param[in] txdataF Input frequency-domain data per antenna port
|
||||||
|
* @param[out] txdataF_BF Output beamformed data per physical antenna
|
||||||
|
* @param[in] frame_parms Frame parameters
|
||||||
|
* @param[in] beam_weights Beamforming weights
|
||||||
|
* @param[in] symbol Current symbol number
|
||||||
|
* @param[in] aa Physical antenna index
|
||||||
|
* @param[in] nb_antenna_ports Number of antenna ports
|
||||||
|
* @param[in] offset Frequency offset
|
||||||
|
* @param[in] ant_state Antenna state for muting control (NULL = all active)
|
||||||
|
*/
|
||||||
|
void nr_beam_precoding_with_muting(c16_t **txdataF,
|
||||||
|
c16_t **txdataF_BF,
|
||||||
|
NR_DL_FRAME_PARMS *frame_parms,
|
||||||
|
int32_t ***beam_weights,
|
||||||
|
int symbol,
|
||||||
|
int aa,
|
||||||
|
int nb_antenna_ports,
|
||||||
|
int offset,
|
||||||
|
const nr_antenna_state_t *ant_state);
|
||||||
|
|
||||||
void apply_nr_rotation_TX(const NR_DL_FRAME_PARMS *fp,
|
void apply_nr_rotation_TX(const NR_DL_FRAME_PARMS *fp,
|
||||||
c16_t *txdataF,
|
c16_t *txdataF,
|
||||||
bool is_flat_buff,
|
bool is_flat_buff,
|
||||||
|
|||||||
@@ -0,0 +1,532 @@
|
|||||||
|
/*
|
||||||
|
* Licensed to the OpenAirInterface (OAI) Software Alliance under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The OpenAirInterface Software Alliance licenses this file to You under
|
||||||
|
* the OAI Public License, Version 1.1 (the "License"); you may not use this file
|
||||||
|
* except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.openairinterface.org/?page_id=698
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*-------------------------------------------------------------------------------
|
||||||
|
* For more information about the OpenAirInterface (OAI) Software Alliance:
|
||||||
|
* contact@openairinterface.org
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! \file PHY/NR_TRANSPORT/nr_antenna_control.c
|
||||||
|
* \brief Antenna control interface implementation with O-RAN M-Plane support
|
||||||
|
* \author OpenAirInterface
|
||||||
|
* \date 2025
|
||||||
|
* \version 2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "nr_antenna_control.h"
|
||||||
|
#include "common/utils/LOG/log.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
// Try to include M-Plane header if available
|
||||||
|
#ifdef OAI_MPLANE
|
||||||
|
#include "radio/fhi_72/mplane/antenna-control.h"
|
||||||
|
#include "radio/fhi_72/mplane/ru-mplane-api.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Global control state
|
||||||
|
static struct {
|
||||||
|
pthread_t thread;
|
||||||
|
int server_fd;
|
||||||
|
nr_antenna_state_t *ant_state;
|
||||||
|
volatile bool running;
|
||||||
|
int port;
|
||||||
|
nr_antenna_ctrl_backend_t backend;
|
||||||
|
#ifdef OAI_MPLANE
|
||||||
|
ru_session_t *ru_session;
|
||||||
|
#else
|
||||||
|
void *ru_session;
|
||||||
|
#endif
|
||||||
|
} g_ctrl = {0};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test Socket Server Implementation (Development/Testing Mode)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// Process a single command and generate response
|
||||||
|
static void process_command(const char *cmd, char *response, size_t resp_size)
|
||||||
|
{
|
||||||
|
char cmd_upper[256];
|
||||||
|
strncpy(cmd_upper, cmd, sizeof(cmd_upper) - 1);
|
||||||
|
cmd_upper[sizeof(cmd_upper) - 1] = '\0';
|
||||||
|
|
||||||
|
// Convert to uppercase for case-insensitive matching
|
||||||
|
for (char *p = cmd_upper; *p; p++) {
|
||||||
|
if (*p >= 'a' && *p <= 'z') *p -= 32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim whitespace
|
||||||
|
char *p = cmd_upper;
|
||||||
|
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') p++;
|
||||||
|
char *end = p + strlen(p) - 1;
|
||||||
|
while (end > p && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) *end-- = '\0';
|
||||||
|
|
||||||
|
if (strncmp(p, "MUTE TX ", 8) == 0) {
|
||||||
|
int ant_id = atoi(p + 8);
|
||||||
|
nr_mute_tx_antenna(g_ctrl.ant_state, ant_id);
|
||||||
|
snprintf(response, resp_size, "OK: TX antenna %d muted\n", ant_id);
|
||||||
|
}
|
||||||
|
else if (strncmp(p, "UNMUTE TX ", 10) == 0) {
|
||||||
|
int ant_id = atoi(p + 10);
|
||||||
|
nr_unmute_tx_antenna(g_ctrl.ant_state, ant_id);
|
||||||
|
snprintf(response, resp_size, "OK: TX antenna %d unmuted\n", ant_id);
|
||||||
|
}
|
||||||
|
else if (strncmp(p, "MUTE RX ", 8) == 0) {
|
||||||
|
int ant_id = atoi(p + 8);
|
||||||
|
nr_mute_rx_antenna(g_ctrl.ant_state, ant_id);
|
||||||
|
snprintf(response, resp_size, "OK: RX antenna %d muted\n", ant_id);
|
||||||
|
}
|
||||||
|
else if (strncmp(p, "UNMUTE RX ", 10) == 0) {
|
||||||
|
int ant_id = atoi(p + 10);
|
||||||
|
nr_unmute_rx_antenna(g_ctrl.ant_state, ant_id);
|
||||||
|
snprintf(response, resp_size, "OK: RX antenna %d unmuted\n", ant_id);
|
||||||
|
}
|
||||||
|
else if (strncmp(p, "SET TX MASK ", 12) == 0) {
|
||||||
|
unsigned int mask;
|
||||||
|
if (sscanf(p + 12, "%x", &mask) == 1 || sscanf(p + 12, "0X%x", &mask) == 1) {
|
||||||
|
uint8_t old = nr_set_tx_antenna_bitmask(g_ctrl.ant_state, (uint8_t)mask);
|
||||||
|
snprintf(response, resp_size, "OK: TX mask 0x%02X -> 0x%02X\n", old, (uint8_t)mask);
|
||||||
|
} else {
|
||||||
|
snprintf(response, resp_size, "ERROR: Invalid mask format. Use hex (e.g., 0F or 0x0F)\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (strncmp(p, "SET RX MASK ", 12) == 0) {
|
||||||
|
unsigned int mask;
|
||||||
|
if (sscanf(p + 12, "%x", &mask) == 1 || sscanf(p + 12, "0X%x", &mask) == 1) {
|
||||||
|
uint8_t old = nr_set_rx_antenna_bitmask(g_ctrl.ant_state, (uint8_t)mask);
|
||||||
|
snprintf(response, resp_size, "OK: RX mask 0x%02X -> 0x%02X\n", old, (uint8_t)mask);
|
||||||
|
} else {
|
||||||
|
snprintf(response, resp_size, "ERROR: Invalid mask format. Use hex (e.g., 0F or 0x0F)\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (strcmp(p, "STATUS") == 0) {
|
||||||
|
bool in_recovery = nr_is_in_recovery_mode(g_ctrl.ant_state);
|
||||||
|
snprintf(response, resp_size,
|
||||||
|
"=== Antenna Status ===\n"
|
||||||
|
"TX Array: %s\n"
|
||||||
|
" Antennas: %d configured, %d active\n"
|
||||||
|
" Bitmask: 0x%02X\n"
|
||||||
|
" Antenna 0 protected: %s\n"
|
||||||
|
"RX Array: %s\n"
|
||||||
|
" Antennas: %d configured, %d active\n"
|
||||||
|
" Bitmask: 0x%02X\n"
|
||||||
|
"Recovery Mode: %s\n"
|
||||||
|
"Backend: %s\n",
|
||||||
|
g_ctrl.ant_state->tx_array_name,
|
||||||
|
g_ctrl.ant_state->num_tx_antennas,
|
||||||
|
nr_get_active_tx_antenna_count(g_ctrl.ant_state),
|
||||||
|
nr_get_tx_antenna_bitmask(g_ctrl.ant_state),
|
||||||
|
g_ctrl.ant_state->config.protect_antenna0 ? "YES" : "NO",
|
||||||
|
g_ctrl.ant_state->rx_array_name,
|
||||||
|
g_ctrl.ant_state->num_rx_antennas,
|
||||||
|
nr_get_active_rx_antenna_count(g_ctrl.ant_state),
|
||||||
|
nr_get_rx_antenna_bitmask(g_ctrl.ant_state),
|
||||||
|
in_recovery ? "ACTIVE (use identity matrix)" : "OFF",
|
||||||
|
g_ctrl.backend == ANTENNA_CTRL_BACKEND_MPLANE ? "O-RAN M-Plane NETCONF" :
|
||||||
|
g_ctrl.backend == ANTENNA_CTRL_BACKEND_TEST_SOCKET ? "Test Socket (NOT O-RAN)" :
|
||||||
|
"None");
|
||||||
|
}
|
||||||
|
else if (strcmp(p, "STATS") == 0) {
|
||||||
|
uint64_t tx_events, rx_events, muted_slots;
|
||||||
|
nr_get_antenna_stats(g_ctrl.ant_state, &tx_events, &rx_events, &muted_slots);
|
||||||
|
snprintf(response, resp_size,
|
||||||
|
"=== Energy Saving Statistics ===\n"
|
||||||
|
"TX mute events: %lu\n"
|
||||||
|
"RX mute events: %lu\n"
|
||||||
|
"Total muted slots: %lu\n",
|
||||||
|
tx_events, rx_events, muted_slots);
|
||||||
|
}
|
||||||
|
else if (strcmp(p, "RESET STATS") == 0) {
|
||||||
|
nr_reset_antenna_stats(g_ctrl.ant_state);
|
||||||
|
snprintf(response, resp_size, "OK: Statistics reset\n");
|
||||||
|
}
|
||||||
|
else if (strcmp(p, "RESET") == 0 || strcmp(p, "UNMUTE ALL") == 0) {
|
||||||
|
// Reset all antennas to active state with recovery mode
|
||||||
|
nr_unmute_all_tx_antennas(g_ctrl.ant_state);
|
||||||
|
nr_unmute_all_rx_antennas(g_ctrl.ant_state);
|
||||||
|
snprintf(response, resp_size,
|
||||||
|
"OK: All antennas unmuted (recovery mode: %u ms)\n"
|
||||||
|
" TX mask: 0x%02X (%d antennas)\n"
|
||||||
|
" RX mask: 0x%02X (%d antennas)\n",
|
||||||
|
g_ctrl.ant_state->recovery.recovery_duration_ms,
|
||||||
|
nr_get_tx_antenna_bitmask(g_ctrl.ant_state), g_ctrl.ant_state->num_tx_antennas,
|
||||||
|
nr_get_rx_antenna_bitmask(g_ctrl.ant_state), g_ctrl.ant_state->num_rx_antennas);
|
||||||
|
}
|
||||||
|
else if (strcmp(p, "PROTECT ANT0 ON") == 0) {
|
||||||
|
nr_set_antenna0_protection(g_ctrl.ant_state, true);
|
||||||
|
snprintf(response, resp_size, "OK: Antenna 0 protection enabled (SSB)\n");
|
||||||
|
}
|
||||||
|
else if (strcmp(p, "PROTECT ANT0 OFF") == 0) {
|
||||||
|
nr_set_antenna0_protection(g_ctrl.ant_state, false);
|
||||||
|
snprintf(response, resp_size, "OK: Antenna 0 protection disabled (WARNING: may cause UE sync loss)\n");
|
||||||
|
}
|
||||||
|
else if (strcmp(p, "SYNC") == 0) {
|
||||||
|
// Synchronize to RU (only works with M-Plane backend)
|
||||||
|
if (nr_antenna_control_sync_to_ru(g_ctrl.ant_state)) {
|
||||||
|
snprintf(response, resp_size, "OK: Synchronized to RU\n");
|
||||||
|
} else {
|
||||||
|
snprintf(response, resp_size, "ERROR: Sync failed (M-Plane backend required)\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (strcmp(p, "HELP") == 0) {
|
||||||
|
snprintf(response, resp_size,
|
||||||
|
"=== Antenna Control ===\n"
|
||||||
|
"MUTE TX/RX <id> - Mute antenna\n"
|
||||||
|
"UNMUTE TX/RX <id> - Unmute antenna\n"
|
||||||
|
"SET TX/RX MASK <hex> - Set bitmask\n"
|
||||||
|
"STATUS - Show state\n"
|
||||||
|
"STATS - Show statistics\n"
|
||||||
|
"RESET - Unmute all\n"
|
||||||
|
"PROTECT ANT0 ON/OFF\n"
|
||||||
|
"HELP/QUIT\n");
|
||||||
|
}
|
||||||
|
else if (strcmp(p, "QUIT") == 0 || strcmp(p, "EXIT") == 0) {
|
||||||
|
snprintf(response, resp_size, "BYE\n");
|
||||||
|
}
|
||||||
|
else if (strlen(p) == 0) {
|
||||||
|
response[0] = '\0'; // Empty command, no response
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
snprintf(response, resp_size, "ERROR: Unknown command '%s'. Type HELP for usage.\n", cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle a single client connection
|
||||||
|
static void handle_client(int client_fd)
|
||||||
|
{
|
||||||
|
char buffer[256];
|
||||||
|
char response[1024];
|
||||||
|
|
||||||
|
// Send welcome message with warning about non-compliance
|
||||||
|
const char *welcome =
|
||||||
|
"=== OAI Antenna Control Test Server ===\n"
|
||||||
|
"WARNING: This is a TEST interface, NOT O-RAN compliant!\n"
|
||||||
|
"For production, use NETCONF via radio/fhi_72/mplane/\n"
|
||||||
|
"Type HELP for commands.\n\n";
|
||||||
|
send(client_fd, welcome, strlen(welcome), 0);
|
||||||
|
|
||||||
|
while (g_ctrl.running) {
|
||||||
|
// Send prompt
|
||||||
|
send(client_fd, "> ", 2, 0);
|
||||||
|
|
||||||
|
// Receive command
|
||||||
|
ssize_t n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
|
||||||
|
if (n <= 0) break;
|
||||||
|
|
||||||
|
buffer[n] = '\0';
|
||||||
|
|
||||||
|
// Process command
|
||||||
|
process_command(buffer, response, sizeof(response));
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
if (strlen(response) > 0) {
|
||||||
|
send(client_fd, response, strlen(response), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for quit
|
||||||
|
if (strstr(buffer, "QUIT") || strstr(buffer, "quit") ||
|
||||||
|
strstr(buffer, "EXIT") || strstr(buffer, "exit")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(client_fd);
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] Client disconnected\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server thread function
|
||||||
|
static void *server_thread(void *arg)
|
||||||
|
{
|
||||||
|
(void)arg;
|
||||||
|
|
||||||
|
struct sockaddr_in client_addr;
|
||||||
|
socklen_t addr_len = sizeof(client_addr);
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] Test server listening on port %d\n", g_ctrl.port);
|
||||||
|
|
||||||
|
while (g_ctrl.running) {
|
||||||
|
// Accept connection with timeout
|
||||||
|
fd_set read_fds;
|
||||||
|
FD_ZERO(&read_fds);
|
||||||
|
FD_SET(g_ctrl.server_fd, &read_fds);
|
||||||
|
|
||||||
|
struct timeval tv = {.tv_sec = 1, .tv_usec = 0};
|
||||||
|
int ret = select(g_ctrl.server_fd + 1, &read_fds, NULL, NULL, &tv);
|
||||||
|
|
||||||
|
if (ret <= 0) continue;
|
||||||
|
|
||||||
|
int client_fd = accept(g_ctrl.server_fd, (struct sockaddr *)&client_addr, &addr_len);
|
||||||
|
if (client_fd < 0) continue;
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] Client connected from %s:%d\n",
|
||||||
|
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
|
||||||
|
|
||||||
|
handle_client(client_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Public API Implementation
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
bool nr_antenna_control_init_mplane(nr_antenna_state_t *ant_state, void *ru_session)
|
||||||
|
{
|
||||||
|
#ifdef OAI_MPLANE
|
||||||
|
if (g_ctrl.running) {
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] Control interface already active\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ant_state == NULL || ru_session == NULL) {
|
||||||
|
LOG_E(PHY, "[ANTENNA_CTRL] Invalid parameters\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_ctrl.ant_state = ant_state;
|
||||||
|
g_ctrl.ru_session = (ru_session_t *)ru_session;
|
||||||
|
g_ctrl.backend = ANTENNA_CTRL_BACKEND_MPLANE;
|
||||||
|
g_ctrl.running = true;
|
||||||
|
|
||||||
|
// Check if RU supports TRX control feature
|
||||||
|
if (!mplane_supports_trx_control(g_ctrl.ru_session)) {
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] RU does not support MPLANE-TRX-CONTROL feature\n");
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] Antenna muting may not work on this RU\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] Initialized with O-RAN M-Plane NETCONF backend\n");
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] TX array: %s, RX array: %s\n",
|
||||||
|
ant_state->tx_array_name, ant_state->rx_array_name);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
LOG_E(PHY, "[ANTENNA_CTRL] M-Plane support not compiled in (OAI_MPLANE not defined)\n");
|
||||||
|
LOG_E(PHY, "[ANTENNA_CTRL] Use nr_antenna_control_start_test_server() for testing\n");
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nr_antenna_control_start_test_server(nr_antenna_state_t *ant_state, int port)
|
||||||
|
{
|
||||||
|
if (g_ctrl.running) {
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] Control interface already active\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ant_state == NULL) {
|
||||||
|
LOG_E(PHY, "[ANTENNA_CTRL] Antenna state is NULL\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] ======================================================\n");
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] Starting TEST socket server - NOT O-RAN COMPLIANT!\n");
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] For production O-RAN compliance, use M-Plane NETCONF\n");
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] See: radio/fhi_72/mplane/antenna-control-mplane.h\n");
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] ======================================================\n");
|
||||||
|
|
||||||
|
g_ctrl.ant_state = ant_state;
|
||||||
|
g_ctrl.port = (port > 0) ? port : NR_ANTENNA_CONTROL_TEST_PORT;
|
||||||
|
g_ctrl.backend = ANTENNA_CTRL_BACKEND_TEST_SOCKET;
|
||||||
|
|
||||||
|
// Create socket
|
||||||
|
g_ctrl.server_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (g_ctrl.server_fd < 0) {
|
||||||
|
LOG_E(PHY, "[ANTENNA_CTRL] Failed to create socket: %s\n", strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow address reuse
|
||||||
|
int opt = 1;
|
||||||
|
setsockopt(g_ctrl.server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||||
|
|
||||||
|
// Bind
|
||||||
|
struct sockaddr_in addr = {
|
||||||
|
.sin_family = AF_INET,
|
||||||
|
.sin_addr.s_addr = htonl(INADDR_ANY),
|
||||||
|
.sin_port = htons(g_ctrl.port)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bind(g_ctrl.server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||||
|
LOG_E(PHY, "[ANTENNA_CTRL] Failed to bind port %d: %s\n", g_ctrl.port, strerror(errno));
|
||||||
|
close(g_ctrl.server_fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen
|
||||||
|
if (listen(g_ctrl.server_fd, 1) < 0) {
|
||||||
|
LOG_E(PHY, "[ANTENNA_CTRL] Failed to listen: %s\n", strerror(errno));
|
||||||
|
close(g_ctrl.server_fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start server thread
|
||||||
|
g_ctrl.running = true;
|
||||||
|
if (pthread_create(&g_ctrl.thread, NULL, server_thread, NULL) != 0) {
|
||||||
|
LOG_E(PHY, "[ANTENNA_CTRL] Failed to create server thread\n");
|
||||||
|
g_ctrl.running = false;
|
||||||
|
close(g_ctrl.server_fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] Test server started on port %d\n", g_ctrl.port);
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] Connect with: nc localhost %d\n", g_ctrl.port);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nr_antenna_control_start(nr_antenna_state_t *ant_state, int port)
|
||||||
|
{
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] nr_antenna_control_start() is DEPRECATED\n");
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] Use nr_antenna_control_init_mplane() for O-RAN compliance\n");
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] or nr_antenna_control_start_test_server() for testing\n");
|
||||||
|
|
||||||
|
return nr_antenna_control_start_test_server(ant_state, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_antenna_control_stop(void)
|
||||||
|
{
|
||||||
|
if (!g_ctrl.running) return;
|
||||||
|
|
||||||
|
g_ctrl.running = false;
|
||||||
|
|
||||||
|
if (g_ctrl.backend == ANTENNA_CTRL_BACKEND_TEST_SOCKET) {
|
||||||
|
// Close server socket to unblock accept()
|
||||||
|
if (g_ctrl.server_fd >= 0) {
|
||||||
|
close(g_ctrl.server_fd);
|
||||||
|
g_ctrl.server_fd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for thread
|
||||||
|
pthread_join(g_ctrl.thread, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
g_ctrl.backend = ANTENNA_CTRL_BACKEND_NONE;
|
||||||
|
g_ctrl.ant_state = NULL;
|
||||||
|
g_ctrl.ru_session = NULL;
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] Control interface stopped\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nr_antenna_control_is_running(void)
|
||||||
|
{
|
||||||
|
return g_ctrl.running;
|
||||||
|
}
|
||||||
|
|
||||||
|
nr_antenna_ctrl_backend_t nr_antenna_control_get_backend(void)
|
||||||
|
{
|
||||||
|
return g_ctrl.backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nr_antenna_control_sync_to_ru(nr_antenna_state_t *ant_state)
|
||||||
|
{
|
||||||
|
#ifdef OAI_MPLANE
|
||||||
|
if (g_ctrl.backend != ANTENNA_CTRL_BACKEND_MPLANE || g_ctrl.ru_session == NULL) {
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] M-Plane backend not active, cannot sync to RU\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ant_state == NULL) {
|
||||||
|
ant_state = g_ctrl.ant_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ant_state == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send TX antenna mask to RU
|
||||||
|
antenna_ctrl_result_t result;
|
||||||
|
result = mplane_set_tx_antenna_mask(g_ctrl.ru_session,
|
||||||
|
ant_state->tx_array_name,
|
||||||
|
nr_get_tx_antenna_bitmask(ant_state),
|
||||||
|
ant_state->num_tx_antennas);
|
||||||
|
if (result != ANTENNA_CTRL_SUCCESS) {
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] Failed to sync TX mask: %s\n",
|
||||||
|
antenna_ctrl_result_str(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send RX antenna mask to RU
|
||||||
|
result = mplane_set_rx_antenna_mask(g_ctrl.ru_session,
|
||||||
|
ant_state->rx_array_name,
|
||||||
|
nr_get_rx_antenna_bitmask(ant_state),
|
||||||
|
ant_state->num_rx_antennas);
|
||||||
|
if (result != ANTENNA_CTRL_SUCCESS) {
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] Failed to sync RX mask: %s\n",
|
||||||
|
antenna_ctrl_result_str(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] Successfully synchronized antenna masks to RU\n");
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] M-Plane not compiled in\n");
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nr_antenna_control_sync_from_ru(nr_antenna_state_t *ant_state)
|
||||||
|
{
|
||||||
|
#ifdef OAI_MPLANE
|
||||||
|
if (g_ctrl.backend != ANTENNA_CTRL_BACKEND_MPLANE || g_ctrl.ru_session == NULL) {
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] M-Plane backend not active, cannot sync from RU\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ant_state == NULL) {
|
||||||
|
ant_state = g_ctrl.ant_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ant_state == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t tx_mask, rx_mask;
|
||||||
|
antenna_ctrl_result_t result;
|
||||||
|
|
||||||
|
// Get TX antenna mask from RU
|
||||||
|
result = mplane_get_tx_antenna_mask(g_ctrl.ru_session,
|
||||||
|
ant_state->tx_array_name,
|
||||||
|
&tx_mask);
|
||||||
|
if (result == ANTENNA_CTRL_SUCCESS) {
|
||||||
|
nr_set_tx_antenna_bitmask(ant_state, tx_mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get RX antenna mask from RU
|
||||||
|
result = mplane_get_rx_antenna_mask(g_ctrl.ru_session,
|
||||||
|
ant_state->rx_array_name,
|
||||||
|
&rx_mask);
|
||||||
|
if (result == ANTENNA_CTRL_SUCCESS) {
|
||||||
|
nr_set_rx_antenna_bitmask(ant_state, rx_mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_CTRL] Synchronized antenna masks from RU: TX=0x%02X, RX=0x%02X\n",
|
||||||
|
tx_mask, rx_mask);
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
LOG_W(PHY, "[ANTENNA_CTRL] M-Plane not compiled in\n");
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#ifndef NR_ANTENNA_CONTROL_H
|
||||||
|
#define NR_ANTENNA_CONTROL_H
|
||||||
|
|
||||||
|
#include "nr_antenna_state.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Default port for the test socket server (development mode only)
|
||||||
|
#define NR_ANTENNA_CONTROL_TEST_PORT 9999
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Antenna control backend type
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
ANTENNA_CTRL_BACKEND_NONE = 0, ///< No external control
|
||||||
|
ANTENNA_CTRL_BACKEND_MPLANE, ///< O-RAN M-Plane NETCONF (production)
|
||||||
|
ANTENNA_CTRL_BACKEND_TEST_SOCKET ///< Test socket interface (development)
|
||||||
|
} nr_antenna_ctrl_backend_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize antenna control with O-RAN M-Plane NETCONF backend
|
||||||
|
*
|
||||||
|
* This is the O-RAN compliant production interface. It connects the
|
||||||
|
* antenna state to a real NETCONF session to an O-RU.
|
||||||
|
*
|
||||||
|
* When antenna masks are changed via nr_set_tx_antenna_bitmask() or
|
||||||
|
* similar functions, the changes are automatically propagated to the
|
||||||
|
* RU via NETCONF edit-config operations.
|
||||||
|
*
|
||||||
|
* @param ant_state Pointer to the antenna state to control
|
||||||
|
* @param ru_session Active M-Plane NETCONF session (from radio/fhi_72/mplane/)
|
||||||
|
* @return true if initialization successful
|
||||||
|
*/
|
||||||
|
bool nr_antenna_control_init_mplane(nr_antenna_state_t *ant_state,
|
||||||
|
void *ru_session);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Start the test socket server (development mode only)
|
||||||
|
*
|
||||||
|
* Creates a simple TCP server for testing antenna control without
|
||||||
|
* NETCONF infrastructure. This is NOT O-RAN compliant.
|
||||||
|
*
|
||||||
|
* Commands supported (for backwards compatibility):
|
||||||
|
* MUTE TX <ant_id> - Mute TX antenna
|
||||||
|
* UNMUTE TX <ant_id> - Unmute TX antenna
|
||||||
|
* MUTE RX <ant_id> - Mute RX antenna
|
||||||
|
* UNMUTE RX <ant_id> - Unmute RX antenna
|
||||||
|
* SET TX MASK <hex> - Set TX antenna bitmask
|
||||||
|
* SET RX MASK <hex> - Set RX antenna bitmask
|
||||||
|
* STATUS - Get current antenna state
|
||||||
|
* STATS - Get energy saving statistics
|
||||||
|
* RESET STATS - Reset statistics
|
||||||
|
* QUIT - Close connection
|
||||||
|
*
|
||||||
|
* @param ant_state Pointer to the antenna state to control
|
||||||
|
* @param port TCP port to listen on (use 0 for default)
|
||||||
|
* @return true if server started successfully
|
||||||
|
*
|
||||||
|
* @note This function logs a warning that it's not O-RAN compliant
|
||||||
|
*/
|
||||||
|
bool nr_antenna_control_start_test_server(nr_antenna_state_t *ant_state, int port);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Legacy function - start antenna control server
|
||||||
|
*
|
||||||
|
* @deprecated Use nr_antenna_control_init_mplane() for O-RAN compliance
|
||||||
|
* or nr_antenna_control_start_test_server() for testing.
|
||||||
|
*
|
||||||
|
* This function starts the test socket server for backwards compatibility.
|
||||||
|
* It logs a deprecation warning.
|
||||||
|
*/
|
||||||
|
bool nr_antenna_control_start(nr_antenna_state_t *ant_state, int port);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Stop the antenna control interface
|
||||||
|
*
|
||||||
|
* Stops the test socket server if running.
|
||||||
|
* For M-Plane backend, this disconnects from NETCONF notifications.
|
||||||
|
*/
|
||||||
|
void nr_antenna_control_stop(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if antenna control is active
|
||||||
|
* @return true if control interface is running
|
||||||
|
*/
|
||||||
|
bool nr_antenna_control_is_running(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get current antenna control backend type
|
||||||
|
* @return Backend type currently in use
|
||||||
|
*/
|
||||||
|
nr_antenna_ctrl_backend_t nr_antenna_control_get_backend(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Synchronize local antenna state to RU via M-Plane NETCONF
|
||||||
|
*
|
||||||
|
* Sends the current antenna bitmask configuration to the RU.
|
||||||
|
* Only applicable when using the M-Plane NETCONF backend.
|
||||||
|
*
|
||||||
|
* @param ant_state Pointer to the antenna state
|
||||||
|
* @return true if synchronization successful or not applicable
|
||||||
|
*/
|
||||||
|
bool nr_antenna_control_sync_to_ru(nr_antenna_state_t *ant_state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Synchronize RU antenna state to local via M-Plane NETCONF
|
||||||
|
*
|
||||||
|
* Reads the current antenna bitmask from the RU and updates local state.
|
||||||
|
* Only applicable when using the M-Plane NETCONF backend.
|
||||||
|
*
|
||||||
|
* @param ant_state Pointer to the antenna state
|
||||||
|
* @return true if synchronization successful or not applicable
|
||||||
|
*/
|
||||||
|
bool nr_antenna_control_sync_from_ru(nr_antenna_state_t *ant_state);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* NR_ANTENNA_CONTROL_H */
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
#include "nr_antenna_state.h"
|
||||||
|
#include "common/utils/LOG/log.h"
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
// Helper to count set bits in a byte
|
||||||
|
static inline int popcount8(uint8_t x) {
|
||||||
|
int count = 0;
|
||||||
|
while (x) {
|
||||||
|
count += x & 1;
|
||||||
|
x >>= 1;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current timestamp in microseconds
|
||||||
|
static inline uint64_t get_timestamp_us(void) {
|
||||||
|
struct timespec ts;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||||
|
return (uint64_t)ts.tv_sec * 1000000ULL + (uint64_t)ts.tv_nsec / 1000ULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_init_antenna_state(nr_antenna_state_t *state,
|
||||||
|
uint8_t num_tx,
|
||||||
|
uint8_t num_rx,
|
||||||
|
const char *tx_array_name,
|
||||||
|
const char *rx_array_name)
|
||||||
|
{
|
||||||
|
if (state == NULL) return;
|
||||||
|
|
||||||
|
memset(state, 0, sizeof(nr_antenna_state_t));
|
||||||
|
|
||||||
|
state->num_tx_antennas = (num_tx > NR_MAX_ANTENNAS) ? NR_MAX_ANTENNAS : num_tx;
|
||||||
|
state->num_rx_antennas = (num_rx > NR_MAX_ANTENNAS) ? NR_MAX_ANTENNAS : num_rx;
|
||||||
|
|
||||||
|
// Initialize with all configured antennas active
|
||||||
|
// Create bitmask with num_tx/num_rx lowest bits set
|
||||||
|
state->tx_antenna_bitmask = (1 << state->num_tx_antennas) - 1;
|
||||||
|
state->rx_antenna_bitmask = (1 << state->num_rx_antennas) - 1;
|
||||||
|
|
||||||
|
// Set O-RAN array names
|
||||||
|
if (tx_array_name) {
|
||||||
|
strncpy(state->tx_array_name, tx_array_name, sizeof(state->tx_array_name) - 1);
|
||||||
|
} else {
|
||||||
|
strncpy(state->tx_array_name, "tx-array-0", sizeof(state->tx_array_name) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rx_array_name) {
|
||||||
|
strncpy(state->rx_array_name, rx_array_name, sizeof(state->rx_array_name) - 1);
|
||||||
|
} else {
|
||||||
|
strncpy(state->rx_array_name, "rx-array-0", sizeof(state->rx_array_name) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_init(&state->lock, NULL);
|
||||||
|
|
||||||
|
state->stats.last_update_timestamp = get_timestamp_us();
|
||||||
|
|
||||||
|
// Initialize recovery mode
|
||||||
|
state->recovery.in_recovery = false;
|
||||||
|
state->recovery.recovery_start_ts = 0;
|
||||||
|
state->recovery.recovery_duration_ms = 500; // 500ms default recovery time
|
||||||
|
|
||||||
|
// Protect antenna 0 by default (needed for SSB/PSS/SSS)
|
||||||
|
state->config.protect_antenna0 = true;
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Initialized: TX antennas=%d (mask=0x%02X, array=%s), "
|
||||||
|
"RX antennas=%d (mask=0x%02X, array=%s), antenna0_protected=%d\n",
|
||||||
|
state->num_tx_antennas, state->tx_antenna_bitmask, state->tx_array_name,
|
||||||
|
state->num_rx_antennas, state->rx_antenna_bitmask, state->rx_array_name,
|
||||||
|
state->config.protect_antenna0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_free_antenna_state(nr_antenna_state_t *state)
|
||||||
|
{
|
||||||
|
if (state == NULL) return;
|
||||||
|
pthread_mutex_destroy(&state->lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t nr_set_tx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask)
|
||||||
|
{
|
||||||
|
if (state == NULL) return 0;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&state->lock);
|
||||||
|
|
||||||
|
uint8_t old_bitmask = state->tx_antenna_bitmask;
|
||||||
|
|
||||||
|
// Mask to only valid antenna bits
|
||||||
|
uint8_t valid_mask = (1 << state->num_tx_antennas) - 1;
|
||||||
|
state->tx_antenna_bitmask = bitmask & valid_mask;
|
||||||
|
|
||||||
|
// Update statistics
|
||||||
|
if (state->tx_antenna_bitmask != old_bitmask) {
|
||||||
|
state->stats.tx_mute_events++;
|
||||||
|
state->stats.last_update_timestamp = get_timestamp_us();
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] TX bitmask changed: 0x%02X -> 0x%02X (array=%s, active=%d/%d)\n",
|
||||||
|
old_bitmask, state->tx_antenna_bitmask, state->tx_array_name,
|
||||||
|
popcount8(state->tx_antenna_bitmask), state->num_tx_antennas);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&state->lock);
|
||||||
|
|
||||||
|
return old_bitmask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t nr_set_rx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask)
|
||||||
|
{
|
||||||
|
if (state == NULL) return 0;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&state->lock);
|
||||||
|
|
||||||
|
uint8_t old_bitmask = state->rx_antenna_bitmask;
|
||||||
|
|
||||||
|
// Mask to only valid antenna bits
|
||||||
|
uint8_t valid_mask = (1 << state->num_rx_antennas) - 1;
|
||||||
|
state->rx_antenna_bitmask = bitmask & valid_mask;
|
||||||
|
|
||||||
|
// Update statistics
|
||||||
|
if (state->rx_antenna_bitmask != old_bitmask) {
|
||||||
|
state->stats.rx_mute_events++;
|
||||||
|
state->stats.last_update_timestamp = get_timestamp_us();
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] RX bitmask changed: 0x%02X -> 0x%02X (array=%s, active=%d/%d)\n",
|
||||||
|
old_bitmask, state->rx_antenna_bitmask, state->rx_array_name,
|
||||||
|
popcount8(state->rx_antenna_bitmask), state->num_rx_antennas);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&state->lock);
|
||||||
|
|
||||||
|
return old_bitmask;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t nr_get_tx_antenna_bitmask(const nr_antenna_state_t *state)
|
||||||
|
{
|
||||||
|
if (state == NULL) return 0xFF;
|
||||||
|
pthread_mutex_lock((pthread_mutex_t*)&state->lock);
|
||||||
|
uint8_t m = state->tx_antenna_bitmask;
|
||||||
|
pthread_mutex_unlock((pthread_mutex_t*)&state->lock);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t nr_get_rx_antenna_bitmask(const nr_antenna_state_t *state)
|
||||||
|
{
|
||||||
|
if (state == NULL) return 0xFF;
|
||||||
|
pthread_mutex_lock((pthread_mutex_t*)&state->lock);
|
||||||
|
uint8_t m = state->rx_antenna_bitmask;
|
||||||
|
pthread_mutex_unlock((pthread_mutex_t*)&state->lock);
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nr_is_tx_antenna_active(const nr_antenna_state_t *state, uint8_t ant_id)
|
||||||
|
{
|
||||||
|
if (state == NULL || ant_id >= state->num_tx_antennas) return false;
|
||||||
|
return (state->tx_antenna_bitmask & (1 << ant_id)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nr_is_rx_antenna_active(const nr_antenna_state_t *state, uint8_t ant_id)
|
||||||
|
{
|
||||||
|
if (state == NULL || ant_id >= state->num_rx_antennas) return false;
|
||||||
|
return (state->rx_antenna_bitmask & (1 << ant_id)) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int nr_get_active_tx_antenna_count(const nr_antenna_state_t *state)
|
||||||
|
{
|
||||||
|
if (state == NULL) return 0;
|
||||||
|
return popcount8(state->tx_antenna_bitmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nr_get_active_rx_antenna_count(const nr_antenna_state_t *state)
|
||||||
|
{
|
||||||
|
if (state == NULL) return 0;
|
||||||
|
return popcount8(state->rx_antenna_bitmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_mute_tx_antenna(nr_antenna_state_t *state, uint8_t ant_id)
|
||||||
|
{
|
||||||
|
if (state == NULL || ant_id >= state->num_tx_antennas) return;
|
||||||
|
|
||||||
|
// Protect antenna 0 if configured (needed for SSB/PSS/SSS transmission)
|
||||||
|
if (ant_id == 0 && state->config.protect_antenna0) {
|
||||||
|
LOG_W(PHY, "[ANTENNA_STATE] Cannot mute TX antenna 0: protected for SSB transmission\n");
|
||||||
|
LOG_W(PHY, "[ANTENNA_STATE] Use 'SET TX MASK' to override protection if needed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t new_mask = state->tx_antenna_bitmask & ~(1 << ant_id);
|
||||||
|
nr_set_tx_antenna_bitmask(state, new_mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_unmute_tx_antenna(nr_antenna_state_t *state, uint8_t ant_id)
|
||||||
|
{
|
||||||
|
if (state == NULL || ant_id >= state->num_tx_antennas) return;
|
||||||
|
|
||||||
|
uint8_t new_mask = state->tx_antenna_bitmask | (1 << ant_id);
|
||||||
|
nr_set_tx_antenna_bitmask(state, new_mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_mute_rx_antenna(nr_antenna_state_t *state, uint8_t ant_id)
|
||||||
|
{
|
||||||
|
if (state == NULL || ant_id >= state->num_rx_antennas) return;
|
||||||
|
|
||||||
|
uint8_t new_mask = state->rx_antenna_bitmask & ~(1 << ant_id);
|
||||||
|
nr_set_rx_antenna_bitmask(state, new_mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_unmute_rx_antenna(nr_antenna_state_t *state, uint8_t ant_id)
|
||||||
|
{
|
||||||
|
if (state == NULL || ant_id >= state->num_rx_antennas) return;
|
||||||
|
|
||||||
|
uint8_t new_mask = state->rx_antenna_bitmask | (1 << ant_id);
|
||||||
|
nr_set_rx_antenna_bitmask(state, new_mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
int nr_get_effective_mimo_layers(const nr_antenna_state_t *state, int requested_layers)
|
||||||
|
{
|
||||||
|
if (state == NULL)
|
||||||
|
return requested_layers;
|
||||||
|
|
||||||
|
// Reduce MIMO layers to match the number of active TX antennas.
|
||||||
|
// When antennas are muted, the RU blanks the corresponding TX buffers.
|
||||||
|
// If the scheduler still uses full layer count, the precoded signal is
|
||||||
|
// distributed across all antenna ports (including muted ones), which
|
||||||
|
// corrupts the UE's MIMO reception. By reducing layers to match active
|
||||||
|
// antennas, the PMI fallback to identity precoding maps layers 1:1 to
|
||||||
|
// active ports, avoiding signal loss on muted ports.
|
||||||
|
int active = nr_get_active_tx_antenna_count(state);
|
||||||
|
if (active > 0 && active < requested_layers)
|
||||||
|
return active;
|
||||||
|
return requested_layers;
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_log_antenna_state(const nr_antenna_state_t *state)
|
||||||
|
{
|
||||||
|
if (state == NULL) {
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] State is NULL\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] === Antenna State Report ===\n");
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] TX Array: %s\n", state->tx_array_name);
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Configured: %d antennas\n", state->num_tx_antennas);
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Bitmask: 0x%02X\n", state->tx_antenna_bitmask);
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Active: %d antennas\n", nr_get_active_tx_antenna_count(state));
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Individual: ");
|
||||||
|
for (int i = 0; i < state->num_tx_antennas; i++) {
|
||||||
|
LOG_I(PHY, "TX%d=%s ", i, nr_is_tx_antenna_active(state, i) ? "ON" : "OFF");
|
||||||
|
}
|
||||||
|
LOG_I(PHY, "\n");
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] RX Array: %s\n", state->rx_array_name);
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Configured: %d antennas\n", state->num_rx_antennas);
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Bitmask: 0x%02X\n", state->rx_antenna_bitmask);
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Active: %d antennas\n", nr_get_active_rx_antenna_count(state));
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Statistics:\n");
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] TX mute events: %lu\n", state->stats.tx_mute_events);
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] RX mute events: %lu\n", state->stats.rx_mute_events);
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Total muted slots: %lu\n", state->stats.total_muted_slots);
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] =============================\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_get_antenna_stats(const nr_antenna_state_t *state,
|
||||||
|
uint64_t *tx_mute_events,
|
||||||
|
uint64_t *rx_mute_events,
|
||||||
|
uint64_t *total_muted_slots)
|
||||||
|
{
|
||||||
|
if (state == NULL) return;
|
||||||
|
|
||||||
|
if (tx_mute_events) *tx_mute_events = state->stats.tx_mute_events;
|
||||||
|
if (rx_mute_events) *rx_mute_events = state->stats.rx_mute_events;
|
||||||
|
if (total_muted_slots) *total_muted_slots = state->stats.total_muted_slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_reset_antenna_stats(nr_antenna_state_t *state)
|
||||||
|
{
|
||||||
|
if (state == NULL) return;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&state->lock);
|
||||||
|
|
||||||
|
state->stats.tx_mute_events = 0;
|
||||||
|
state->stats.rx_mute_events = 0;
|
||||||
|
state->stats.total_muted_slots = 0;
|
||||||
|
state->stats.energy_saved_estimate = 0;
|
||||||
|
state->stats.last_update_timestamp = get_timestamp_us();
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Statistics reset\n");
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&state->lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_unmute_all_tx_antennas(nr_antenna_state_t *state)
|
||||||
|
{
|
||||||
|
if (state == NULL) return;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&state->lock);
|
||||||
|
|
||||||
|
uint8_t old_mask = state->tx_antenna_bitmask;
|
||||||
|
uint8_t all_on = (1 << state->num_tx_antennas) - 1;
|
||||||
|
state->tx_antenna_bitmask = all_on;
|
||||||
|
|
||||||
|
// Enter recovery mode if we were muted
|
||||||
|
if (old_mask != all_on) {
|
||||||
|
state->recovery.in_recovery = true;
|
||||||
|
state->recovery.recovery_start_ts = get_timestamp_us();
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Entering recovery mode for %u ms after unmuting TX antennas\n",
|
||||||
|
state->recovery.recovery_duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
state->stats.tx_mute_events++;
|
||||||
|
state->stats.last_update_timestamp = get_timestamp_us();
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] All TX antennas unmuted: 0x%02X -> 0x%02X (array=%s)\n",
|
||||||
|
old_mask, all_on, state->tx_array_name);
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&state->lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_unmute_all_rx_antennas(nr_antenna_state_t *state)
|
||||||
|
{
|
||||||
|
if (state == NULL) return;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&state->lock);
|
||||||
|
|
||||||
|
uint8_t old_mask = state->rx_antenna_bitmask;
|
||||||
|
uint8_t all_on = (1 << state->num_rx_antennas) - 1;
|
||||||
|
state->rx_antenna_bitmask = all_on;
|
||||||
|
|
||||||
|
state->stats.rx_mute_events++;
|
||||||
|
state->stats.last_update_timestamp = get_timestamp_us();
|
||||||
|
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] All RX antennas unmuted: 0x%02X -> 0x%02X (array=%s)\n",
|
||||||
|
old_mask, all_on, state->rx_array_name);
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&state->lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nr_is_in_recovery_mode(const nr_antenna_state_t *state)
|
||||||
|
{
|
||||||
|
if (state == NULL) return false;
|
||||||
|
|
||||||
|
if (!state->recovery.in_recovery) return false;
|
||||||
|
|
||||||
|
// Check if recovery period has elapsed
|
||||||
|
uint64_t now = get_timestamp_us();
|
||||||
|
uint64_t elapsed_us = now - state->recovery.recovery_start_ts;
|
||||||
|
uint64_t duration_us = (uint64_t)state->recovery.recovery_duration_ms * 1000ULL;
|
||||||
|
|
||||||
|
if (elapsed_us >= duration_us) {
|
||||||
|
// Recovery complete - cast away const for state update
|
||||||
|
nr_antenna_state_t *s = (nr_antenna_state_t *)state;
|
||||||
|
s->recovery.in_recovery = false;
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Recovery mode complete after %lu ms\n", elapsed_us / 1000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void nr_set_antenna0_protection(nr_antenna_state_t *state, bool protect)
|
||||||
|
{
|
||||||
|
if (state == NULL) return;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&state->lock);
|
||||||
|
state->config.protect_antenna0 = protect;
|
||||||
|
LOG_I(PHY, "[ANTENNA_STATE] Antenna 0 protection %s\n", protect ? "enabled" : "disabled");
|
||||||
|
pthread_mutex_unlock(&state->lock);
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
#ifndef NR_ANTENNA_STATE_H
|
||||||
|
#define NR_ANTENNA_STATE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/// Maximum number of physical antennas supported
|
||||||
|
#define NR_MAX_ANTENNAS 8
|
||||||
|
|
||||||
|
/// O-RAN compliant antenna state for TX/RX arrays
|
||||||
|
/// Mirrors o-ran-uplane-conf.yang: tx-array-antenna-mask-config
|
||||||
|
typedef struct {
|
||||||
|
/// Antenna bitmask per O-RAN WG4 spec: bit N = 1 means antenna N is active
|
||||||
|
/// Corresponds to o-ran-uplane-conf:antenna-bitmask
|
||||||
|
uint8_t tx_antenna_bitmask;
|
||||||
|
uint8_t rx_antenna_bitmask;
|
||||||
|
|
||||||
|
/// Total number of configured antennas
|
||||||
|
uint8_t num_tx_antennas;
|
||||||
|
uint8_t num_rx_antennas;
|
||||||
|
|
||||||
|
/// Thread safety for runtime updates
|
||||||
|
pthread_mutex_t lock;
|
||||||
|
|
||||||
|
/// Energy saving statistics (for research/monitoring)
|
||||||
|
struct {
|
||||||
|
uint64_t tx_mute_events; ///< Number of TX mute operations
|
||||||
|
uint64_t rx_mute_events; ///< Number of RX mute operations
|
||||||
|
uint64_t total_muted_slots; ///< Total slots with at least one antenna muted
|
||||||
|
uint64_t energy_saved_estimate; ///< Estimated energy saved (arbitrary units)
|
||||||
|
uint64_t last_update_timestamp; ///< Last update time (for O1 notifications)
|
||||||
|
} stats;
|
||||||
|
|
||||||
|
/// O-RAN compliance: array name for NETCONF identification
|
||||||
|
char tx_array_name[64];
|
||||||
|
char rx_array_name[64];
|
||||||
|
|
||||||
|
/// Recovery mode: after unmuting, allow CSI to update before using full capacity
|
||||||
|
struct {
|
||||||
|
bool in_recovery; ///< Currently in recovery mode
|
||||||
|
uint64_t recovery_start_ts; ///< Timestamp when recovery started (us)
|
||||||
|
uint32_t recovery_duration_ms; ///< Recovery duration in milliseconds (default 500ms)
|
||||||
|
} recovery;
|
||||||
|
|
||||||
|
/// Configuration flags
|
||||||
|
struct {
|
||||||
|
bool protect_antenna0; ///< Never mute antenna 0 (needed for SSB)
|
||||||
|
} config;
|
||||||
|
|
||||||
|
} nr_antenna_state_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize antenna state with all antennas active
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param num_tx Number of TX antennas
|
||||||
|
* @param num_rx Number of RX antennas
|
||||||
|
* @param tx_array_name O-RAN array name for TX (e.g., "tx-array-0")
|
||||||
|
* @param rx_array_name O-RAN array name for RX (e.g., "rx-array-0")
|
||||||
|
*/
|
||||||
|
void nr_init_antenna_state(nr_antenna_state_t *state,
|
||||||
|
uint8_t num_tx,
|
||||||
|
uint8_t num_rx,
|
||||||
|
const char *tx_array_name,
|
||||||
|
const char *rx_array_name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Free antenna state resources
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
*/
|
||||||
|
void nr_free_antenna_state(nr_antenna_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set TX antenna bitmask (O-RAN O1 interface equivalent)
|
||||||
|
*
|
||||||
|
* This function implements the O-RAN WG4 MPLANE-TRX-CONTROL feature.
|
||||||
|
* Corresponds to NETCONF edit-config on tx-array-antenna-mask-config.
|
||||||
|
*
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param bitmask Antenna bitmask (bit N = 1 means antenna N active)
|
||||||
|
* @return Previous bitmask value
|
||||||
|
*/
|
||||||
|
uint8_t nr_set_tx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set RX antenna bitmask (O-RAN O1 interface equivalent)
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param bitmask Antenna bitmask (bit N = 1 means antenna N active)
|
||||||
|
* @return Previous bitmask value
|
||||||
|
*/
|
||||||
|
uint8_t nr_set_rx_antenna_bitmask(nr_antenna_state_t *state, uint8_t bitmask);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get current TX antenna bitmask
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @return Current TX antenna bitmask
|
||||||
|
*/
|
||||||
|
uint8_t nr_get_tx_antenna_bitmask(const nr_antenna_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get current RX antenna bitmask
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @return Current RX antenna bitmask
|
||||||
|
*/
|
||||||
|
uint8_t nr_get_rx_antenna_bitmask(const nr_antenna_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if a specific TX antenna is active
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param ant_id Antenna index (0-based)
|
||||||
|
* @return true if antenna is active, false if muted
|
||||||
|
*/
|
||||||
|
bool nr_is_tx_antenna_active(const nr_antenna_state_t *state, uint8_t ant_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if a specific RX antenna is active
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param ant_id Antenna index (0-based)
|
||||||
|
* @return true if antenna is active, false if muted
|
||||||
|
*/
|
||||||
|
bool nr_is_rx_antenna_active(const nr_antenna_state_t *state, uint8_t ant_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get count of currently active TX antennas
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @return Number of active TX antennas
|
||||||
|
*/
|
||||||
|
int nr_get_active_tx_antenna_count(const nr_antenna_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get count of currently active RX antennas
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @return Number of active RX antennas
|
||||||
|
*/
|
||||||
|
int nr_get_active_rx_antenna_count(const nr_antenna_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Mute a specific TX antenna
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param ant_id Antenna index to mute (0-based)
|
||||||
|
*/
|
||||||
|
void nr_mute_tx_antenna(nr_antenna_state_t *state, uint8_t ant_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unmute a specific TX antenna
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param ant_id Antenna index to unmute (0-based)
|
||||||
|
*/
|
||||||
|
void nr_unmute_tx_antenna(nr_antenna_state_t *state, uint8_t ant_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Mute a specific RX antenna
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param ant_id Antenna index to mute (0-based)
|
||||||
|
*/
|
||||||
|
void nr_mute_rx_antenna(nr_antenna_state_t *state, uint8_t ant_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unmute a specific RX antenna
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param ant_id Antenna index to unmute (0-based)
|
||||||
|
*/
|
||||||
|
void nr_unmute_rx_antenna(nr_antenna_state_t *state, uint8_t ant_id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get effective number of MIMO layers based on active antennas
|
||||||
|
*
|
||||||
|
* For MIMO, the number of layers is limited by active antennas.
|
||||||
|
* This function returns min(requested_layers, active_antennas).
|
||||||
|
*
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param requested_layers Originally requested number of layers
|
||||||
|
* @return Effective number of layers that can be used
|
||||||
|
*/
|
||||||
|
int nr_get_effective_mimo_layers(const nr_antenna_state_t *state, int requested_layers);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Log antenna state for debugging
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
*/
|
||||||
|
void nr_log_antenna_state(const nr_antenna_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get energy saving statistics
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param tx_mute_events Output: number of TX mute events
|
||||||
|
* @param rx_mute_events Output: number of RX mute events
|
||||||
|
* @param total_muted_slots Output: total slots with muting
|
||||||
|
*/
|
||||||
|
void nr_get_antenna_stats(const nr_antenna_state_t *state,
|
||||||
|
uint64_t *tx_mute_events,
|
||||||
|
uint64_t *rx_mute_events,
|
||||||
|
uint64_t *total_muted_slots);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reset energy saving statistics
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
*/
|
||||||
|
void nr_reset_antenna_stats(nr_antenna_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unmute all TX antennas and enter recovery mode
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
*
|
||||||
|
* This initiates a recovery period during which the scheduler
|
||||||
|
* uses conservative settings to allow UE CSI reports to update.
|
||||||
|
*/
|
||||||
|
void nr_unmute_all_tx_antennas(nr_antenna_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unmute all RX antennas
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
*/
|
||||||
|
void nr_unmute_all_rx_antennas(nr_antenna_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if system is in recovery mode after unmuting
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @return true if in recovery mode
|
||||||
|
*/
|
||||||
|
bool nr_is_in_recovery_mode(const nr_antenna_state_t *state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set antenna 0 protection (SSB antenna)
|
||||||
|
* @param state Pointer to antenna state structure
|
||||||
|
* @param protect true to prevent muting antenna 0
|
||||||
|
*/
|
||||||
|
void nr_set_antenna0_protection(nr_antenna_state_t *state, bool protect);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* NR_ANTENNA_STATE_H */
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "common/utils/threadPool/task_ans.h"
|
#include "common/utils/threadPool/task_ans.h"
|
||||||
#include "openair1/PHY/defs_RU.h"
|
#include "openair1/PHY/defs_RU.h"
|
||||||
#include "common/utils/ds/spsc_q.h"
|
#include "common/utils/ds/spsc_q.h"
|
||||||
|
#include "PHY/NR_TRANSPORT/nr_antenna_state.h"
|
||||||
|
|
||||||
#define MAX_NUM_RU_PER_gNB 8
|
#define MAX_NUM_RU_PER_gNB 8
|
||||||
#define MAX_PUCCH0_NID 8
|
#define MAX_PUCCH0_NID 8
|
||||||
@@ -430,6 +431,8 @@ typedef struct PHY_VARS_gNB_s {
|
|||||||
|
|
||||||
bool enable_analog_das;
|
bool enable_analog_das;
|
||||||
|
|
||||||
|
nr_antenna_state_t antenna_state;
|
||||||
|
|
||||||
time_stats_t l1_tx_proc;
|
time_stats_t l1_tx_proc;
|
||||||
time_stats_t l1_rx_proc;
|
time_stats_t l1_rx_proc;
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,14 @@
|
|||||||
|
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
|
static inline int count_nnz_c16(const c16_t *v, int n)
|
||||||
|
{
|
||||||
|
int nnz = 0;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
nnz += (v[i].r != 0) || (v[i].i != 0);
|
||||||
|
return nnz;
|
||||||
|
}
|
||||||
|
|
||||||
// RU OFDM Modulator gNodeB
|
// RU OFDM Modulator gNodeB
|
||||||
// OFDM modulation core routine, generates a first_symbol to first_symbol+num_symbols on a particular slot and TX antenna port
|
// OFDM modulation core routine, generates a first_symbol to first_symbol+num_symbols on a particular slot and TX antenna port
|
||||||
void nr_feptx0(RU_t *ru, int tti_tx, int first_symbol, int num_symbols, int aa)
|
void nr_feptx0(RU_t *ru, int tti_tx, int first_symbol, int num_symbols, int aa)
|
||||||
@@ -165,14 +173,22 @@ void nr_feptx_prec(RU_t *ru, int frame_tx, int slot_tx)
|
|||||||
int Ptx = cfg->carrier_config.num_tx_ant.value;
|
int Ptx = cfg->carrier_config.num_tx_ant.value;
|
||||||
// If there is no digital beamforming we just need to copy the data to RU
|
// If there is no digital beamforming we just need to copy the data to RU
|
||||||
if (ru->config.dbt_config.num_dig_beams == 0 || ru->gNB_list[0]->common_vars.analog_bf) {
|
if (ru->config.dbt_config.num_dig_beams == 0 || ru->gNB_list[0]->common_vars.analog_bf) {
|
||||||
|
const nr_antenna_state_t *ant_state = &gNB->antenna_state;
|
||||||
for (int b = 0; b < ru->num_beams_period; b++) {
|
for (int b = 0; b < ru->num_beams_period; b++) {
|
||||||
for (int i = 0; i < Ptx; ++i) {
|
for (int i = 0; i < Ptx; ++i) {
|
||||||
int tx_idx = i + b * ru->nb_tx;
|
int tx_idx = i + b * ru->nb_tx;
|
||||||
|
// Check antenna muting state for energy saving
|
||||||
|
if (!nr_is_tx_antenna_active(ant_state, i)) {
|
||||||
|
// Antenna is muted - zero out the buffer (transmission blanking)
|
||||||
|
memset((void *)ru->common.txdataF_BF[tx_idx], 0, fp->samples_per_slot_wCP * sizeof(int32_t));
|
||||||
|
LOG_D(PHY, "[ANTENNA_MUTE] TX antenna %d muted in nr_feptx_prec, blanking slot %d\n", i, slot_tx);
|
||||||
|
} else {
|
||||||
memcpy((void *)ru->common.txdataF_BF[tx_idx],
|
memcpy((void *)ru->common.txdataF_BF[tx_idx],
|
||||||
(void *)gNB->common_vars.txdataF[b][i],
|
(void *)gNB->common_vars.txdataF[b][i],
|
||||||
fp->samples_per_slot_wCP * sizeof(int32_t));
|
fp->samples_per_slot_wCP * sizeof(int32_t));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
AssertFatal(false, "This needs to be fixed by using appropriate beams from config\n");
|
AssertFatal(false, "This needs to be fixed by using appropriate beams from config\n");
|
||||||
}
|
}
|
||||||
@@ -205,8 +221,14 @@ void nr_feptx(void *arg)
|
|||||||
|
|
||||||
// If there is no digital beamforming we just need to copy the data to RU
|
// If there is no digital beamforming we just need to copy the data to RU
|
||||||
if (ru->config.dbt_config.num_dig_beams == 0 || ru->gNB_list[0]->common_vars.analog_bf) {
|
if (ru->config.dbt_config.num_dig_beams == 0 || ru->gNB_list[0]->common_vars.analog_bf) {
|
||||||
// FFT shift
|
|
||||||
const NR_DL_FRAME_PARMS *fp = &ru->gNB_list[0]->frame_parms;
|
const NR_DL_FRAME_PARMS *fp = &ru->gNB_list[0]->frame_parms;
|
||||||
|
const nr_antenna_state_t *ant_state = &ru->gNB_list[0]->antenna_state;
|
||||||
|
if (!nr_is_tx_antenna_active(ant_state, aa)) {
|
||||||
|
// Antenna is muted - zero out the FFT-shift output (transmission blanking)
|
||||||
|
memset((void *)&ru->common.txdataF_BF[tx_idx][startSymbol * fp->ofdm_symbol_size], 0,
|
||||||
|
numSymbols * fp->ofdm_symbol_size * sizeof(int32_t));
|
||||||
|
} else {
|
||||||
|
// FFT shift
|
||||||
fft_shift(ru->gNB_list[0]->common_vars.txdataF[bb][aa],
|
fft_shift(ru->gNB_list[0]->common_vars.txdataF[bb][aa],
|
||||||
fp->ofdm_symbol_size,
|
fp->ofdm_symbol_size,
|
||||||
fp->N_RB_DL,
|
fp->N_RB_DL,
|
||||||
@@ -214,10 +236,23 @@ void nr_feptx(void *arg)
|
|||||||
fp->ofdm_symbol_size,
|
fp->ofdm_symbol_size,
|
||||||
startSymbol,
|
startSymbol,
|
||||||
numSymbols);
|
numSymbols);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
AssertFatal(false, "This needs to be fixed by using appropriate beams from config\n");
|
AssertFatal(false, "This needs to be fixed by using appropriate beams from config\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* const c16_t *dst = (const c16_t *)&ru->common.txdataF_BF[tx_idx][txdataF_BF_offset]; */
|
||||||
|
/* const int dst_nnz = count_nnz_c16(dst, numSamples); */
|
||||||
|
/* if (startSymbol == 0 && ru->proc.frame_tx % 10 == 0 && dst_nnz != 0) { */
|
||||||
|
/* const int dst_pwr_db = dB_fixed(signal_energy_nodc((c16_t *)dst, 2 * numSamples)); */
|
||||||
|
|
||||||
|
/* LOG_I(PHY, */
|
||||||
|
/* "[TXF_PWR] frame=%d slot=%d beam=%d aa=%d tx_idx=%d " */
|
||||||
|
/* "DST pwr=%d dB nnz=%d\n", */
|
||||||
|
/* ru->proc.frame_tx, slot, bb, aa, tx_idx, dst_pwr_db, dst_nnz); */
|
||||||
|
/* } */
|
||||||
|
|
||||||
|
|
||||||
if (tx_idx == 0)
|
if (tx_idx == 0)
|
||||||
stop_meas(&ru->precoding_stats);
|
stop_meas(&ru->precoding_stats);
|
||||||
|
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ static bool allocate_dl_retransmission(gNB_MAC_INST *nr_mac,
|
|||||||
NR_UE_DL_BWP_t *dl_bwp = &UE->current_DL_BWP;
|
NR_UE_DL_BWP_t *dl_bwp = &UE->current_DL_BWP;
|
||||||
NR_UE_UL_BWP_t *ul_bwp = &UE->current_UL_BWP;
|
NR_UE_UL_BWP_t *ul_bwp = &UE->current_UL_BWP;
|
||||||
NR_sched_pdsch_t new_sched = sched_ctrl->harq_processes[current_harq_pid].sched_pdsch;
|
NR_sched_pdsch_t new_sched = sched_ctrl->harq_processes[current_harq_pid].sched_pdsch;
|
||||||
int layers = get_dl_nrOfLayers(sched_ctrl, dl_bwp->dci_format);
|
int layers = get_dl_nrOfLayers(nr_mac, sched_ctrl, dl_bwp->dci_format);
|
||||||
int pm_index = get_pm_index(nr_mac, UE, dl_bwp->dci_format, layers, nr_mac->radio_config.pdsch_AntennaPorts.XP);
|
int pm_index = get_pm_index(nr_mac, UE, dl_bwp->dci_format, layers, nr_mac->radio_config.pdsch_AntennaPorts.XP);
|
||||||
|
|
||||||
// If the RI changed between current rtx and a previous transmission
|
// If the RI changed between current rtx and a previous transmission
|
||||||
@@ -565,7 +565,6 @@ static bool allocate_dl_retransmission(gNB_MAC_INST *nr_mac,
|
|||||||
static void ack_reconfig(gNB_MAC_INST *mac, NR_UE_info_t *UE)
|
static void ack_reconfig(gNB_MAC_INST *mac, NR_UE_info_t *UE)
|
||||||
{
|
{
|
||||||
if (!UE->reconfigCellGroup) {
|
if (!UE->reconfigCellGroup) {
|
||||||
LOG_W(NR_MAC, "Received ACK for RRCReconfiguration, but nothing to apply!\n");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ASN_STRUCT_FREE(asn_DEF_NR_CellGroupConfig, UE->CellGroup);
|
ASN_STRUCT_FREE(asn_DEF_NR_CellGroupConfig, UE->CellGroup);
|
||||||
@@ -594,8 +593,57 @@ typedef struct UEsched_s {
|
|||||||
float coef;
|
float coef;
|
||||||
NR_UE_info_t * UE;
|
NR_UE_info_t * UE;
|
||||||
int selected_mcs;
|
int selected_mcs;
|
||||||
|
bool mumimo_paired; /* nakaolab: already allocated as MU-MIMO partner; main loop must skip */
|
||||||
} UEsched_t;
|
} UEsched_t;
|
||||||
|
|
||||||
|
/* nakaolab: cached MU-MIMO enable state.
|
||||||
|
* Read once via OAI_MU_MIMO env var. 0 = SU-MIMO (default), 1 = MU-MIMO co-scheduling. */
|
||||||
|
static int mumimo_enabled(void)
|
||||||
|
{
|
||||||
|
static int cached = -1;
|
||||||
|
if (cached < 0) {
|
||||||
|
const char *e = getenv("OAI_MU_MIMO");
|
||||||
|
cached = (e && atoi(e) > 0) ? 1 : 0;
|
||||||
|
if (cached)
|
||||||
|
LOG_W(NR_MAC, "[nakaolab] MU-MIMO downlink scheduling ENABLED (OAI_MU_MIMO=1)\n");
|
||||||
|
}
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* nakaolab: orthogonal-PMI test for 4-port Type I rank-1 codebook (N1=2, N2=1).
|
||||||
|
*
|
||||||
|
* The 4 codewords sharing the same spatial beam (same pmi_x1) are constructed
|
||||||
|
* with mutually orthogonal co-phasings (theta_n in {1, j, -1, -j}):
|
||||||
|
* w_(x1, n) = (1/sqrt(4)) * [v_x1 ; j^n * v_x1]
|
||||||
|
* Pairwise inner product <w_(x1,a), w_(x1,b)> for any a != b is zero, so all
|
||||||
|
* 4 codewords with the same x1 are mutually orthogonal. Up to 4 UEs (one per
|
||||||
|
* x2 class) can be co-scheduled simultaneously.
|
||||||
|
*
|
||||||
|
* Two UEs with different x1 use different spatial beams. With OAI default
|
||||||
|
* oversampling (O1=4) the DFT vectors are not strictly orthogonal, so we
|
||||||
|
* conservatively don't pair across x1.
|
||||||
|
*
|
||||||
|
* OAI_MU_MIMO_RELAXED=1 accepts any rank-1 pair (used to validate the
|
||||||
|
* scheduler path on a symmetric rfsim channel where PMIs collapse). */
|
||||||
|
static bool pmi_is_orthogonal(const NR_UE_info_t *a, const NR_UE_info_t *b)
|
||||||
|
{
|
||||||
|
const struct CRI_RI_LI_PMI_CQI *ra = &a->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report;
|
||||||
|
const struct CRI_RI_LI_PMI_CQI *rb = &b->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report;
|
||||||
|
if (ra->ri != 0 || rb->ri != 0)
|
||||||
|
return false;
|
||||||
|
static int relaxed = -1;
|
||||||
|
if (relaxed < 0) {
|
||||||
|
const char *e = getenv("OAI_MU_MIMO_RELAXED");
|
||||||
|
relaxed = (e && atoi(e) > 0) ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (relaxed)
|
||||||
|
return true;
|
||||||
|
if (ra->pmi_x1 != rb->pmi_x1)
|
||||||
|
return false;
|
||||||
|
/* Same beam, distinct co-phasing -> mutually orthogonal codewords. */
|
||||||
|
return ra->pmi_x2 != rb->pmi_x2;
|
||||||
|
}
|
||||||
|
|
||||||
static int comparator(const void *p, const void *q)
|
static int comparator(const void *p, const void *q)
|
||||||
{
|
{
|
||||||
const UEsched_t *pp = p;
|
const UEsched_t *pp = p;
|
||||||
@@ -700,7 +748,7 @@ static void pf_dl(gNB_MAC_INST *mac,
|
|||||||
sched_ctrl->dl_bler_stats.mcs = selected_mcs;
|
sched_ctrl->dl_bler_stats.mcs = selected_mcs;
|
||||||
} else
|
} else
|
||||||
selected_mcs = get_mcs_from_bler(bo, stats, &sched_ctrl->dl_bler_stats, max_mcs, frame);
|
selected_mcs = get_mcs_from_bler(bo, stats, &sched_ctrl->dl_bler_stats, max_mcs, frame);
|
||||||
int l = get_dl_nrOfLayers(sched_ctrl, current_BWP->dci_format);
|
int l = get_dl_nrOfLayers(mac, sched_ctrl, current_BWP->dci_format);
|
||||||
const uint8_t Qm = nr_get_Qm_dl(selected_mcs, current_BWP->mcsTableIdx);
|
const uint8_t Qm = nr_get_Qm_dl(selected_mcs, current_BWP->mcsTableIdx);
|
||||||
const uint16_t R = nr_get_code_rate_dl(selected_mcs, current_BWP->mcsTableIdx);
|
const uint16_t R = nr_get_code_rate_dl(selected_mcs, current_BWP->mcsTableIdx);
|
||||||
uint32_t tbs = nr_compute_tbs(Qm,
|
uint32_t tbs = nr_compute_tbs(Qm,
|
||||||
@@ -730,12 +778,31 @@ static void pf_dl(gNB_MAC_INST *mac,
|
|||||||
|
|
||||||
qsort(UE_sched, numUE, sizeof(UEsched_t), comparator);
|
qsort(UE_sched, numUE, sizeof(UEsched_t), comparator);
|
||||||
UEsched_t *iterator = UE_sched;
|
UEsched_t *iterator = UE_sched;
|
||||||
|
/* nakaolab: tag to mark UEs already paired in this pf_dl call. Increments each
|
||||||
|
* call so a UE's stored tag != current tag means "available for pairing". */
|
||||||
|
static uint64_t mumimo_pf_call_id = 0;
|
||||||
|
if (mumimo_enabled())
|
||||||
|
mumimo_pf_call_id++;
|
||||||
|
/* nakaolab: diagnostic — log numUE > 1 to understand MU-MIMO opportunity */
|
||||||
|
if (mumimo_enabled() && numUE >= 2) {
|
||||||
|
static int dump_count = 0;
|
||||||
|
if (dump_count++ < 20)
|
||||||
|
LOG_W(NR_MAC, "[nakaolab MU-MIMO] %4d.%2d numUE=%d (PF candidates)\n", frame, slot, numUE);
|
||||||
|
}
|
||||||
|
|
||||||
const int min_rbSize = 5;
|
const int min_rbSize = 5;
|
||||||
|
|
||||||
/* Loop UE_sched to find max coeff and allocate transmission */
|
/* Loop UE_sched to find max coeff and allocate transmission */
|
||||||
while (iterator->UE != NULL) {
|
while (iterator->UE != NULL) {
|
||||||
|
|
||||||
|
/* nakaolab: skip UEs already allocated as a MU-MIMO partner of an earlier UE
|
||||||
|
* in this same pf_dl call (mumimo_pf_call_id matches). */
|
||||||
|
if (mumimo_enabled() &&
|
||||||
|
iterator->UE->UE_sched_ctrl.mumimo_paired_slot == (int64_t)mumimo_pf_call_id) {
|
||||||
|
iterator++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
NR_UE_sched_ctrl_t *sched_ctrl = &iterator->UE->UE_sched_ctrl;
|
NR_UE_sched_ctrl_t *sched_ctrl = &iterator->UE->UE_sched_ctrl;
|
||||||
const uint16_t rnti = iterator->UE->rnti;
|
const uint16_t rnti = iterator->UE->rnti;
|
||||||
|
|
||||||
@@ -837,7 +904,7 @@ static void pf_dl(gNB_MAC_INST *mac,
|
|||||||
sched_ctrl->cce_index = CCEIndex;
|
sched_ctrl->cce_index = CCEIndex;
|
||||||
fill_pdcch_vrb_map(mac, CC_id, &sched_ctrl->sched_pdcch, CCEIndex, sched_ctrl->aggregation_level, beam.idx);
|
fill_pdcch_vrb_map(mac, CC_id, &sched_ctrl->sched_pdcch, CCEIndex, sched_ctrl->aggregation_level, beam.idx);
|
||||||
|
|
||||||
int l = get_dl_nrOfLayers(sched_ctrl, dl_bwp->dci_format);
|
int l = get_dl_nrOfLayers(mac, sched_ctrl, dl_bwp->dci_format);
|
||||||
NR_sched_pdsch_t sched_pdsch = {
|
NR_sched_pdsch_t sched_pdsch = {
|
||||||
// rbSize below
|
// rbSize below
|
||||||
.rbStart = rbStart,
|
.rbStart = rbStart,
|
||||||
@@ -878,7 +945,179 @@ static void pf_dl(gNB_MAC_INST *mac,
|
|||||||
&sched_pdsch.tb_size,
|
&sched_pdsch.tb_size,
|
||||||
&sched_pdsch.rbSize);
|
&sched_pdsch.rbSize);
|
||||||
|
|
||||||
|
/* nakaolab MU-MIMO: find up to MAX_MU_MIMO_PARTNERS orthogonal-PMI partner UEs
|
||||||
|
* that can ride on the same RB allocation. Each partner gets its own DCI/CCE,
|
||||||
|
* its own DMRS port (1000+layer_idx), and is forced to the leader's MCS/TBS.
|
||||||
|
* Up to 4 UEs total per slot (one per orthogonal x2 class). */
|
||||||
|
#define MAX_MU_MIMO_PARTNERS 3
|
||||||
|
NR_UE_info_t *partners[MAX_MU_MIMO_PARTNERS] = {NULL};
|
||||||
|
int partner_CCEs[MAX_MU_MIMO_PARTNERS] = {-1, -1, -1};
|
||||||
|
int partner_allocs[MAX_MU_MIMO_PARTNERS] = {-1, -1, -1};
|
||||||
|
int n_partners = 0;
|
||||||
|
|
||||||
|
/* nakaolab Phase 4: cap MU-MIMO group size to the active TX antenna count.
|
||||||
|
* When the antenna controller mutes some TX chains (e.g., 4->2 or 4->1), the
|
||||||
|
* effective number of orthogonal codewords in the codebook shrinks. Each
|
||||||
|
* co-scheduled UE consumes one spatial layer = one TX port. So:
|
||||||
|
* active_tx = 4 -> up to 4 UEs (3 partners), 4-way MU-MIMO
|
||||||
|
* active_tx = 2 -> up to 2 UEs (1 partner), 2-way MU-MIMO
|
||||||
|
* active_tx = 1 -> SU-MIMO only (0 partners) */
|
||||||
|
int max_partners_now = MAX_MU_MIMO_PARTNERS;
|
||||||
|
if (mac->antenna_state) {
|
||||||
|
const uint8_t tx_mask = nr_get_tx_antenna_bitmask(mac->antenna_state);
|
||||||
|
int active_tx = __builtin_popcount(tx_mask);
|
||||||
|
if (active_tx < 1) active_tx = 1;
|
||||||
|
if (active_tx - 1 < max_partners_now)
|
||||||
|
max_partners_now = active_tx - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mumimo_enabled() && remainUEs[beam.idx] > 0 && max_partners_now > 0) {
|
||||||
|
const uint8_t leader_x2 = iterator->UE->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
|
||||||
|
uint8_t classes_used = 1 << leader_x2;
|
||||||
|
static int diag_count = 0;
|
||||||
|
bool log_diag = ((diag_count++ % 200) == 0);
|
||||||
|
if (log_diag) {
|
||||||
|
int n_cand = 0;
|
||||||
|
UE_iterator (UE_list, c) {
|
||||||
|
if (c != iterator->UE && nr_mac_ue_is_active(c) &&
|
||||||
|
c->UE_sched_ctrl.available_dl_harq.head >= 0)
|
||||||
|
n_cand++;
|
||||||
|
}
|
||||||
|
LOG_W(NR_MAC, "[mumimo-diag] %4d.%2d ENTER partner-search after RNTI %04x leader_x2=%d; eligible_others=%d\n",
|
||||||
|
frame, slot, iterator->UE->rnti, leader_x2, n_cand);
|
||||||
|
}
|
||||||
|
/* Find up to max_partners_now partners, one per remaining x2 class.
|
||||||
|
* max_partners_now is capped by active TX antenna count (Phase 4). */
|
||||||
|
while (n_partners < max_partners_now && remainUEs[beam.idx] > 0) {
|
||||||
|
NR_UE_info_t *new_partner = NULL;
|
||||||
|
int new_CCE = -1;
|
||||||
|
int new_alloc = -1;
|
||||||
|
NR_beam_alloc_t new_pbeam = {0};
|
||||||
|
UE_iterator (UE_list, cand_ue) {
|
||||||
|
if (cand_ue == iterator->UE) continue;
|
||||||
|
if (cand_ue->UE_sched_ctrl.mumimo_paired_slot == (int64_t)mumimo_pf_call_id) continue;
|
||||||
|
/* Skip if already a partner of this leader. */
|
||||||
|
bool already_partner = false;
|
||||||
|
for (int i = 0; i < n_partners; i++)
|
||||||
|
if (partners[i] == cand_ue) { already_partner = true; break; }
|
||||||
|
if (already_partner) continue;
|
||||||
|
/* Skip if cand's class is already represented (leader or earlier partner). */
|
||||||
|
const uint8_t cand_x2 = cand_ue->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
|
||||||
|
if (classes_used & (1 << cand_x2)) continue;
|
||||||
|
if (!nr_mac_ue_is_active(cand_ue)) continue;
|
||||||
|
if (cand_ue->UE_sched_ctrl.available_dl_harq.head < 0) continue;
|
||||||
|
update_dlsch_buffer(pp_pdsch->frame, pp_pdsch->slot, cand_ue);
|
||||||
|
if (!dlsch_to_schedule(&cand_ue->UE_sched_ctrl)) continue;
|
||||||
|
if (!pmi_is_orthogonal(iterator->UE, cand_ue)) continue;
|
||||||
|
NR_beam_alloc_t pbeam = beam_allocation_procedure(&mac->beam_info, frame, slot,
|
||||||
|
cand_ue->UE_beam_index,
|
||||||
|
slots_per_frame);
|
||||||
|
if (pbeam.idx != beam.idx) {
|
||||||
|
reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index,
|
||||||
|
slots_per_frame, pbeam.new_beam);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int p_CCE = get_cce_index(mac, CC_id, slot, cand_ue->rnti,
|
||||||
|
&cand_ue->UE_sched_ctrl.aggregation_level,
|
||||||
|
pbeam.idx,
|
||||||
|
cand_ue->UE_sched_ctrl.search_space,
|
||||||
|
cand_ue->UE_sched_ctrl.coreset,
|
||||||
|
&cand_ue->UE_sched_ctrl.sched_pdcch,
|
||||||
|
cand_ue->UE_sched_ctrl.pdcch_cl_adjust);
|
||||||
|
if (p_CCE < 0) {
|
||||||
|
reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index,
|
||||||
|
slots_per_frame, pbeam.new_beam);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int p_alloc = -1;
|
||||||
|
if (!get_FeedbackDisabled(cand_ue->sc_info.downlinkHARQ_FeedbackDisabled_r17,
|
||||||
|
cand_ue->UE_sched_ctrl.available_dl_harq.head)) {
|
||||||
|
int p_pucch = nr_get_pucch_resource(cand_ue->UE_sched_ctrl.coreset,
|
||||||
|
cand_ue->current_UL_BWP.pucch_Config, p_CCE);
|
||||||
|
p_alloc = nr_acknack_scheduling(mac, cand_ue, frame, slot,
|
||||||
|
cand_ue->UE_beam_index, p_pucch, 0);
|
||||||
|
if (p_alloc < 0) {
|
||||||
|
reset_beam_status(&mac->beam_info, frame, slot, cand_ue->UE_beam_index,
|
||||||
|
slots_per_frame, pbeam.new_beam);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_partner = cand_ue;
|
||||||
|
new_CCE = p_CCE;
|
||||||
|
new_alloc = p_alloc;
|
||||||
|
new_pbeam = pbeam;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (new_partner == NULL) break;
|
||||||
|
partners[n_partners] = new_partner;
|
||||||
|
partner_CCEs[n_partners] = new_CCE;
|
||||||
|
partner_allocs[n_partners] = new_alloc;
|
||||||
|
const uint8_t p_x2 = new_partner->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
|
||||||
|
classes_used |= 1 << p_x2;
|
||||||
|
n_partners++;
|
||||||
|
(void)new_pbeam; /* beam already taken via beam_allocation_procedure */
|
||||||
|
}
|
||||||
|
if (n_partners > 0) {
|
||||||
|
/* Compose pairing log: leader + partners */
|
||||||
|
char pair_str[256];
|
||||||
|
int off = snprintf(pair_str, sizeof(pair_str), "RNTI %04x (x2=%d)",
|
||||||
|
iterator->UE->rnti, leader_x2);
|
||||||
|
for (int i = 0; i < n_partners && off < (int)sizeof(pair_str) - 32; i++)
|
||||||
|
off += snprintf(pair_str + off, sizeof(pair_str) - off, " + RNTI %04x (x2=%d)",
|
||||||
|
partners[i]->rnti,
|
||||||
|
partners[i]->UE_sched_ctrl.CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2);
|
||||||
|
LOG_W(NR_MAC, "[nakaolab MU-MIMO] %4d.%2d %d-way pair %s on RBs [%d..%d]\n",
|
||||||
|
frame, slot, n_partners + 1, pair_str,
|
||||||
|
sched_pdsch.rbStart, sched_pdsch.rbStart + sched_pdsch.rbSize - 1);
|
||||||
|
/* MU-MIMO needs DMRS pilots from both CDM groups protected from data. */
|
||||||
|
sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
post_process_dlsch(mac, pp_pdsch, iterator->UE, &sched_pdsch);
|
post_process_dlsch(mac, pp_pdsch, iterator->UE, &sched_pdsch);
|
||||||
|
if (mumimo_enabled())
|
||||||
|
iterator->UE->UE_sched_ctrl.mumimo_paired_slot = (int64_t)mumimo_pf_call_id;
|
||||||
|
|
||||||
|
/* nakaolab: allocate each MU-MIMO partner on the same RBs.
|
||||||
|
* DMRS port assignment: leader = port 1000 (mask bit 0), partner_i = port
|
||||||
|
* 1001+i (mask bit i+1). For 4-way: 1000 (CDM group 0), 1001 (CDM group 0),
|
||||||
|
* 1002 (CDM group 1), 1003 (CDM group 1). */
|
||||||
|
for (int i = 0; i < n_partners; i++) {
|
||||||
|
NR_UE_info_t *p_ue = partners[i];
|
||||||
|
NR_UE_sched_ctrl_t *p_sched_ctrl = &p_ue->UE_sched_ctrl;
|
||||||
|
NR_UE_DL_BWP_t *p_dl_bwp = &p_ue->current_DL_BWP;
|
||||||
|
|
||||||
|
p_sched_ctrl->cce_index = partner_CCEs[i];
|
||||||
|
fill_pdcch_vrb_map(mac, CC_id, &p_sched_ctrl->sched_pdcch, partner_CCEs[i],
|
||||||
|
p_sched_ctrl->aggregation_level, beam.idx);
|
||||||
|
|
||||||
|
const int p_l = 1;
|
||||||
|
NR_sched_pdsch_t p_sched_pdsch = {
|
||||||
|
.rbStart = sched_pdsch.rbStart,
|
||||||
|
.rbSize = sched_pdsch.rbSize,
|
||||||
|
.mcs = sched_pdsch.mcs,
|
||||||
|
.R = sched_pdsch.R,
|
||||||
|
.Qm = sched_pdsch.Qm,
|
||||||
|
.tb_size = sched_pdsch.tb_size,
|
||||||
|
.dl_harq_pid = p_sched_ctrl->available_dl_harq.head,
|
||||||
|
.pucch_allocation = partner_allocs[i],
|
||||||
|
.pm_index = get_pm_index(mac, p_ue, p_dl_bwp->dci_format, p_l,
|
||||||
|
mac->radio_config.pdsch_AntennaPorts.XP),
|
||||||
|
.nrOfLayers = p_l,
|
||||||
|
.bwp_info = get_pdsch_bwp_start_size(mac, p_ue),
|
||||||
|
.dmrs_parms = get_dl_dmrs_params(scc, p_dl_bwp, &tda_info, p_l),
|
||||||
|
.time_domain_allocation = tda,
|
||||||
|
.tda_info = tda_info,
|
||||||
|
.action = NULL,
|
||||||
|
};
|
||||||
|
/* Partner i (i in 0..2) uses DMRS port 1001+i: mask bit (i+1). */
|
||||||
|
p_sched_pdsch.dmrs_parms.dmrs_ports_id = 1u << (i + 1);
|
||||||
|
p_sched_pdsch.dmrs_parms.numDmrsCdmGrpsNoData = 2;
|
||||||
|
|
||||||
|
post_process_dlsch(mac, pp_pdsch, p_ue, &p_sched_pdsch);
|
||||||
|
p_sched_ctrl->mumimo_paired_slot = (int64_t)mumimo_pf_call_id;
|
||||||
|
remainUEs[beam.idx]--;
|
||||||
|
}
|
||||||
|
|
||||||
/* transmissions: directly allocate */
|
/* transmissions: directly allocate */
|
||||||
n_rb_sched[beam.idx] -= sched_pdsch.rbSize;
|
n_rb_sched[beam.idx] -= sched_pdsch.rbSize;
|
||||||
|
|||||||
@@ -101,14 +101,13 @@ int get_ssbidx_from_beam(gNB_MAC_INST *mac, int beam_idx)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t get_dl_nrOfLayers(const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format)
|
uint8_t get_dl_nrOfLayers(const gNB_MAC_INST *mac, const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format)
|
||||||
{
|
{
|
||||||
// TODO check this but it should be enough for now
|
int layers = (dci_format == NR_DL_DCI_FORMAT_1_0) ? 1
|
||||||
// if there is not csi report activated RI is 0 from initialization
|
: sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri + 1;
|
||||||
if(dci_format == NR_DL_DCI_FORMAT_1_0)
|
if (mac && mac->antenna_state)
|
||||||
return 1;
|
return nr_get_effective_mimo_layers(mac->antenna_state, layers);
|
||||||
else
|
return layers;
|
||||||
return sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri + 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int get_ul_nrOfLayers(const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format)
|
int get_ul_nrOfLayers(const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format)
|
||||||
@@ -168,13 +167,30 @@ uint16_t get_pm_index(const gNB_MAC_INST *nrmac,
|
|||||||
if (dci_format == NR_DL_DCI_FORMAT_1_0 || nrmac->identity_pm || xp_pdsch_antenna_ports == 1)
|
if (dci_format == NR_DL_DCI_FORMAT_1_0 || nrmac->identity_pm || xp_pdsch_antenna_ports == 1)
|
||||||
return 0; //identity matrix (basic 5G configuration handled by PMI report is with XP antennas)
|
return 0; //identity matrix (basic 5G configuration handled by PMI report is with XP antennas)
|
||||||
const NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl;
|
const NR_UE_sched_ctrl_t *sched_ctrl = &UE->UE_sched_ctrl;
|
||||||
|
|
||||||
|
int reported_ri_layers = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri + 1;
|
||||||
|
if (layers != reported_ri_layers) {
|
||||||
|
LOG_D(NR_MAC, "PMI mismatch: using %d layers but RI report is for %d layers, using identity matrix\n",
|
||||||
|
layers, reported_ri_layers);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
nr_antenna_state_t* ant_state = nrmac->antenna_state;
|
||||||
|
if (ant_state) {
|
||||||
|
if (nr_is_in_recovery_mode(ant_state)) {
|
||||||
|
LOG_D(NR_MAC, "[ANTENNA_MUTE] Recovery mode active, using identity matrix\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Don't fall back to identity matrix for muted antennas.
|
||||||
|
// Antenna muting is handled at RU level (nr_feptx zeros muted antenna buffers).
|
||||||
|
// Using proper precoding ensures data reaches all active antennas.
|
||||||
|
}
|
||||||
|
|
||||||
const int report_id = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.csi_report_id;
|
const int report_id = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.csi_report_id;
|
||||||
const nr_csi_report_t *csi_report = &UE->csi_report_template[report_id];
|
const nr_csi_report_t *csi_report = &UE->csi_report_template[report_id];
|
||||||
const int N1 = csi_report->N1;
|
const int N1 = csi_report->N1;
|
||||||
const int N2 = csi_report->N2;
|
const int N2 = csi_report->N2;
|
||||||
const int antenna_ports = (N1 * N2) << 1;
|
const int antenna_ports = (N1 * N2) << 1;
|
||||||
if (antenna_ports < 2)
|
if (antenna_ports < 2) return 0; // single antenna port
|
||||||
return 0; // single antenna port
|
|
||||||
|
|
||||||
int x1 = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1;
|
int x1 = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.pmi_x1;
|
||||||
const int x2 = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
|
const int x2 = sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.pmi_x2;
|
||||||
@@ -189,8 +205,9 @@ uint16_t get_pm_index(const gNB_MAC_INST *nrmac,
|
|||||||
// order of matrices depends on layers to be transmitted
|
// order of matrices depends on layers to be transmitted
|
||||||
// elements from 1 to n for 1 layer
|
// elements from 1 to n for 1 layer
|
||||||
// elements from n+1 to m for 2 layers etc.
|
// elements from n+1 to m for 2 layers etc.
|
||||||
|
int pm_index;
|
||||||
if (antenna_ports == 2)
|
if (antenna_ports == 2)
|
||||||
return 1 + prev_layers_size + x2; // 0 for identity matrix
|
pm_index = 1 + prev_layers_size + x2; // 0 for identity matrix
|
||||||
else {
|
else {
|
||||||
int idx = layers - 1;
|
int idx = layers - 1;
|
||||||
// the order of i1x in X1 report needs to be verified
|
// the order of i1x in X1 report needs to be verified
|
||||||
@@ -220,8 +237,15 @@ uint16_t get_pm_index(const gNB_MAC_INST *nrmac,
|
|||||||
get_K1_K2(N1, N2, &K1, &K2, layers);
|
get_K1_K2(N1, N2, &K1, &K2, layers);
|
||||||
// computing precoding matrix index according to rule set in allocation function init_codebook_gNB
|
// computing precoding matrix index according to rule set in allocation function init_codebook_gNB
|
||||||
lay_index = i2 + (i11 * max_i2) + (i12 * max_i2 * N1 * O1) + (k1 * max_i2 * N1 * O1 * N2 * O2) + (k2 * max_i2 * N1 * O1 * N2 * O2 * K1);
|
lay_index = i2 + (i11 * max_i2) + (i12 * max_i2 * N1 * O1) + (k1 * max_i2 * N1 * O1 * N2 * O2) + (k2 * max_i2 * N1 * O1 * N2 * O2 * K1);
|
||||||
return 1 + prev_layers_size + lay_index;
|
pm_index = 1 + prev_layers_size + lay_index;
|
||||||
}
|
}
|
||||||
|
int max_valid_pm = prev_layers_size + nrmac->precoding_matrix_size[layers - 1];
|
||||||
|
if (pm_index < 1 || pm_index > max_valid_pm) {
|
||||||
|
LOG_W(NR_MAC, "PM index %d out of range [1, %d] for %d layers, using identity matrix\n",
|
||||||
|
pm_index, max_valid_pm, layers);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return pm_index;
|
||||||
}
|
}
|
||||||
|
|
||||||
// look-up table for AMC. Based on BLER vs SNR curves from the nr_dlsim simulation
|
// look-up table for AMC. Based on BLER vs SNR curves from the nr_dlsim simulation
|
||||||
|
|||||||
@@ -497,7 +497,9 @@ static void evaluate_sinr_report(NR_UE_info_t *UE,
|
|||||||
stats->num_sinr_meas++;
|
stats->num_sinr_meas++;
|
||||||
|
|
||||||
const int mcs_table = UE->current_DL_BWP.mcsTableIdx;
|
const int mcs_table = UE->current_DL_BWP.mcsTableIdx;
|
||||||
const int nrOfLayers = get_dl_nrOfLayers(sched_ctrl, UE->current_DL_BWP.dci_format);
|
/* NULL mac arg: SINR-based MCS estimation skips antenna-state rank cap
|
||||||
|
(caller doesn't have nrmac in scope; this is only for max-MCS hint). */
|
||||||
|
const int nrOfLayers = get_dl_nrOfLayers(NULL, sched_ctrl, UE->current_DL_BWP.dci_format);
|
||||||
sched_ctrl->dl_max_mcs = get_mcs_from_SINRx10(mcs_table, sinr_report->r[0].SINRx10, nrOfLayers);
|
sched_ctrl->dl_max_mcs = get_mcs_from_SINRx10(mcs_table, sinr_report->r[0].SINRx10, nrOfLayers);
|
||||||
|
|
||||||
LOG_D(MAC, "Reported SSB-SINR = %01f, dl_max_mcs %d\n", sinr_report->r[0].SINRx10 / 10.0, sched_ctrl->dl_max_mcs);
|
LOG_D(MAC, "Reported SSB-SINR = %01f, dl_max_mcs %d\n", sinr_report->r[0].SINRx10 / 10.0, sched_ctrl->dl_max_mcs);
|
||||||
@@ -761,8 +763,20 @@ static void extract_pucch_csi_report(NR_CSI_MeasConfig_t *csi_MeasConfig,
|
|||||||
evaluate_cri_report(payload, cri_bitlen, cumul_bits, sched_ctrl);
|
evaluate_cri_report(payload, cri_bitlen, cumul_bits, sched_ctrl);
|
||||||
cumul_bits += cri_bitlen;
|
cumul_bits += cri_bitlen;
|
||||||
ri_bitlen = csi_report->csi_meas_bitlen.ri_bitlen;
|
ri_bitlen = csi_report->csi_meas_bitlen.ri_bitlen;
|
||||||
if (ri_bitlen)
|
if (ri_bitlen) {
|
||||||
r_index = evaluate_ri_report(payload, ri_bitlen, csi_report->csi_meas_bitlen.ri_restriction, cumul_bits, sched_ctrl);
|
r_index = evaluate_ri_report(payload, ri_bitlen, csi_report->csi_meas_bitlen.ri_restriction, cumul_bits, sched_ctrl);
|
||||||
|
} else {
|
||||||
|
/* nakaolab: ri_bitlen=0 (single RI allowed via ri_restriction).
|
||||||
|
* Pick that RI directly — without this, r_index stays -1 and trips
|
||||||
|
* the DevAssert(ri>=0) in evaluate_cqi_report. */
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
if ((csi_report->csi_meas_bitlen.ri_restriction >> i) & 0x01) {
|
||||||
|
r_index = i;
|
||||||
|
sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
cumul_bits += ri_bitlen;
|
cumul_bits += ri_bitlen;
|
||||||
if (r_index != -1)
|
if (r_index != -1)
|
||||||
skip_zero_padding(&cumul_bits, csi_report, r_index, bitlen);
|
skip_zero_padding(&cumul_bits, csi_report, r_index, bitlen);
|
||||||
@@ -775,8 +789,20 @@ static void extract_pucch_csi_report(NR_CSI_MeasConfig_t *csi_MeasConfig,
|
|||||||
evaluate_cri_report(payload, cri_bitlen, cumul_bits, sched_ctrl);
|
evaluate_cri_report(payload, cri_bitlen, cumul_bits, sched_ctrl);
|
||||||
cumul_bits += cri_bitlen;
|
cumul_bits += cri_bitlen;
|
||||||
ri_bitlen = csi_report->csi_meas_bitlen.ri_bitlen;
|
ri_bitlen = csi_report->csi_meas_bitlen.ri_bitlen;
|
||||||
if (ri_bitlen)
|
if (ri_bitlen) {
|
||||||
r_index = evaluate_ri_report(payload, ri_bitlen, csi_report->csi_meas_bitlen.ri_restriction, cumul_bits, sched_ctrl);
|
r_index = evaluate_ri_report(payload, ri_bitlen, csi_report->csi_meas_bitlen.ri_restriction, cumul_bits, sched_ctrl);
|
||||||
|
} else {
|
||||||
|
/* nakaolab: ri_bitlen=0 (single RI allowed via ri_restriction).
|
||||||
|
* Pick that RI directly — without this, r_index stays -1 and trips
|
||||||
|
* the DevAssert(ri>=0) in evaluate_cqi_report. */
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
if ((csi_report->csi_meas_bitlen.ri_restriction >> i) & 0x01) {
|
||||||
|
r_index = i;
|
||||||
|
sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
cumul_bits += ri_bitlen;
|
cumul_bits += ri_bitlen;
|
||||||
if (r_index != -1)
|
if (r_index != -1)
|
||||||
skip_zero_padding(&cumul_bits, csi_report, r_index, bitlen);
|
skip_zero_padding(&cumul_bits, csi_report, r_index, bitlen);
|
||||||
@@ -792,8 +818,20 @@ static void extract_pucch_csi_report(NR_CSI_MeasConfig_t *csi_MeasConfig,
|
|||||||
evaluate_cri_report(payload, cri_bitlen, cumul_bits, sched_ctrl);
|
evaluate_cri_report(payload, cri_bitlen, cumul_bits, sched_ctrl);
|
||||||
cumul_bits += cri_bitlen;
|
cumul_bits += cri_bitlen;
|
||||||
ri_bitlen = csi_report->csi_meas_bitlen.ri_bitlen;
|
ri_bitlen = csi_report->csi_meas_bitlen.ri_bitlen;
|
||||||
if (ri_bitlen)
|
if (ri_bitlen) {
|
||||||
r_index = evaluate_ri_report(payload, ri_bitlen, csi_report->csi_meas_bitlen.ri_restriction, cumul_bits, sched_ctrl);
|
r_index = evaluate_ri_report(payload, ri_bitlen, csi_report->csi_meas_bitlen.ri_restriction, cumul_bits, sched_ctrl);
|
||||||
|
} else {
|
||||||
|
/* nakaolab: ri_bitlen=0 (single RI allowed via ri_restriction).
|
||||||
|
* Pick that RI directly — without this, r_index stays -1 and trips
|
||||||
|
* the DevAssert(ri>=0) in evaluate_cqi_report. */
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
if ((csi_report->csi_meas_bitlen.ri_restriction >> i) & 0x01) {
|
||||||
|
r_index = i;
|
||||||
|
sched_ctrl->CSI_report.cri_ri_li_pmi_cqi_report.ri = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
cumul_bits += ri_bitlen;
|
cumul_bits += ri_bitlen;
|
||||||
li_bitlen = evaluate_li_report(payload, csi_report, cumul_bits, r_index, sched_ctrl);
|
li_bitlen = evaluate_li_report(payload, csi_report, cumul_bits, r_index, sched_ctrl);
|
||||||
cumul_bits += li_bitlen;
|
cumul_bits += li_bitlen;
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ uint16_t get_pm_index(const gNB_MAC_INST *nrmac,
|
|||||||
int get_mcs_from_SINRx10(int mcs_table, int SINRx10, int Nl);
|
int get_mcs_from_SINRx10(int mcs_table, int SINRx10, int Nl);
|
||||||
uint8_t get_mcs_from_cqi(int mcs_table, int cqi_table, int cqi_idx);
|
uint8_t get_mcs_from_cqi(int mcs_table, int cqi_table, int cqi_idx);
|
||||||
|
|
||||||
uint8_t get_dl_nrOfLayers(const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format);
|
uint8_t get_dl_nrOfLayers(const gNB_MAC_INST *mac, const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format);
|
||||||
int get_ul_nrOfLayers(const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format);
|
int get_ul_nrOfLayers(const NR_UE_sched_ctrl_t *sched_ctrl, const nr_dci_format_t dci_format);
|
||||||
|
|
||||||
void free_sched_pucch_list(NR_UE_sched_ctrl_t *sched_ctrl);
|
void free_sched_pucch_list(NR_UE_sched_ctrl_t *sched_ctrl);
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
#include "LAYER2/NR_MAC_COMMON/nr_mac_common.h"
|
#include "LAYER2/NR_MAC_COMMON/nr_mac_common.h"
|
||||||
#include "LAYER2/NR_MAC_gNB/mac_config.h"
|
#include "LAYER2/NR_MAC_gNB/mac_config.h"
|
||||||
#include "NR_TAG.h"
|
#include "NR_TAG.h"
|
||||||
|
#include "PHY/NR_TRANSPORT/nr_antenna_state.h"
|
||||||
|
|
||||||
/* Defs */
|
/* Defs */
|
||||||
#define MAX_NUM_BWP 5
|
#define MAX_NUM_BWP 5
|
||||||
@@ -663,6 +664,10 @@ typedef struct {
|
|||||||
int release_timer;
|
int release_timer;
|
||||||
CSI_report_t CSI_report;
|
CSI_report_t CSI_report;
|
||||||
bool SR;
|
bool SR;
|
||||||
|
/// nakaolab MU-MIMO: encoded frame*1000+slot when this UE was last allocated as
|
||||||
|
/// a MU-MIMO partner (or leader). -1 = never. Lets pf_dl skip UEs already paired
|
||||||
|
/// in the current slot when scanning connected_ue_list for partners.
|
||||||
|
int64_t mumimo_paired_slot;
|
||||||
/// information about every HARQ process
|
/// information about every HARQ process
|
||||||
NR_UE_harq_t harq_processes[NR_MAX_HARQ_PROCESSES];
|
NR_UE_harq_t harq_processes[NR_MAX_HARQ_PROCESSES];
|
||||||
/// HARQ processes that are free
|
/// HARQ processes that are free
|
||||||
@@ -965,6 +970,10 @@ typedef struct gNB_MAC_INST_s {
|
|||||||
nr_mac_config_t radio_config;
|
nr_mac_config_t radio_config;
|
||||||
nr_rlc_configuration_t rlc_config;
|
nr_rlc_configuration_t rlc_config;
|
||||||
|
|
||||||
|
/// Pointer to PHY antenna state (port mute mask) — set in scheduler init,
|
||||||
|
/// used by get_dl_nrOfLayers to cap MIMO rank to active TX port count.
|
||||||
|
nr_antenna_state_t *antenna_state;
|
||||||
|
|
||||||
NR_sched_ctrl_sib1_t *sched_ctrlSIB1;
|
NR_sched_ctrl_sib1_t *sched_ctrlSIB1;
|
||||||
NR_sched_pdcch_t *sched_pdcch_otherSI;
|
NR_sched_pdcch_t *sched_pdcch_otherSI;
|
||||||
uint16_t cset0_bwp_start;
|
uint16_t cset0_bwp_start;
|
||||||
|
|||||||
@@ -1324,7 +1324,29 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t,
|
|||||||
rxAddInput(input, temp_array[aarx], aarx, ptr->channel_model, nsamps);
|
rxAddInput(input, temp_array[aarx], aarx, ptr->channel_model, nsamps);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (is_first_beam && is_first_peer && (ptr->nbAnt == 1 || nbAnt == 1)) {
|
/* nakaolab MU-MIMO: when OAI_UE_CHANNEL_CLASS is set in the env, the UE
|
||||||
|
* applies a deterministic rank-1 channel whose dominant right-singular-vector
|
||||||
|
* matches Type I 4-port codebook entry w_class. This makes the UE report a
|
||||||
|
* stable PMI = class, giving the gNB scheduler natural PMI diversity across
|
||||||
|
* UEs assigned to different classes. Four classes give four mutually-orthogonal
|
||||||
|
* codewords, supporting up to 4-way MU-MIMO.
|
||||||
|
*
|
||||||
|
* Codeword w_k[a] for 4-port rank-1 (Type I, N1=2, N2=1):
|
||||||
|
* w_0 = [1, 1, 1, 1]/2
|
||||||
|
* w_1 = [1, j, -1, -j]/2
|
||||||
|
* w_2 = [1, -1, 1, -1]/2
|
||||||
|
* w_3 = [1, -j, -1, j]/2
|
||||||
|
* The 4x4 channel H^(k) is rank-1: H^(k)[r,a] = w_k[a]* (per-row constant), so
|
||||||
|
* the dominant right singular vector is w_k. */
|
||||||
|
static int ue_class_cached = -2;
|
||||||
|
if (ue_class_cached == -2) {
|
||||||
|
const char *env = getenv("OAI_UE_CHANNEL_CLASS");
|
||||||
|
ue_class_cached = (env && atoi(env) >= 0 && atoi(env) < 4) ? atoi(env) : -1;
|
||||||
|
if (ue_class_cached >= 0)
|
||||||
|
LOG_W(HW, "[nakaolab MU-MIMO] rfsim DL channel: orthogonal class %d\n", ue_class_cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_first_beam && is_first_peer && (ptr->nbAnt == 1 || nbAnt == 1) && ue_class_cached < 0) {
|
||||||
// optimization: The buffer is uninitialized so samples can be written directly in the buffer
|
// optimization: The buffer is uninitialized so samples can be written directly in the buffer
|
||||||
combine_received_beams(t, ptr->received_packets, timestamp - t->chan_offset, 1, nsamps, rx_beam_id, samples);
|
combine_received_beams(t, ptr->received_packets, timestamp - t->chan_offset, 1, nsamps, rx_beam_id, samples);
|
||||||
} else {
|
} else {
|
||||||
@@ -1334,6 +1356,52 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t,
|
|||||||
input[aatx] = ant_buffers[aatx].data();
|
input[aatx] = ant_buffers[aatx].data();
|
||||||
}
|
}
|
||||||
combine_received_beams(t, ptr->received_packets, timestamp - t->chan_offset, ptr->nbAnt, nsamps, rx_beam_id, input);
|
combine_received_beams(t, ptr->received_packets, timestamp - t->chan_offset, ptr->nbAnt, nsamps, rx_beam_id, input);
|
||||||
|
|
||||||
|
if (ue_class_cached >= 0 && ant_buffers.size() == 4) {
|
||||||
|
/* nakaolab MU-MIMO: per-UE channel that combines the default symmetric
|
||||||
|
* rfsim kernel with a class-specific TX-antenna weighting w_k[a]:
|
||||||
|
*
|
||||||
|
* H^(k)[r, a] = H_orig[r,a] * w_k[a]
|
||||||
|
*
|
||||||
|
* The default kernel H_orig[r,a] = (a==r ? 1.0 : 0.2/|a-r|) is full-rank,
|
||||||
|
* preserving SSB and reference-signal sync (signal reaches all RX). The
|
||||||
|
* class-specific weighting rotates each TX antenna's contribution
|
||||||
|
* differently per class, so the optimal PMI for class k is exactly
|
||||||
|
* codeword w_k from the Type I 4-port codebook (modulo diagonal scaling).
|
||||||
|
*
|
||||||
|
* w_0 = [1, 1, 1, 1]/2
|
||||||
|
* w_1 = [1, j, -1, -j]/2
|
||||||
|
* w_2 = [1, -1, 1, -1]/2
|
||||||
|
* w_3 = [1, -j, -1, j]/2 */
|
||||||
|
static const double W_RE[4][4] = {
|
||||||
|
{ 0.5, 0.5, 0.5, 0.5 },
|
||||||
|
{ 0.5, 0.0, -0.5, 0.0 },
|
||||||
|
{ 0.5, -0.5, 0.5, -0.5 },
|
||||||
|
{ 0.5, 0.0, -0.5, 0.0 }
|
||||||
|
};
|
||||||
|
static const double W_IM[4][4] = {
|
||||||
|
{ 0.0, 0.0, 0.0, 0.0 },
|
||||||
|
{ 0.0, 0.5, 0.0, -0.5 },
|
||||||
|
{ 0.0, 0.0, 0.0, 0.0 },
|
||||||
|
{ 0.0, -0.5, 0.0, 0.5 }
|
||||||
|
};
|
||||||
|
const int k = ue_class_cached;
|
||||||
|
for (int aarx = 0; aarx < nbAnt; aarx++) {
|
||||||
|
for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) {
|
||||||
|
const uint32_t ant_diff = (uint32_t)std::abs(aatx - aarx);
|
||||||
|
const double h_orig = ant_diff ? (0.2 / ant_diff) : 1.0;
|
||||||
|
/* Effective channel coeff: h_orig * w_k[a] (complex multiply). */
|
||||||
|
const double hre = h_orig * W_RE[k][aatx];
|
||||||
|
const double him = h_orig * W_IM[k][aatx];
|
||||||
|
for (int i = 0; i < nsamps; i++) {
|
||||||
|
const double xr = ant_buffers[aatx][i].r;
|
||||||
|
const double xi = ant_buffers[aatx][i].i;
|
||||||
|
samples[aarx][i].r += (int16_t)(hre * xr - him * xi);
|
||||||
|
samples[aarx][i].i += (int16_t)(hre * xi + him * xr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
for (int aarx = 0; aarx < nbAnt; aarx++) {
|
for (int aarx = 0; aarx < nbAnt; aarx++) {
|
||||||
double H_awgn_mimo_coeff[ant_buffers.size()];
|
double H_awgn_mimo_coeff[ant_buffers.size()];
|
||||||
for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) {
|
for (int aatx = 0; aatx < (int)ant_buffers.size(); aatx++) {
|
||||||
@@ -1351,6 +1419,7 @@ static void rfsimulator_read_internal(rfsimulator_state_t *t,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
is_first_peer = false;
|
is_first_peer = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user