srsRAN Integration Example
This example demonstrates how to integrate ai_nn_controller with a real wireless RAN system — srsRAN — by bridging its InfluxDB metrics server into the ai_nn_controller message bus. It highlights the framework’s multi-domain capability: the same controller that manages optical amplifiers and ROADMs can simultaneously ingest live 5G RAN KPIs and expose them to AI agents.
Overview
srsRAN ships with a metrics server that writes UE-level and system-level KPIs to an InfluxDB bucket (the same data source Grafana dashboards use). The integration adds two components:
srsRAN Network Node (
network_nodes/srsran_node/) — polls InfluxDB for the latest metrics and pushes them into the ai_nn_controller message bus.A read-only control application — subscribes to those measurements and exposes them via the REST API and MCP tools. See srsRAN Read Measurements App.
┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐
│ srsRAN │ │ │ │ ai_nn_controller Message Bus │
│ gNodeB / │──────►│ InfluxDB │◄──────│ │
│ Metrics │ write │ │ poll │ srsRAN Node (ID=10) │
│ Server │ └──────────────┘ │ │ │
└──────────────┘ │ ▼ PUSH measurements │
│ Broker ──► PUB │
│ │ │
│ ┌─────────▼────────┐ │
│ │ SrsranReadMeasure │ │
│ │ ments App │ │
│ │ (SUB, REST, MCP) │ │
│ └──────────────────┘ │
└──────────────────────────┘
Available Metrics
The srsRAN node exposes two categories of measurements:
UE-Level Metrics (from ue_info InfluxDB measurement):
Metric |
Description |
|---|---|
|
Physical Cell ID |
|
Radio Network Temporary Identifier |
|
Downlink bitrate |
|
Uplink bitrate |
|
Downlink Block Error Rate |
|
Uplink Block Error Rate |
|
Downlink Modulation and Coding Scheme |
|
Uplink Modulation and Coding Scheme |
|
Downlink successful / failed transmissions |
|
Uplink successful / failed transmissions |
|
Buffer Status Report |
|
Channel Quality Indicator |
|
Rank Indicator |
|
Uplink SNR |
|
PUSCH SNR (dB) |
|
PUCCH SNR (dB) |
System-Level Metrics (from app_resource_usage InfluxDB measurement):
Metric |
Description |
|---|---|
|
CPU usage of the srsRAN process |
|
Memory consumption (MB) |
|
Estimated power consumption (W) |
srsRAN Network Node
The node (network_nodes/srsran_node/node.py) is built on the controlled_entity
framework – the same base class and decorator pattern used by the dummy nodes. The
key difference is the setup() hook, which initializes an InfluxDB poller thread
that queries the srsRAN metrics bucket.
This demonstrates how to integrate a real external data source using the
ControlledEntity abstraction. The node developer only implements the southbound
logic; all ZMQ plumbing is handled by NodeRunner.
from controlled_entity import ControlledEntity, node, NodeRunner
import threading
@node(name="srsRAN")
class SrsranNode(ControlledEntity):
available_measurements = [
"session_id",
"pci", "rnti", "dl_bitrate", "ul_bitrate",
"dl_bler", "ul_bler", "cqi", "ul_snr",
"cpu_usage_percent", "memory_usage_MB",
# ... full list in source
]
measurement_interval = 1.0
def setup(self):
"""Start InfluxDB poller thread -- runs after registration."""
self._latest_metrics = {}
self._metrics_lock = threading.Lock()
self._influx_url = self.config.get("influxdb_url", "http://influxdb:8086")
self._influx_bucket = self.config.get("influxdb_bucket", "srsran")
thread = threading.Thread(target=self._poll_influxdb, daemon=True)
thread.start()
def poll_measurements(self):
with self._metrics_lock:
current = dict(self._latest_metrics)
if current:
current["session_id"] = f"session_{self.config['node_id']}_{int(time.time())}"
return current
return None
if __name__ == "__main__":
NodeRunner().run()
Configuration (node.conf):
ip_address = aic_register
register_port = 5558
node_id = 10
pub_port = 5580
# InfluxDB connection (must match srsRAN metrics-server config)
influxdb_url = http://influxdb:8086
influxdb_token = <your-token>
influxdb_org = srs
influxdb_bucket = srsran
poll_interval = 1
All custom keys (influxdb_url, influxdb_bucket, etc.) are accessible via
self.config in the node’s setup() method.
How it works:
@node(name="srsRAN")registers the class with thecontrolled_entityframeworkNodeRunner().run()handles registration withaic_registeras node ID 10After registration,
setup()initializes the InfluxDB connection and starts a background poller threadThe poller queries two InfluxDB measurements:
ue_infoandapp_resource_usagepoll_measurements()returns the cached metrics everymeasurement_intervalsecondsNodeRunnerpublishes the measurements to the broker via ZMQ PUSH
# Core InfluxDB query (from node.py)
ue_query = f'''
from(bucket: "{self.influxdb_bucket}")
|> range(start: -30s)
|> filter(fn: (r) => r._measurement == "ue_info")
|> last()
'''
srsRAN Read Measurements App
The read-only control application subscribes to all srsRAN node metrics and prints them. This follows exactly the same pattern as any other ai_nn_controller app:
from ai_nn_controller.decorators.aic_app import aic_app
from ai_nn_controller.AicApp import AicApp
from ai_nn_controller.AicController import AicController
@aic_app(name="SrsranReadMeasurements")
class SrsranReadMeasurementsApp(AicApp):
aic_app_id = 10
control_loop_update_time = 2
# Subscribe to all srsRAN metrics from node 10
read_measurements = {
10: [
"session_id",
"pci", "rnti",
"dl_bitrate", "ul_bitrate",
"dl_bler", "ul_bler",
"dl_mcs", "ul_mcs",
"dl_nof_ok", "dl_nof_nok",
"ul_nof_ok", "ul_nof_nok",
"bsr", "cqi", "ri",
"ul_snr", "pusch_snr_db", "pucch_snr_db",
"cpu_usage_percent", "memory_usage_MB",
"power_consumption_Watts",
],
}
# Read-only -- no control functions
control_functions = {}
@classmethod
def process(cls, measurements):
latest = measurements.get(10, [None])[-1] if measurements.get(10) else None
if not latest:
print("No srsRAN data yet")
return
print(f"DL bitrate : {latest.get('dl_bitrate')}")
print(f"CQI : {latest.get('cqi')}")
print(f"CPU usage : {latest.get('cpu_usage_percent')}%")
if __name__ == "__main__":
AicController(with_api=True).run()
Because this app is registered with the framework, the following are auto-generated:
REST endpoint:
GET /apps/SrsranReadMeasurements/measurementsMCP tool:
SrsranReadMeasurements_get_measurements
This means an AI agent can query live 5G RAN KPIs via MCP alongside optical network metrics, all from the same controller.
Docker Deployment
The srsRAN node is defined in network_nodes/srsran_node/ and requires an
active srsRAN deployment with InfluxDB. To add it to your stack, add the
following to docker-compose.yml:
srsran_node:
container_name: srsran_node
build:
context: ./network_nodes/srsran_node/
dockerfile: Dockerfile
depends_on:
- aic_register
- node_msg_broker
networks:
- aic_network
- docker_metrics # Access InfluxDB on the srsRAN metrics network
command: >
sh -c "sleep 5 && python3 node.py"
srsran_reader:
container_name: srsran_reader
build:
context: ./
dockerfile: control_applications/srsran_read_measurements/Dockerfile
ports:
- "8000:8000"
networks:
- aic_network
command: >
sh -c "sleep 25 && pip install --no-cache-dir /ai_nn_controller && python3 aic_app.py --verbose"
networks:
docker_metrics:
external: true # Created by the srsRAN docker-compose
Note
The docker_metrics network must already exist (created by the srsRAN
Docker Compose stack). This allows the srsRAN node container to reach
InfluxDB on its internal network.
Expected Output
When running, the srsRAN reader app prints measurements every 2 seconds:
======================================================================
[srsRAN Measurements] Processing at 1707609600.00
======================================================================
session_id : session_10_1707609600
--- UE-Level Metrics ---
pci : 1.0
rnti : 17921.0
dl_bitrate : 28500000.0
ul_bitrate : 12300000.0
dl_bler : 0.02
ul_bler : 0.01
dl_mcs : 27.0
ul_mcs : 22.0
cqi : 15.0
ri : 2.0
ul_snr : 25.3
pusch_snr_db : 24.8
pucch_snr_db : 23.1
--- System-Level Metrics ---
cpu_usage_percent : 45.2
memory_usage_MB : 512.0
power_consumption_Watts: 35.0
======================================================================
Key Patterns Demonstrated
controlled_entity abstraction: The srsRAN node is a
ControlledEntitysubclass, just like the dummy nodes. The only difference is thesetup()hook that initializes the InfluxDB connection – all ZMQ plumbing is handled byNodeRunnerExternal data-source bridging: The
setup()pattern can be used for any external telemetry source (Prometheus, SNMP, gRPC streaming, REST APIs, etc.)Read-only apps: Not every app needs control functions; pure monitoring apps expose data via REST and MCP without sending commands
Multi-domain integration: The same controller simultaneously manages optical nodes (amplifiers, ROADMs) and wireless nodes (srsRAN gNodeB), demonstrating cross-domain network intelligence
Docker network bridging: The
docker_metricsexternal network lets the srsRAN node reach InfluxDB without exposing it on the host
Next Steps
Add
@agent_controlledoperations to let an AI agent trigger RAN optimizations based on live KPIs (e.g., adjust scheduling weights when CQI drops)Build a cross-domain app that correlates optical link quality with RAN throughput
Connect additional domain nodes (core network, transport) to create a fully converged multi-domain AI controller