Files
openairinterface5g/openair1/PHY/NR_TRANSPORT/nr_antenna_control.c
T
2026-09-19 23:28:08 +09:00

736 lines
26 KiB
C

/*
* 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>
#include <time.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;
int64_t tx_lock_until_ms[NR_MAX_ANTENNAS];
pthread_mutex_t command_lock;
pthread_mutex_t clients_lock;
pthread_cond_t clients_done;
unsigned int client_count;
#ifdef OAI_MPLANE
ru_session_t *ru_session;
#else
void *ru_session;
#endif
} g_ctrl = {0};
static bool valid_tsm_duration(int seconds)
{
return seconds == 0 || seconds == 15 || seconds == 30 ||
seconds == 45 || seconds == 60;
}
static int64_t monotonic_ms(void)
{
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000;
}
static int64_t tx_lock_remaining_ms(int ant_id)
{
if (ant_id < 0 || ant_id >= NR_MAX_ANTENNAS)
return 0;
int64_t remaining = g_ctrl.tx_lock_until_ms[ant_id] - monotonic_ms();
if (remaining <= 0) {
g_ctrl.tx_lock_until_ms[ant_id] = 0;
return 0;
}
return remaining;
}
static void set_tx_lock(int ant_id, int seconds)
{
if (ant_id < 0 || ant_id >= NR_MAX_ANTENNAS)
return;
g_ctrl.tx_lock_until_ms[ant_id] =
seconds > 0 ? monotonic_ms() + (int64_t)seconds * 1000 : 0;
}
static void clear_all_tx_locks(void)
{
memset(g_ctrl.tx_lock_until_ms, 0, sizeof(g_ctrl.tx_lock_until_ms));
}
// ============================================================================
// 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 = -1;
int duration = 0;
int fields = sscanf(p + 8, "%d %d", &ant_id, &duration);
if (fields < 1 || ant_id < 0 || ant_id >= g_ctrl.ant_state->num_tx_antennas) {
snprintf(response, resp_size, "ERROR: Invalid TX antenna id\n");
return;
}
if (fields >= 2 && !valid_tsm_duration(duration)) {
snprintf(response, resp_size,
"ERROR: Unsupported TSM duration %d; use 15, 30, 45, or 60 seconds\n",
duration);
return;
}
nr_mute_tx_antenna(g_ctrl.ant_state, ant_id);
set_tx_lock(ant_id, duration);
snprintf(response, resp_size, "OK: TX antenna %d muted (TSM=%ds)\n",
ant_id, duration);
}
else if (strncmp(p, "UNMUTE TX ", 10) == 0) {
int ant_id = -1;
char option[16] = {0};
int fields = sscanf(p + 10, "%d %15s", &ant_id, option);
if (fields < 1 || ant_id < 0 || ant_id >= g_ctrl.ant_state->num_tx_antennas) {
snprintf(response, resp_size, "ERROR: Invalid TX antenna id\n");
return;
}
bool force = fields >= 2 && strcmp(option, "FORCE") == 0;
int64_t remaining = tx_lock_remaining_ms(ant_id);
if (remaining > 0 && !force) {
snprintf(response, resp_size,
"ERROR: TX antenna %d locked for %ld more ms\n",
ant_id, (long)remaining);
return;
}
set_tx_lock(ant_id, 0);
nr_unmute_tx_antenna(g_ctrl.ant_state, ant_id);
snprintf(response, resp_size, "OK: TX antenna %d unmuted%s\n",
ant_id, force ? " (emergency override)" : "");
}
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;
int duration = 0;
char option[16] = {0};
int fields = sscanf(p + 12, "%x %d %15s", &mask, &duration, option);
if (fields < 1) {
snprintf(response, resp_size, "ERROR: Invalid mask format. Use hex (e.g., 0F or 0x0F)\n");
return;
}
if (fields >= 2 && !valid_tsm_duration(duration)) {
snprintf(response, resp_size,
"ERROR: Unsupported TSM duration %d; use 15, 30, 45, or 60 seconds\n",
duration);
return;
}
bool force = fields >= 3 && strcmp(option, "FORCE") == 0;
uint8_t valid_mask = (1u << g_ctrl.ant_state->num_tx_antennas) - 1u;
uint8_t requested = (uint8_t)mask & valid_mask;
uint8_t old_requested = nr_get_requested_tx_antenna_bitmask(g_ctrl.ant_state);
uint8_t waking = requested & ~old_requested;
for (int ant_id = 0; ant_id < g_ctrl.ant_state->num_tx_antennas; ++ant_id) {
if ((waking & (1u << ant_id)) && tx_lock_remaining_ms(ant_id) > 0 && !force) {
snprintf(response, resp_size,
"ERROR: TX antenna %d is still TSM-locked\n", ant_id);
return;
}
}
nr_request_tx_antenna_bitmask(g_ctrl.ant_state, requested);
uint8_t muted = old_requested & ~requested;
for (int ant_id = 0; ant_id < g_ctrl.ant_state->num_tx_antennas; ++ant_id) {
uint8_t bit = 1u << ant_id;
if (muted & bit)
set_tx_lock(ant_id, duration);
if (waking & bit)
set_tx_lock(ant_id, 0);
}
bool pending = nr_get_pending_tx_antenna_bitmask(g_ctrl.ant_state, NULL);
uint8_t active = nr_get_tx_antenna_bitmask(g_ctrl.ant_state);
snprintf(response, resp_size,
"OK: TX mask 0x%02X -> 0x%02X (active=0x%02X%s, TSM=%ds%s)\n",
old_requested, requested, active,
pending ? ", HARQ drain pending" : "",
duration, force ? ", emergency override" : "");
}
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);
uint8_t requested_mask = nr_get_requested_tx_antenna_bitmask(g_ctrl.ant_state);
bool pending = nr_get_pending_tx_antenna_bitmask(g_ctrl.ant_state, NULL);
snprintf(response, resp_size,
"=== Antenna Status ===\n"
"TX Array: %s\n"
" Antennas: %d configured, %d active\n"
" Bitmask: 0x%02X\n"
" Requested bitmask: 0x%02X\n"
" HARQ drain pending: %s\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),
requested_mask,
pending ? "YES" : "NO",
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");
size_t used = strlen(response);
if (used < resp_size) {
used += snprintf(response + used, resp_size - used, "TSM locks:");
for (int ant_id = 0;
ant_id < g_ctrl.ant_state->num_tx_antennas && used < resp_size;
++ant_id) {
int64_t remaining = tx_lock_remaining_ms(ant_id);
if (remaining > 0)
used += snprintf(response + used, resp_size - used,
" TX%d=%lds", ant_id, (long)((remaining + 999) / 1000));
}
if (used < resp_size)
snprintf(response + used, resp_size - used, "\n");
}
}
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
clear_all_tx_locks();
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 <id> [15|30|45|60] - Mute TX antenna with optional TSM lock\n"
"UNMUTE TX <id> [FORCE] - Unmute TX antenna\n"
"MUTE/UNMUTE RX <id> - Change RX antenna state\n"
"SET TX MASK <hex> [duration] [FORCE] - Set TX mask and TSM lock\n"
"SET RX MASK <hex> - Set RX 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);
}
}
static void send_client_prompt(int client_fd)
{
const char prompt[] = "> ";
if (send(client_fd, prompt, sizeof(prompt) - 1, MSG_NOSIGNAL) < 0)
LOG_D(PHY, "[ANTENNA_CTRL] Failed to send client prompt: %s\n", strerror(errno));
}
// 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), MSG_NOSIGNAL);
send_client_prompt(client_fd);
while (g_ctrl.running) {
// Receive command
ssize_t n = recv(client_fd, buffer, sizeof(buffer) - 1, 0);
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR))
continue;
if (n <= 0)
break;
buffer[n] = '\0';
// Serialize state-changing commands while allowing multiple connected
// clients (for example the persistent O1 bridge and a diagnostic client).
pthread_mutex_lock(&g_ctrl.command_lock);
process_command(buffer, response, sizeof(response));
pthread_mutex_unlock(&g_ctrl.command_lock);
// Send response
if (strlen(response) > 0) {
send(client_fd, response, strlen(response), MSG_NOSIGNAL);
}
// Check for quit
if (strstr(buffer, "QUIT") || strstr(buffer, "quit") ||
strstr(buffer, "EXIT") || strstr(buffer, "exit")) {
break;
}
send_client_prompt(client_fd);
}
close(client_fd);
LOG_I(PHY, "[ANTENNA_CTRL] Client disconnected\n");
}
static void *client_thread(void *arg)
{
const int client_fd = (int)(intptr_t)arg;
handle_client(client_fd);
pthread_mutex_lock(&g_ctrl.clients_lock);
AssertFatal(g_ctrl.client_count > 0, "Antenna-control client count underflow\n");
g_ctrl.client_count--;
pthread_cond_broadcast(&g_ctrl.clients_done);
pthread_mutex_unlock(&g_ctrl.clients_lock);
return NULL;
}
// 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));
// Bound recv() so shutdown can wait for detached client workers without
// leaving a thread blocked forever on an idle persistent connection.
struct timeval client_timeout = {.tv_sec = 1, .tv_usec = 0};
setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &client_timeout, sizeof(client_timeout));
pthread_mutex_lock(&g_ctrl.clients_lock);
g_ctrl.client_count++;
pthread_mutex_unlock(&g_ctrl.clients_lock);
pthread_t client;
if (pthread_create(&client, NULL, client_thread, (void *)(intptr_t)client_fd) != 0) {
LOG_E(PHY, "[ANTENNA_CTRL] Failed to create client thread\n");
close(client_fd);
pthread_mutex_lock(&g_ctrl.clients_lock);
g_ctrl.client_count--;
pthread_cond_broadcast(&g_ctrl.clients_done);
pthread_mutex_unlock(&g_ctrl.clients_lock);
continue;
}
pthread_detach(client);
}
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;
pthread_mutex_init(&g_ctrl.command_lock, NULL);
pthread_mutex_init(&g_ctrl.clients_lock, NULL);
pthread_cond_init(&g_ctrl.clients_done, NULL);
g_ctrl.client_count = 0;
// 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));
pthread_cond_destroy(&g_ctrl.clients_done);
pthread_mutex_destroy(&g_ctrl.clients_lock);
pthread_mutex_destroy(&g_ctrl.command_lock);
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);
pthread_cond_destroy(&g_ctrl.clients_done);
pthread_mutex_destroy(&g_ctrl.clients_lock);
pthread_mutex_destroy(&g_ctrl.command_lock);
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);
pthread_cond_destroy(&g_ctrl.clients_done);
pthread_mutex_destroy(&g_ctrl.clients_lock);
pthread_mutex_destroy(&g_ctrl.command_lock);
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);
pthread_cond_destroy(&g_ctrl.clients_done);
pthread_mutex_destroy(&g_ctrl.clients_lock);
pthread_mutex_destroy(&g_ctrl.command_lock);
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);
// Client workers wake from recv() at least once per second and observe
// running=false. Keep ant_state valid until every worker has exited.
pthread_mutex_lock(&g_ctrl.clients_lock);
while (g_ctrl.client_count > 0)
pthread_cond_wait(&g_ctrl.clients_done, &g_ctrl.clients_lock);
pthread_mutex_unlock(&g_ctrl.clients_lock);
pthread_cond_destroy(&g_ctrl.clients_done);
pthread_mutex_destroy(&g_ctrl.clients_lock);
pthread_mutex_destroy(&g_ctrl.command_lock);
}
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
}