dapps 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dapp/__init__.py +4 -0
- dapp/dapp.py +66 -0
- dapps-0.0.1.dist-info/METADATA +83 -0
- dapps-0.0.1.dist-info/RECORD +20 -0
- dapps-0.0.1.dist-info/WHEEL +4 -0
- dapps-0.0.1.dist-info/licenses/LICENSE +201 -0
- e3interface/__init__.py +4 -0
- e3interface/defs/e3.asn +27 -0
- e3interface/e3_connector.py +295 -0
- e3interface/e3_interface.py +187 -0
- e3interface/e3_logging.py +23 -0
- spectrum/__init__.py +4 -0
- spectrum/spectrum_dapp.py +169 -0
- visualization/__init__.py +4 -0
- visualization/dashboard.py +104 -0
- visualization/energy.py +77 -0
- visualization/grafana.py +84 -0
- visualization/influx.py +98 -0
- visualization/iq.py +122 -0
- visualization/templates/index.html +195 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from enum import Enum
|
|
4
|
+
import os
|
|
5
|
+
import socket
|
|
6
|
+
import zmq
|
|
7
|
+
from .e3_logging import e3_logger, LOG_DIR
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class E3LinkLayer(Enum):
|
|
11
|
+
ZMQ = "zmq"
|
|
12
|
+
POSIX = "posix"
|
|
13
|
+
|
|
14
|
+
def __str__(self):
|
|
15
|
+
return self.value
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def from_string(link_layer_str: str):
|
|
19
|
+
try:
|
|
20
|
+
return E3LinkLayer(link_layer_str.lower())
|
|
21
|
+
except ValueError:
|
|
22
|
+
raise ValueError(f"Invalid link layer: '{link_layer_str}'. Must be one of {[e.value for e in E3LinkLayer]}")
|
|
23
|
+
|
|
24
|
+
class E3TransportLayer(Enum):
|
|
25
|
+
SCTP = "sctp"
|
|
26
|
+
TCP = "tcp"
|
|
27
|
+
IPC = "ipc"
|
|
28
|
+
|
|
29
|
+
def __str__(self):
|
|
30
|
+
return self.value
|
|
31
|
+
|
|
32
|
+
@staticmethod
|
|
33
|
+
def from_string(transport_layer_str: str):
|
|
34
|
+
try:
|
|
35
|
+
return E3TransportLayer(transport_layer_str.lower())
|
|
36
|
+
except ValueError:
|
|
37
|
+
raise ValueError(f"Invalid transport layer: '{transport_layer_str}'. Must be one of {[e.value for e in E3TransportLayer]}")
|
|
38
|
+
|
|
39
|
+
class E3Connector(ABC):
|
|
40
|
+
# List of known valid configurations
|
|
41
|
+
VALID_CONFIGURATIONS = [
|
|
42
|
+
(E3LinkLayer.ZMQ, E3TransportLayer.IPC),
|
|
43
|
+
(E3LinkLayer.ZMQ, E3TransportLayer.TCP),
|
|
44
|
+
(E3LinkLayer.POSIX, E3TransportLayer.TCP),
|
|
45
|
+
(E3LinkLayer.POSIX, E3TransportLayer.SCTP),
|
|
46
|
+
(E3LinkLayer.POSIX, E3TransportLayer.IPC)
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
IPC_BASE_DIR = "/tmp/dapps"
|
|
50
|
+
E3_IPC_SETUP_PATH = f"{IPC_BASE_DIR}/setup"
|
|
51
|
+
E3_IPC_SOCKET_PATH = f"{IPC_BASE_DIR}/e3_socket" # inbound
|
|
52
|
+
DAPP_IPC_SOCKET_PATH = f"{IPC_BASE_DIR}/dapp_socket" # outbound
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def setup_connector(link_layer: str, transport_layer: str):
|
|
56
|
+
link_layer = E3LinkLayer.from_string(link_layer)
|
|
57
|
+
transport_layer = E3TransportLayer.from_string(transport_layer)
|
|
58
|
+
|
|
59
|
+
# Check if the configuration is part of the valid configurations
|
|
60
|
+
if (link_layer, transport_layer) not in E3Connector.VALID_CONFIGURATIONS:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
f"Invalid configuration: Link Layer={link_layer}, Transport Layer={transport_layer}. "
|
|
63
|
+
f"Must be one of {E3Connector.VALID_CONFIGURATIONS}"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if link_layer == E3LinkLayer.POSIX:
|
|
67
|
+
return POSIXConnector(transport_layer)
|
|
68
|
+
else:
|
|
69
|
+
return ZMQConnector(transport_layer)
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def send_setup_request(self, payload):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def setup_inbound_connection(self):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def receive(self) -> bytes:
|
|
81
|
+
"""Receive a byte-encoded payload based on the configuration."""
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
@abstractmethod
|
|
85
|
+
def setup_outbound_connection(self):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def send(self, payload: bytes):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
@abstractmethod
|
|
93
|
+
def dispose(self):
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
class ZMQConnector(E3Connector):
|
|
97
|
+
setup_context: zmq.Context
|
|
98
|
+
inbound_context: zmq.Context
|
|
99
|
+
outbound_context: zmq.Context
|
|
100
|
+
|
|
101
|
+
def __init__(self, transport_layer: E3TransportLayer):
|
|
102
|
+
self.setup_context = zmq.Context()
|
|
103
|
+
|
|
104
|
+
match transport_layer:
|
|
105
|
+
case E3TransportLayer.SCTP | E3TransportLayer.TCP:
|
|
106
|
+
self.setup_endpoint = f"{transport_layer}://127.0.0.1:9990"
|
|
107
|
+
self.inbound_endpoint = f"{transport_layer}://127.0.0.1:9991"
|
|
108
|
+
self.outbound_endpoint = f"{transport_layer}://127.0.0.1:9999"
|
|
109
|
+
|
|
110
|
+
case E3TransportLayer.IPC:
|
|
111
|
+
self.setup_endpoint = f"{transport_layer}://{self.E3_IPC_SETUP_PATH}"
|
|
112
|
+
self.inbound_endpoint = f"{transport_layer}://{self.E3_IPC_SOCKET_PATH}"
|
|
113
|
+
self.outbound_endpoint = f"{transport_layer}://{self.DAPP_IPC_SOCKET_PATH}"
|
|
114
|
+
|
|
115
|
+
case _:
|
|
116
|
+
raise ValueError(f'Unknown/Unsupported value for transport layer {transport_layer}')
|
|
117
|
+
|
|
118
|
+
self.transport_layer = transport_layer
|
|
119
|
+
|
|
120
|
+
def send_setup_request(self, payload):
|
|
121
|
+
# Lazy pirate pattern, i.e., reliability on the client side
|
|
122
|
+
request_timeout = 1000
|
|
123
|
+
request_retries = 5
|
|
124
|
+
|
|
125
|
+
while request_retries > 0:
|
|
126
|
+
setup_socket = self.setup_context.socket(zmq.REQ)
|
|
127
|
+
setup_socket.connect(self.setup_endpoint)
|
|
128
|
+
e3_logger.debug("Send E3 Setup request")
|
|
129
|
+
setup_socket.send(payload)
|
|
130
|
+
|
|
131
|
+
if (setup_socket.poll(request_timeout) & zmq.POLLIN) != 0:
|
|
132
|
+
reply = setup_socket.recv()
|
|
133
|
+
e3_logger.debug('ZMQ setup socket replied')
|
|
134
|
+
return reply
|
|
135
|
+
|
|
136
|
+
request_retries -= 1
|
|
137
|
+
e3_logger.error("ZMQ setup did not reply")
|
|
138
|
+
setup_socket.setsockopt(zmq.LINGER, 0)
|
|
139
|
+
setup_socket.close()
|
|
140
|
+
e3_logger.debug('Retrying to connect')
|
|
141
|
+
|
|
142
|
+
raise ConnectionRefusedError('E3 Setup request procedure did not went through')
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def setup_inbound_connection(self):
|
|
146
|
+
self.inbound_context = zmq.Context()
|
|
147
|
+
self.inbound_socket = self.inbound_context.socket(zmq.SUB)
|
|
148
|
+
self.inbound_socket.setsockopt_string(zmq.SUBSCRIBE, "") # subscribe to all the messages
|
|
149
|
+
self.inbound_socket.setsockopt(zmq.CONFLATE, 1) # Keep only last message
|
|
150
|
+
self.inbound_socket.connect(self.inbound_endpoint)
|
|
151
|
+
|
|
152
|
+
self.poller = zmq.Poller()
|
|
153
|
+
self.poller.register(self.inbound_socket, zmq.POLLIN)
|
|
154
|
+
|
|
155
|
+
def receive(self) -> bytes:
|
|
156
|
+
# The commented part should be decommented for measuring the effective performance of the control loop
|
|
157
|
+
# start_time = time.perf_counter()
|
|
158
|
+
# while True:
|
|
159
|
+
|
|
160
|
+
# # Poll the socket without blocking
|
|
161
|
+
# events = dict(self.poller.poll(0))
|
|
162
|
+
|
|
163
|
+
# if self.inbound_socket in events and events[self.inbound_socket] == zmq.POLLIN:
|
|
164
|
+
# with open(f"{LOG_DIR}/busy.txt", "a") as f:
|
|
165
|
+
# print(f"{time.perf_counter() - start_time}", file=f)
|
|
166
|
+
return self.inbound_socket.recv()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def setup_outbound_connection(self):
|
|
170
|
+
self.outbound_context = zmq.Context()
|
|
171
|
+
self.outbound_socket = self.outbound_context.socket(zmq.PUB)
|
|
172
|
+
self.outbound_socket.bind(self.outbound_endpoint)
|
|
173
|
+
if self.transport_layer == E3TransportLayer.IPC:
|
|
174
|
+
os.chmod(self.DAPP_IPC_SOCKET_PATH, 0o666)
|
|
175
|
+
|
|
176
|
+
def send(self, payload: bytes):
|
|
177
|
+
self.outbound_socket.send(payload)
|
|
178
|
+
|
|
179
|
+
def dispose(self):
|
|
180
|
+
if hasattr(self, "setup_context"):
|
|
181
|
+
self.setup_context.destroy()
|
|
182
|
+
if hasattr(self, "inbound_context"):
|
|
183
|
+
self.inbound_context.destroy()
|
|
184
|
+
if hasattr(self, "outbound_context"):
|
|
185
|
+
self.outbound_context.destroy()
|
|
186
|
+
|
|
187
|
+
class POSIXConnector(E3Connector):
|
|
188
|
+
CHUNK_SIZE = 8192
|
|
189
|
+
|
|
190
|
+
def __init__(self, transport_layer: E3TransportLayer):
|
|
191
|
+
match transport_layer:
|
|
192
|
+
case E3TransportLayer.SCTP | E3TransportLayer.TCP:
|
|
193
|
+
self.setup_endpoint = ("127.0.0.1", 9990)
|
|
194
|
+
self.inbound_endpoint = ("127.0.0.1", 9991)
|
|
195
|
+
self.outbound_endpoint = ("127.0.0.1", 9999)
|
|
196
|
+
|
|
197
|
+
case E3TransportLayer.IPC:
|
|
198
|
+
self.setup_endpoint = self.E3_IPC_SETUP_PATH
|
|
199
|
+
self.inbound_endpoint = self.E3_IPC_SOCKET_PATH
|
|
200
|
+
self.outbound_endpoint = self.DAPP_IPC_SOCKET_PATH
|
|
201
|
+
|
|
202
|
+
case _:
|
|
203
|
+
raise ValueError(f'Unknown/Unsupported value for transport layer {transport_layer}')
|
|
204
|
+
|
|
205
|
+
self.transport_layer = transport_layer
|
|
206
|
+
|
|
207
|
+
def _create_socket(self):
|
|
208
|
+
match self.transport_layer:
|
|
209
|
+
case E3TransportLayer.SCTP:
|
|
210
|
+
try:
|
|
211
|
+
import sctp
|
|
212
|
+
except ModuleNotFoundError:
|
|
213
|
+
e3_logger.critical(
|
|
214
|
+
"SCTP selected as transport layer, but the optional dependency 'pysctp' is not installed.\n"
|
|
215
|
+
"Fix this by running:\n\n"
|
|
216
|
+
" pip install 'dApps[network]' # OR\n"
|
|
217
|
+
" pip install 'dApps[all]'\n",
|
|
218
|
+
exc_info=True
|
|
219
|
+
)
|
|
220
|
+
exit(-1)
|
|
221
|
+
sock = sctp.sctpsocket_tcp(socket.AF_INET)
|
|
222
|
+
case E3TransportLayer.TCP:
|
|
223
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
224
|
+
case E3TransportLayer.IPC:
|
|
225
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
226
|
+
case _:
|
|
227
|
+
raise ValueError(f'Unknown/Unsupported value for transport layer {self.transport_layer}')
|
|
228
|
+
return sock
|
|
229
|
+
|
|
230
|
+
def send_setup_request(self, payload):
|
|
231
|
+
setup_socket = self._create_socket()
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
setup_socket.connect(self.setup_endpoint)
|
|
235
|
+
setup_socket.send(payload)
|
|
236
|
+
reply = self.receive_in_chunks(setup_socket)
|
|
237
|
+
finally:
|
|
238
|
+
setup_socket.close()
|
|
239
|
+
|
|
240
|
+
return reply
|
|
241
|
+
|
|
242
|
+
def setup_inbound_connection(self):
|
|
243
|
+
self.inbound_socket = self._create_socket()
|
|
244
|
+
self.inbound_socket.bind(self.inbound_endpoint)
|
|
245
|
+
self.inbound_socket.listen(5)
|
|
246
|
+
self.inbound_connection, _ = self.inbound_socket.accept()
|
|
247
|
+
|
|
248
|
+
def receive_in_chunks(self, conn):
|
|
249
|
+
data = bytearray()
|
|
250
|
+
chunks = 0
|
|
251
|
+
|
|
252
|
+
# Receive the size of the buffer first
|
|
253
|
+
raw_size = conn.recv(4)
|
|
254
|
+
if len(raw_size) < 4:
|
|
255
|
+
e3_logger.error("Failed to receive buffer size")
|
|
256
|
+
return None
|
|
257
|
+
buffer_size = struct.unpack("!I", raw_size)[0]
|
|
258
|
+
e3_logger.debug(f"buffer size is {buffer_size}")
|
|
259
|
+
|
|
260
|
+
# Receive the buffer in chunks
|
|
261
|
+
while len(data) < buffer_size:
|
|
262
|
+
remaining = buffer_size - len(data)
|
|
263
|
+
chunk = conn.recv(min(self.CHUNK_SIZE, remaining))
|
|
264
|
+
if not chunk:
|
|
265
|
+
e3_logger.error("Connection closed unexpectedly")
|
|
266
|
+
return None # Connection closed unexpectedly
|
|
267
|
+
data.extend(chunk)
|
|
268
|
+
chunks += 1
|
|
269
|
+
|
|
270
|
+
e3_logger.debug(f"Chunks recv {chunks}")
|
|
271
|
+
e3_logger.debug(f"Total size recv {len(data)}")
|
|
272
|
+
return bytes(data)
|
|
273
|
+
|
|
274
|
+
def receive(self) -> bytes:
|
|
275
|
+
return self.receive_in_chunks(self.inbound_connection)
|
|
276
|
+
|
|
277
|
+
def setup_outbound_connection(self):
|
|
278
|
+
self.outbound_socket = self._create_socket()
|
|
279
|
+
self.outbound_socket.connect(self.outbound_endpoint)
|
|
280
|
+
|
|
281
|
+
def send(self, payload: bytes):
|
|
282
|
+
self.outbound_socket.send(payload)
|
|
283
|
+
|
|
284
|
+
def dispose(self):
|
|
285
|
+
if hasattr(self, "outbound_socket"):
|
|
286
|
+
self.outbound_socket.close()
|
|
287
|
+
if hasattr(self, "inbound_connection"):
|
|
288
|
+
self.inbound_connection.close()
|
|
289
|
+
if hasattr(self, "inbound_socket"):
|
|
290
|
+
self.inbound_socket.close()
|
|
291
|
+
|
|
292
|
+
if self.transport_layer == E3TransportLayer.IPC:
|
|
293
|
+
os.remove(self.E3_IPC_SETUP_PATH)
|
|
294
|
+
os.remove(self.E3_IPC_SOCKET_PATH)
|
|
295
|
+
os.remove(self.DAPP_IPC_SOCKET_PATH)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import multiprocessing
|
|
3
|
+
import queue
|
|
4
|
+
import threading
|
|
5
|
+
from .e3_connector import E3Connector
|
|
6
|
+
from .e3_logging import e3_logger
|
|
7
|
+
import asn1tools
|
|
8
|
+
|
|
9
|
+
class E3Interface:
|
|
10
|
+
_instance = None
|
|
11
|
+
_lock = threading.Lock()
|
|
12
|
+
|
|
13
|
+
def __new__(cls, *args, **kwargs):
|
|
14
|
+
with cls._lock:
|
|
15
|
+
if not cls._instance:
|
|
16
|
+
cls._instance = super(E3Interface, cls).__new__(cls)
|
|
17
|
+
return cls._instance
|
|
18
|
+
|
|
19
|
+
def __init__(self, *args, **kwargs):
|
|
20
|
+
if not hasattr(self, "initialized"):
|
|
21
|
+
self.callbacks = []
|
|
22
|
+
self.stop_event = multiprocessing.Event()
|
|
23
|
+
self.initialized = True
|
|
24
|
+
|
|
25
|
+
# Create an E3Connector instance based on the configuration
|
|
26
|
+
self.e3_connector = E3Connector.setup_connector(kwargs.get('link', ''), kwargs.get('transport', ''))
|
|
27
|
+
|
|
28
|
+
e3_logger.info(f"Endpoint setup {self.e3_connector.setup_endpoint}")
|
|
29
|
+
e3_logger.info(f"Endpoint inbound {self.e3_connector.inbound_endpoint}")
|
|
30
|
+
e3_logger.info(f"Endpoint outbound {self.e3_connector.outbound_endpoint}")
|
|
31
|
+
|
|
32
|
+
self.defs = asn1tools.compile_files(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./defs/e3.asn"), codec="per")
|
|
33
|
+
self.outbound_queue = multiprocessing.Queue()
|
|
34
|
+
|
|
35
|
+
def send_setup_request(self, dappId: int = 1) -> bool:
|
|
36
|
+
e3_logger.info("Start setup request")
|
|
37
|
+
payload = self.create_setup_request(dappId)
|
|
38
|
+
try:
|
|
39
|
+
response = self.e3_connector.send_setup_request(payload)
|
|
40
|
+
except ConnectionRefusedError as e:
|
|
41
|
+
e3_logger.error(f"Unable to connect to E3 setup endpoint, connection refused: {e}")
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
e3_logger.info("Setup response received")
|
|
45
|
+
pdu = self.defs.decode('E3-PDU', response)
|
|
46
|
+
if pdu[0] == "setupResponse":
|
|
47
|
+
e3_setup_response = pdu[1]
|
|
48
|
+
e3_logger.info(e3_setup_response)
|
|
49
|
+
# Either here or in setup connections we need E3 sub request and response
|
|
50
|
+
self.setup_connections()
|
|
51
|
+
return e3_setup_response['responseCode'] == 'positive'
|
|
52
|
+
else:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def setup_connections(self):
|
|
56
|
+
# Two connections, one for the inbound the other for the outbound
|
|
57
|
+
self.inbound_process = multiprocessing.Process(target=self._inbound_connection)
|
|
58
|
+
self.outbound_process = multiprocessing.Process(target=self._outbound_connection)
|
|
59
|
+
|
|
60
|
+
self.inbound_process.start()
|
|
61
|
+
self.outbound_process.start()
|
|
62
|
+
|
|
63
|
+
def _inbound_connection(self):
|
|
64
|
+
"""
|
|
65
|
+
Inbound is for all the messages that are coming from the RAN after the initial setup
|
|
66
|
+
"""
|
|
67
|
+
e3_logger.info(f'Start inbound connection')
|
|
68
|
+
self.e3_connector.setup_inbound_connection()
|
|
69
|
+
e3_logger.info(f'Start inbound loop')
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
while not self.stop_event.is_set():
|
|
73
|
+
data = self.e3_connector.receive()
|
|
74
|
+
if not data:
|
|
75
|
+
e3_logger.error(f'No data received, connection closed, end')
|
|
76
|
+
break
|
|
77
|
+
e3_logger.debug(f'Received data size: {len(data)}')
|
|
78
|
+
e3_logger.debug(data.hex())
|
|
79
|
+
pdu = self.defs.decode("E3-PDU", data)
|
|
80
|
+
e3_logger.debug(f"Data decoded")
|
|
81
|
+
match pdu[0]:
|
|
82
|
+
case "indicationMessage":
|
|
83
|
+
e3_indication_message = pdu[1]
|
|
84
|
+
protocolData = e3_indication_message['protocolData']
|
|
85
|
+
e3_logger.debug(protocolData)
|
|
86
|
+
e3_logger.debug(f"Indication message protocolData {len(protocolData)}")
|
|
87
|
+
self._handle_incoming_data(protocolData)
|
|
88
|
+
|
|
89
|
+
case "xAppControlAction":
|
|
90
|
+
e3_xapp_control_action = pdu[1]
|
|
91
|
+
raise NotImplementedError()
|
|
92
|
+
|
|
93
|
+
case _:
|
|
94
|
+
raise ValueError("Unrecognized value ", pdu)
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
e3_logger.error(f"Error in inbound thread: {e}")
|
|
98
|
+
self.stop_event.set()
|
|
99
|
+
finally:
|
|
100
|
+
e3_logger.info("Close inbound connection")
|
|
101
|
+
|
|
102
|
+
def _outbound_connection(self):
|
|
103
|
+
"""
|
|
104
|
+
Outbound is for all the messages that should go to the RAN after the initial setup
|
|
105
|
+
Messages are dApp Control Action and dApp Report Message
|
|
106
|
+
"""
|
|
107
|
+
e3_logger.info(f'Start outbound connection')
|
|
108
|
+
self.e3_connector.setup_outbound_connection()
|
|
109
|
+
|
|
110
|
+
e3_logger.info(f'Start outbound loop')
|
|
111
|
+
try:
|
|
112
|
+
while not self.stop_event.is_set():
|
|
113
|
+
try:
|
|
114
|
+
msg, data = self.outbound_queue.get(timeout=0.5)
|
|
115
|
+
except queue.Empty:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
e3_logger.debug(f"Outbound queue has got '{msg}', {data}")
|
|
119
|
+
|
|
120
|
+
match msg:
|
|
121
|
+
case "control":
|
|
122
|
+
payload = self.create_control_action(data)
|
|
123
|
+
case "report":
|
|
124
|
+
payload = self.create_dapp_report(data)
|
|
125
|
+
|
|
126
|
+
case _:
|
|
127
|
+
raise ValueError("Unrecognized value ", msg)
|
|
128
|
+
|
|
129
|
+
e3_logger.debug(f"Send the pdu encoded {payload}")
|
|
130
|
+
self.e3_connector.send(payload)
|
|
131
|
+
|
|
132
|
+
except Exception as e:
|
|
133
|
+
e3_logger.error(f"Error outbound thread: {e}")
|
|
134
|
+
self.stop_event.set()
|
|
135
|
+
finally:
|
|
136
|
+
e3_logger.info("Close outbound connection")
|
|
137
|
+
|
|
138
|
+
def _handle_incoming_data(self, data):
|
|
139
|
+
for callback in self.callbacks:
|
|
140
|
+
e3_logger.debug("Launch callback")
|
|
141
|
+
callback(data)
|
|
142
|
+
|
|
143
|
+
def schedule_control(self, payload: bytes):
|
|
144
|
+
self.outbound_queue.put(('control', payload))
|
|
145
|
+
|
|
146
|
+
def schedule_report(self, payload: bytes):
|
|
147
|
+
self.outbound_queue.put(('report', payload))
|
|
148
|
+
|
|
149
|
+
def add_callback(self, callback):
|
|
150
|
+
if callback not in self.callbacks:
|
|
151
|
+
e3_logger.debug("Add callback")
|
|
152
|
+
self.callbacks.append(callback)
|
|
153
|
+
|
|
154
|
+
def remove_callback(self, callback):
|
|
155
|
+
if callback in self.callbacks:
|
|
156
|
+
e3_logger.debug("Remove callback")
|
|
157
|
+
self.callbacks.remove(callback)
|
|
158
|
+
|
|
159
|
+
def terminate_connections(self):
|
|
160
|
+
e3_logger.info("Stop event")
|
|
161
|
+
self.stop_event.set()
|
|
162
|
+
|
|
163
|
+
if hasattr(self, "inbound_process"):
|
|
164
|
+
self.inbound_process.join()
|
|
165
|
+
if hasattr(self, "outbound_process"):
|
|
166
|
+
self.outbound_process.join()
|
|
167
|
+
|
|
168
|
+
self.e3_connector.dispose()
|
|
169
|
+
|
|
170
|
+
def create_control_action(self, actionData: bytes):
|
|
171
|
+
control_message = ("controlAction", {"actionData": actionData})
|
|
172
|
+
payload = self.defs.encode("E3-PDU", control_message)
|
|
173
|
+
return payload
|
|
174
|
+
|
|
175
|
+
def create_dapp_report(self, reportData: bytes):
|
|
176
|
+
dapp_report_message = ("dAppReport", {"reportData": reportData})
|
|
177
|
+
payload = self.defs.encode("E3-PDU", dapp_report_message)
|
|
178
|
+
return payload
|
|
179
|
+
|
|
180
|
+
def create_setup_request(self, ranId: int = 1, ranFunctions: list = []):
|
|
181
|
+
setup_request_message = ("setupRequest", {"ranIdentifier": ranId, "ranFunctionsList": ranFunctions})
|
|
182
|
+
payload = self.defs.encode("E3-PDU", setup_request_message)
|
|
183
|
+
return payload
|
|
184
|
+
|
|
185
|
+
def __del__(self):
|
|
186
|
+
if not self.stop_event.is_set():
|
|
187
|
+
self.terminate_connections()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
LOG_DIR = ('.' if os.geteuid() != 0 else '') + '/logs/'
|
|
5
|
+
|
|
6
|
+
def setup_logger(name, log_file, level=logging.INFO, format_str="[%(name)s] [%(created)f] %(levelname)s - %(message)s"):
|
|
7
|
+
logger = logging.getLogger(name)
|
|
8
|
+
logger.setLevel(level)
|
|
9
|
+
|
|
10
|
+
handler = logging.FileHandler(log_file)
|
|
11
|
+
handler.setLevel(level)
|
|
12
|
+
|
|
13
|
+
formatter = logging.Formatter(format_str)
|
|
14
|
+
handler.setFormatter(formatter)
|
|
15
|
+
|
|
16
|
+
# Avoid adding multiple handlers if logger is reused
|
|
17
|
+
if not logger.handlers:
|
|
18
|
+
logger.addHandler(handler)
|
|
19
|
+
|
|
20
|
+
return logger
|
|
21
|
+
|
|
22
|
+
e3_logger = setup_logger("e3_logger", f"{LOG_DIR}/e3.log", format_str="[E3] [%(created)f] %(levelname)s - %(message)s")
|
|
23
|
+
dapp_logger = setup_logger("dapp_logger", f"{LOG_DIR}/dapp.log", format_str="[dApp] [%(created)f] %(levelname)s - %(message)s")
|
spectrum/__init__.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
dApp for Spectrum Sharing
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__author__ = "Andrea Lacava"
|
|
7
|
+
|
|
8
|
+
import multiprocessing
|
|
9
|
+
import time
|
|
10
|
+
import numpy as np
|
|
11
|
+
# np.set_printoptions(threshold=sys.maxsize)
|
|
12
|
+
|
|
13
|
+
from dapp.dapp import DApp
|
|
14
|
+
from e3interface.e3_logging import dapp_logger, LOG_DIR
|
|
15
|
+
|
|
16
|
+
class SpectrumSharingDApp(DApp):
|
|
17
|
+
|
|
18
|
+
### Configuration ###
|
|
19
|
+
# gNB runs with BW = 40 MHz, with -E (3/4 sampling)
|
|
20
|
+
# Center frequency = 3.6192 GHz
|
|
21
|
+
# OAI sampling frequency 46.08e6
|
|
22
|
+
# No SRS for now, hence in gNB config file set do_SRS = 0
|
|
23
|
+
# gNB->frame_parms.ofdm_symbol_size = 1536
|
|
24
|
+
# gNB->frame_parms.first_carrier_offset = 900
|
|
25
|
+
# Noise floor threshold needs to be calibrated
|
|
26
|
+
# We receive the symbols and average them over some frames, and do thresholding.
|
|
27
|
+
|
|
28
|
+
def __init__(self, id: int = 1, ota: bool = False, save_iqs: bool = False, control: bool = False, link: str = 'posix', transport:str = 'uds', **kwargs):
|
|
29
|
+
super().__init__(link=link, transport=transport, **kwargs)
|
|
30
|
+
|
|
31
|
+
self.bw = 40.08e6 # Bandwidth in Hz
|
|
32
|
+
self.center_freq = 3.6192e9 # Center frequency in Hz
|
|
33
|
+
self.First_carrier_offset = 900
|
|
34
|
+
self.Num_car_prb = 12
|
|
35
|
+
self.prb_thrs = 75 # This avoids blacklisting PRBs where the BWP is scheduled (it’s a workaround bc the UE and gNB would not be able to communicate anymore, a cleaner fix is to move the BWP if needed or things like that)
|
|
36
|
+
self.FFT_SIZE = 1536
|
|
37
|
+
self.Average_over_frames = 63
|
|
38
|
+
|
|
39
|
+
if ota:
|
|
40
|
+
dapp_logger.info(f'Using OTA configuration')
|
|
41
|
+
self.Noise_floor_threshold = 20 # this really depends on the RF conditions and should be carefully calibrated
|
|
42
|
+
else: # Colosseum
|
|
43
|
+
dapp_logger.info(f'Using Colosseum configuration')
|
|
44
|
+
self.Noise_floor_threshold = 53
|
|
45
|
+
|
|
46
|
+
self.save_iqs = save_iqs
|
|
47
|
+
self.e3_interface.add_callback(self.get_iqs_from_ran)
|
|
48
|
+
if self.save_iqs:
|
|
49
|
+
self.iq_save_file = open(f"{LOG_DIR}/iqs_{int(time.time())}.bin", "ab")
|
|
50
|
+
self.save_counter = 0
|
|
51
|
+
self.limit_per_file = 200
|
|
52
|
+
self.control = control
|
|
53
|
+
dapp_logger.info(f"Control is {'not ' if not self.control else ''}active")
|
|
54
|
+
|
|
55
|
+
self.energyGui = kwargs.get('energyGui', False)
|
|
56
|
+
self.iqPlotterGui = kwargs.get('iqPlotterGui', False)
|
|
57
|
+
self.demoGui = kwargs.get('demoGui', False)
|
|
58
|
+
|
|
59
|
+
self.control_count = 1
|
|
60
|
+
self.abs_iq_av = np.zeros(self.FFT_SIZE)
|
|
61
|
+
|
|
62
|
+
if self.energyGui:
|
|
63
|
+
from visualization.energy import EnergyPlotter
|
|
64
|
+
self.sig_queue = multiprocessing.Queue()
|
|
65
|
+
self.energyPlotter = EnergyPlotter(self.FFT_SIZE, bw=self.bw, center_freq=self.center_freq)
|
|
66
|
+
|
|
67
|
+
if self.iqPlotterGui:
|
|
68
|
+
from visualization.iq import IQPlotter
|
|
69
|
+
self.iq_queue = multiprocessing.Queue()
|
|
70
|
+
iq_size = self.FFT_SIZE * 2 # double the size of ofdm_symbol_size since real and imaginary parts are interleaved
|
|
71
|
+
self.iqPlotter = IQPlotter(buffer_size=500, iq_size=iq_size, bw=self.bw, center_freq=self.center_freq)
|
|
72
|
+
|
|
73
|
+
if self.demoGui:
|
|
74
|
+
from visualization.dashboard import DemoGui
|
|
75
|
+
self.demo_queue = multiprocessing.Queue()
|
|
76
|
+
iq_size = self.FFT_SIZE * 2 # double the size of ofdm_symbol_size since real and imaginary parts are interleaved
|
|
77
|
+
self.demo = DemoGui(buffer_size=100, iq_size=iq_size)
|
|
78
|
+
|
|
79
|
+
def get_iqs_from_ran(self, data):
|
|
80
|
+
dapp_logger.debug(f'Triggered callback')
|
|
81
|
+
if self.save_iqs:
|
|
82
|
+
dapp_logger.debug("I will write on the logfile iqs")
|
|
83
|
+
self.save_counter += 1
|
|
84
|
+
self.iq_save_file.write(data)
|
|
85
|
+
self.iq_save_file.flush()
|
|
86
|
+
if self.save_counter > self.limit_per_file:
|
|
87
|
+
self.iq_save_file.close()
|
|
88
|
+
self.iq_save_file = open(f"{LOG_DIR}/iqs_{int(time.time())}.bin", "ab")
|
|
89
|
+
|
|
90
|
+
iq_arr = np.frombuffer(data, dtype=np.int16)
|
|
91
|
+
dapp_logger.debug(f"Shape of iq_arr {iq_arr.shape}")
|
|
92
|
+
|
|
93
|
+
if self.iqPlotterGui:
|
|
94
|
+
self.iq_queue.put(iq_arr)
|
|
95
|
+
|
|
96
|
+
if self.demoGui:
|
|
97
|
+
self.demo_queue.put(("iq_data", iq_arr))
|
|
98
|
+
|
|
99
|
+
if self.control:
|
|
100
|
+
dapp_logger.debug("Start control operations")
|
|
101
|
+
iq_comp = iq_arr[::2] + iq_arr[1::2] * 1j
|
|
102
|
+
dapp_logger.debug(f"Shape of iq_comp {iq_comp.shape}")
|
|
103
|
+
abs_iq = np.abs(iq_comp).astype(float)
|
|
104
|
+
dapp_logger.debug(f"After iq division self.abs_iq_av: {self.abs_iq_av.shape} abs_iq: {abs_iq.shape}")
|
|
105
|
+
self.abs_iq_av += abs_iq
|
|
106
|
+
self.control_count += 1
|
|
107
|
+
dapp_logger.debug(f"Control count is: {self.control_count}")
|
|
108
|
+
|
|
109
|
+
if self.iqPlotterGui:
|
|
110
|
+
self.iq_queue.put(iq_arr)
|
|
111
|
+
|
|
112
|
+
if self.control_count == self.Average_over_frames:
|
|
113
|
+
abs_iq_av_db = 20 * np.log10(1 + (self.abs_iq_av/(self.Average_over_frames)))
|
|
114
|
+
abs_iq_av_db_offset_correct = np.append(abs_iq_av_db[self.First_carrier_offset:self.FFT_SIZE],abs_iq_av_db[0:self.First_carrier_offset])
|
|
115
|
+
dapp_logger.info(f'--- AVG VALUES ----')
|
|
116
|
+
dapp_logger.info(f'abs_iq_av_db_offset_correct: {abs_iq_av_db_offset_correct.mean()}')
|
|
117
|
+
dapp_logger.info(f'--- MAX VALUES ----')
|
|
118
|
+
dapp_logger.info(f'abs_iq_av_db_offset_correct: {abs_iq_av_db_offset_correct.max()}')
|
|
119
|
+
# last_5_max_abs_iq_av_db_offset_correct = np.sort(abs_iq_av_db_offset_correct)[-20:]
|
|
120
|
+
# dapp_logger.info(f'Last 20 max values (abs_iq_av_db_offset_correct): {last_5_max_abs_iq_av_db_offset_correct}')
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# PRB blocking based on the noise floor threshold
|
|
124
|
+
f_ind = np.arange(self.FFT_SIZE)
|
|
125
|
+
blklist_sub_carrier = f_ind[abs_iq_av_db_offset_correct > self.Noise_floor_threshold]
|
|
126
|
+
np.sort(blklist_sub_carrier)
|
|
127
|
+
dapp_logger.info(f'blklist_sub_carrier: {blklist_sub_carrier}')
|
|
128
|
+
prb_blk_list = np.unique((np.floor(blklist_sub_carrier/self.Num_car_prb))).astype(np.uint16)
|
|
129
|
+
dapp_logger.info(f'prb_blk_list: {prb_blk_list}')
|
|
130
|
+
prb_blk_list = prb_blk_list[prb_blk_list > self.prb_thrs]
|
|
131
|
+
# prb_blk_list = np.array([76, 77, 78, 79, 80, 81, 82, 83], dtype=np.uint16) # 76, 77, 78, 79, 80, 81, 82, 83
|
|
132
|
+
# prb_blk_list = np.arange(start=76, stop=140, dtype=np.uint16)
|
|
133
|
+
dapp_logger.info(f"Blacklisted prbs ({prb_blk_list.size}): {prb_blk_list}")
|
|
134
|
+
prb_new = prb_blk_list.view(prb_blk_list.dtype.newbyteorder('>'))
|
|
135
|
+
|
|
136
|
+
# Create the payload
|
|
137
|
+
size = prb_blk_list.size.to_bytes(2,'little')
|
|
138
|
+
prbs_to_send = prb_new.tobytes(order="C")
|
|
139
|
+
|
|
140
|
+
# Schedule the delivery
|
|
141
|
+
self.e3_interface.schedule_control(size+prbs_to_send)
|
|
142
|
+
|
|
143
|
+
if self.energyGui:
|
|
144
|
+
self.sig_queue.put(abs_iq_av_db)
|
|
145
|
+
|
|
146
|
+
if self.demoGui:
|
|
147
|
+
self.demo_queue.put(("prb_list", prb_blk_list))
|
|
148
|
+
|
|
149
|
+
# Reset the variables
|
|
150
|
+
self.abs_iq_av = np.zeros(self.FFT_SIZE)
|
|
151
|
+
self.control_count = 1
|
|
152
|
+
|
|
153
|
+
def _control_loop(self):
|
|
154
|
+
if self.energyGui:
|
|
155
|
+
abs_iq_av_db = self.sig_queue.get()
|
|
156
|
+
self.energyPlotter.process_iq_data(abs_iq_av_db)
|
|
157
|
+
if self.iqPlotterGui:
|
|
158
|
+
iq_data = self.iq_queue.get()
|
|
159
|
+
self.iqPlotter.process_iq_data(iq_data)
|
|
160
|
+
if self.demoGui:
|
|
161
|
+
message = self.demo_queue.get()
|
|
162
|
+
self.demo.process_iq_data(message)
|
|
163
|
+
|
|
164
|
+
def _stop(self):
|
|
165
|
+
if self.save_iqs:
|
|
166
|
+
self.iq_save_file.close()
|
|
167
|
+
|
|
168
|
+
if self.demoGui:
|
|
169
|
+
self.demo.stop()
|