dapps 0.0.2__tar.gz → 0.0.4__tar.gz

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.
Files changed (43) hide show
  1. {dapps-0.0.2 → dapps-0.0.4}/.github/workflows/tag-and-release.yml +10 -9
  2. dapps-0.0.4/PKG-INFO +114 -0
  3. dapps-0.0.4/README.md +70 -0
  4. dapps-0.0.4/VERSION +1 -0
  5. dapps-0.0.4/examples/spectrum_dapp.py +110 -0
  6. dapps-0.0.4/pyproject.toml +66 -0
  7. {dapps-0.0.2 → dapps-0.0.4}/src/dapp/dapp.py +1 -1
  8. {dapps-0.0.2 → dapps-0.0.4}/src/e3interface/defs/e3.asn +1 -1
  9. {dapps-0.0.2 → dapps-0.0.4}/src/spectrum/spectrum_dapp.py +40 -31
  10. {dapps-0.0.2 → dapps-0.0.4}/src/visualization/dashboard.py +14 -22
  11. {dapps-0.0.2 → dapps-0.0.4}/src/visualization/iq.py +4 -4
  12. dapps-0.0.4/src/visualization/templates/index.html +308 -0
  13. dapps-0.0.2/PKG-INFO +0 -83
  14. dapps-0.0.2/README.md +0 -42
  15. dapps-0.0.2/VERSION +0 -1
  16. dapps-0.0.2/examples/spectrum_dapp.py +0 -69
  17. dapps-0.0.2/pyproject.toml +0 -58
  18. dapps-0.0.2/src/visualization/templates/index.html +0 -214
  19. {dapps-0.0.2 → dapps-0.0.4}/.github/workflows/mirror.yml +0 -0
  20. {dapps-0.0.2 → dapps-0.0.4}/.gitignore +0 -0
  21. {dapps-0.0.2 → dapps-0.0.4}/LICENSE +0 -0
  22. {dapps-0.0.2 → dapps-0.0.4}/MANIFEST.in +0 -0
  23. {dapps-0.0.2 → dapps-0.0.4}/docker/Dockerfile +0 -0
  24. {dapps-0.0.2 → dapps-0.0.4}/docker/build.sh +0 -0
  25. {dapps-0.0.2 → dapps-0.0.4}/docs/dapp_uml.txt +0 -0
  26. {dapps-0.0.2 → dapps-0.0.4}/docs/gui_uml.txt +0 -0
  27. {dapps-0.0.2 → dapps-0.0.4}/e3protocol/README.md +0 -0
  28. {dapps-0.0.2 → dapps-0.0.4}/e3protocol/build_asn.sh +0 -0
  29. {dapps-0.0.2 → dapps-0.0.4}/e3protocol/client/client.c +0 -0
  30. {dapps-0.0.2 → dapps-0.0.4}/e3protocol/dump_packets.sh +0 -0
  31. {dapps-0.0.2 → dapps-0.0.4}/e3protocol/server/server.py +0 -0
  32. {dapps-0.0.2 → dapps-0.0.4}/e3protocol/wrapper/e3.c +0 -0
  33. {dapps-0.0.2 → dapps-0.0.4}/e3protocol/wrapper/e3.h +0 -0
  34. {dapps-0.0.2 → dapps-0.0.4}/src/dapp/__init__.py +0 -0
  35. {dapps-0.0.2 → dapps-0.0.4}/src/e3interface/__init__.py +0 -0
  36. {dapps-0.0.2 → dapps-0.0.4}/src/e3interface/e3_connector.py +0 -0
  37. {dapps-0.0.2 → dapps-0.0.4}/src/e3interface/e3_interface.py +0 -0
  38. {dapps-0.0.2 → dapps-0.0.4}/src/e3interface/e3_logging.py +0 -0
  39. {dapps-0.0.2 → dapps-0.0.4}/src/spectrum/__init__.py +0 -0
  40. {dapps-0.0.2 → dapps-0.0.4}/src/visualization/__init__.py +0 -0
  41. {dapps-0.0.2 → dapps-0.0.4}/src/visualization/energy.py +0 -0
  42. {dapps-0.0.2 → dapps-0.0.4}/src/visualization/grafana.py +0 -0
  43. {dapps-0.0.2 → dapps-0.0.4}/src/visualization/influx.py +0 -0
@@ -9,7 +9,7 @@ jobs:
9
9
  create-tag:
10
10
  runs-on: ubuntu-latest
11
11
  outputs:
12
- version: ${{ steps.set_version.outputs.version }}
12
+ version: ${{ steps.get_version.outputs.version }}
13
13
  tag_created: ${{ steps.set_output.outputs.tag_created }}
14
14
  steps:
15
15
  - name: Checkout repository
@@ -48,11 +48,6 @@ jobs:
48
48
  echo "tag_created=false" >> $GITHUB_OUTPUT
49
49
  fi
50
50
 
51
- - name: Set version output
52
- id: set_version
53
- run: |
54
- echo "version=${{ steps.get_version.outputs.version }}" >> $GITHUB_OUTPUT
55
-
56
51
  build:
57
52
  name: Build distribution 📦
58
53
  runs-on: ubuntu-latest
@@ -78,7 +73,9 @@ jobs:
78
73
 
79
74
  publish-to-pypi:
80
75
  name: Publish to PyPI
81
- needs: build
76
+ needs:
77
+ - build
78
+ - create-tag
82
79
  runs-on: ubuntu-latest
83
80
  if: ${{ needs.create-tag.outputs.tag_created == 'true' && github.repository == 'wineslab/dApp-library' }}
84
81
  environment:
@@ -97,7 +94,9 @@ jobs:
97
94
 
98
95
  github-release:
99
96
  name: Sign packages with Sigstore and upload them to GitHub Release
100
- needs: publish-to-pypi
97
+ needs:
98
+ - publish-to-pypi
99
+ - create-tag
101
100
  runs-on: ubuntu-latest
102
101
  if: ${{ needs.create-tag.outputs.tag_created == 'true' && github.repository == 'wineslab/dApp-library' }}
103
102
  permissions:
@@ -126,7 +125,9 @@ jobs:
126
125
 
127
126
  publish-to-testpypi:
128
127
  name: Publish to TestPyPI
129
- needs: build
128
+ needs:
129
+ - publish-to-pypi
130
+ - create-tag
130
131
  runs-on: ubuntu-latest
131
132
  if: ${{ needs.create-tag.outputs.tag_created == 'true' && github.repository == 'wineslab/dApp-library' }}
132
133
  environment:
dapps-0.0.4/PKG-INFO ADDED
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: dapps
3
+ Version: 0.0.4
4
+ Summary: A library for creating and connecting dApps to Open RAN units.
5
+ Project-URL: Homepage, https://openrangym.com/o-ran-frameworks/dapps
6
+ Project-URL: Package, https://pypi.org/project/dapps/
7
+ Project-URL: Source, https://github.com/wineslab/dApp-library
8
+ Project-URL: Bug Tracker, https://github.com/wineslab/dApp-library/issues
9
+ Author-email: Andrea Lacava <thecave003@gmail.com>
10
+ License-File: LICENSE
11
+ Keywords: 5G,O-RAN,cellular,dApps,networks
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: Implementation :: CPython
20
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
21
+ Requires-Python: >=3.8
22
+ Requires-Dist: asn1tools
23
+ Requires-Dist: numpy
24
+ Requires-Dist: pyzmq
25
+ Requires-Dist: typing-extensions
26
+ Provides-Extra: all
27
+ Requires-Dist: flask; extra == 'all'
28
+ Requires-Dist: flask-socketio; extra == 'all'
29
+ Requires-Dist: influxdb-client; extra == 'all'
30
+ Requires-Dist: libiq; extra == 'all'
31
+ Requires-Dist: matplotlib; extra == 'all'
32
+ Requires-Dist: pysctp; extra == 'all'
33
+ Provides-Extra: api
34
+ Requires-Dist: influxdb-client; extra == 'api'
35
+ Provides-Extra: cnn
36
+ Requires-Dist: libiq; extra == 'cnn'
37
+ Provides-Extra: gui
38
+ Requires-Dist: flask; extra == 'gui'
39
+ Requires-Dist: flask-socketio; extra == 'gui'
40
+ Requires-Dist: matplotlib; extra == 'gui'
41
+ Provides-Extra: network
42
+ Requires-Dist: pysctp; extra == 'network'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # dApp Library
46
+
47
+ A complete tutorial on how to deploy a dApp can be found on the [OpenRAN Gym website](https://openrangym.com/tutorials/dapps-oai). Please refer to that guide to instrument your system.
48
+
49
+ ## Installation
50
+
51
+ ### Python package installation (recommended)
52
+
53
+ ```
54
+ pip3 install dapps[all]
55
+ ```
56
+
57
+ The dApp can be found in the `examples` directory or it can be downloaded from this repository.
58
+
59
+ ### Manual installation (use only for library development)
60
+
61
+ Clone the repository and install it manually with:
62
+
63
+ ```
64
+ hatch build
65
+ pip3 install dist/*.tar.gz
66
+ ```
67
+
68
+ ### Launch the Spectrum Sharing dApp example
69
+
70
+ OAI should start _before_ running the dApp
71
+
72
+ ```python
73
+ python3 examples/spectrum_dapp.py
74
+ ```
75
+
76
+ This dApp implements a spectrum sharing use case discussed in [our paper](https://doi.org/10.1016/j.comnet.2025.111342) ([here for the archive version](https://arxiv.org/pdf/2501.16502)).
77
+ The dApp parameters can be controlled through the following command-line arguments:
78
+
79
+ - `--link` (str, default: 'zmq', choices: [layer.value for layer in E3LinkLayer]): Specify the link layer to be used
80
+ - `--transport` (str, default: 'ipc', choices: [layer.value for layer in E3TransportLayer]): Specify the transport layer to be used
81
+ - `--save-iqs` (store_true, default: False): Specify if this is data collection run or not. In the first case I/Q samples will be saved
82
+ - `--control` (store_true, default: False): Set whether to perform control of PRB
83
+ - `--noise-floor-threshold` (int, default: None): Set the noise floor threshold for determining the presence of incumbents and for detecting the PRBs affected.
84
+ - `--ota` (store_true, default: False): Specify if the setup used is OTA or on Colosseum for determining the noise floor threshold. If the `noise_floor_threshold` parameter is specified, this parameter is ignored
85
+ - `--energy-gui` (store_true, default: False): Set whether to enable the energy GUI
86
+ - `--iq-plotter-gui` (store_true, default: False): Set whether to enable the IQ Plotter GUI
87
+ - `--demo-gui` (store_true, default: False): Set whether to enable the Demo GUI
88
+ - `--num-prbs` (int, default: 106): Number of PRBs
89
+ - `--num-subcarrier-spacing` (int, default: 30): Subcarrier spacing in kHz (FR1 is 30)
90
+ - `--e` (store_true, default: False): Set if 3/4 sampling for FFT size is set on the gNB (-E option on OAI)
91
+ - `--center-freq` (float, default: 3.6192e9): Center frequency in Hz
92
+ - `--timed` (store_true, default: False): Run with a 5-minute time limit
93
+ - `--model` (str, default: ''): Path to the CNN model file to be used
94
+ - `--time-window` (int, default: 5): Number of input vectors to pass to the CNN model
95
+ - `--moving-avg-window` (int, default: 30): Window size (in samples) for the moving average used to detect energy peaks in the spectrum
96
+ - `--extraction-window` (int, default: 600): Number of samples to retain after detecting an energy peak
97
+
98
+
99
+ If you use the dApp concept and/or the framework to develop your own dApps, please cite the following paper:
100
+
101
+ ```text
102
+ @article{LACAVA2025111342,
103
+ title = {{dApps: Enabling Real-Time AI-based Open RAN Control}},
104
+ journal = {Computer Networks},
105
+ pages = {111342},
106
+ year = {2025},
107
+ issn = {1389-1286},
108
+ doi = {https://doi.org/10.1016/j.comnet.2025.111342},
109
+ url = {https://www.sciencedirect.com/science/article/pii/S1389128625003093},
110
+ author = {Andrea Lacava and Leonardo Bonati and Niloofar Mohamadi and Rajeev Gangula and Florian Kaltenberger and Pedram Johari and Salvatore D’Oro and Francesca Cuomo and Michele Polese and Tommaso Melodia},
111
+ keywords = {Open RAN, dApps, Real-time control loops, Radio Resource Management (RRM), Spectrum sharing, Positioning, Integrated Sensing and Communication (ISAC)},
112
+ abstract = {Open Radio Access Networks (RANs) leverage disaggregated and programmable RAN functions and open interfaces to enable closed-loop, data-driven radio resource management. This is performed through custom intelligent applications on the RAN Intelligent Controllers (RICs), optimizing RAN policy scheduling, network slicing, user session management, and medium access control, among others. In this context, we have proposed dApps as a key extension of the O-RAN architecture into the real-time and user-plane domains. Deployed directly on RAN nodes, dApps access data otherwise unavailable to RICs due to privacy or timing constraints, enabling the execution of control actions within shorter time intervals. In this paper, we propose for the first time a reference architecture for dApps, defining their life cycle from deployment by the Service Management and Orchestration (SMO) to real-time control loop interactions with the RAN nodes where they are hosted. We introduce a new dApp interface, E3, along with an Application Protocol (AP) that supports structured message exchanges and extensible communication for various service models. By bridging E3 with the existing O-RAN E2 interface, we enable dApps, xApps, and rApps to coexist and coordinate. These applications can then collaborate on complex use cases and employ hierarchical control to resolve shared resource conflicts. Finally, we present and open-source a dApp framework based on OpenAirInterface (OAI). We benchmark its performance in two real-time control use cases, i.e., spectrum sharing and positioning in a 5th generation (5G) Next Generation Node Base (gNB) scenario. Our experimental results show that standardized real-time control loops via dApps are feasible, achieving average control latency below 450microseconds and allowing optimal use of shared spectral resources.}
113
+ }
114
+ ```
dapps-0.0.4/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # dApp Library
2
+
3
+ A complete tutorial on how to deploy a dApp can be found on the [OpenRAN Gym website](https://openrangym.com/tutorials/dapps-oai). Please refer to that guide to instrument your system.
4
+
5
+ ## Installation
6
+
7
+ ### Python package installation (recommended)
8
+
9
+ ```
10
+ pip3 install dapps[all]
11
+ ```
12
+
13
+ The dApp can be found in the `examples` directory or it can be downloaded from this repository.
14
+
15
+ ### Manual installation (use only for library development)
16
+
17
+ Clone the repository and install it manually with:
18
+
19
+ ```
20
+ hatch build
21
+ pip3 install dist/*.tar.gz
22
+ ```
23
+
24
+ ### Launch the Spectrum Sharing dApp example
25
+
26
+ OAI should start _before_ running the dApp
27
+
28
+ ```python
29
+ python3 examples/spectrum_dapp.py
30
+ ```
31
+
32
+ This dApp implements a spectrum sharing use case discussed in [our paper](https://doi.org/10.1016/j.comnet.2025.111342) ([here for the archive version](https://arxiv.org/pdf/2501.16502)).
33
+ The dApp parameters can be controlled through the following command-line arguments:
34
+
35
+ - `--link` (str, default: 'zmq', choices: [layer.value for layer in E3LinkLayer]): Specify the link layer to be used
36
+ - `--transport` (str, default: 'ipc', choices: [layer.value for layer in E3TransportLayer]): Specify the transport layer to be used
37
+ - `--save-iqs` (store_true, default: False): Specify if this is data collection run or not. In the first case I/Q samples will be saved
38
+ - `--control` (store_true, default: False): Set whether to perform control of PRB
39
+ - `--noise-floor-threshold` (int, default: None): Set the noise floor threshold for determining the presence of incumbents and for detecting the PRBs affected.
40
+ - `--ota` (store_true, default: False): Specify if the setup used is OTA or on Colosseum for determining the noise floor threshold. If the `noise_floor_threshold` parameter is specified, this parameter is ignored
41
+ - `--energy-gui` (store_true, default: False): Set whether to enable the energy GUI
42
+ - `--iq-plotter-gui` (store_true, default: False): Set whether to enable the IQ Plotter GUI
43
+ - `--demo-gui` (store_true, default: False): Set whether to enable the Demo GUI
44
+ - `--num-prbs` (int, default: 106): Number of PRBs
45
+ - `--num-subcarrier-spacing` (int, default: 30): Subcarrier spacing in kHz (FR1 is 30)
46
+ - `--e` (store_true, default: False): Set if 3/4 sampling for FFT size is set on the gNB (-E option on OAI)
47
+ - `--center-freq` (float, default: 3.6192e9): Center frequency in Hz
48
+ - `--timed` (store_true, default: False): Run with a 5-minute time limit
49
+ - `--model` (str, default: ''): Path to the CNN model file to be used
50
+ - `--time-window` (int, default: 5): Number of input vectors to pass to the CNN model
51
+ - `--moving-avg-window` (int, default: 30): Window size (in samples) for the moving average used to detect energy peaks in the spectrum
52
+ - `--extraction-window` (int, default: 600): Number of samples to retain after detecting an energy peak
53
+
54
+
55
+ If you use the dApp concept and/or the framework to develop your own dApps, please cite the following paper:
56
+
57
+ ```text
58
+ @article{LACAVA2025111342,
59
+ title = {{dApps: Enabling Real-Time AI-based Open RAN Control}},
60
+ journal = {Computer Networks},
61
+ pages = {111342},
62
+ year = {2025},
63
+ issn = {1389-1286},
64
+ doi = {https://doi.org/10.1016/j.comnet.2025.111342},
65
+ url = {https://www.sciencedirect.com/science/article/pii/S1389128625003093},
66
+ author = {Andrea Lacava and Leonardo Bonati and Niloofar Mohamadi and Rajeev Gangula and Florian Kaltenberger and Pedram Johari and Salvatore D’Oro and Francesca Cuomo and Michele Polese and Tommaso Melodia},
67
+ keywords = {Open RAN, dApps, Real-time control loops, Radio Resource Management (RRM), Spectrum sharing, Positioning, Integrated Sensing and Communication (ISAC)},
68
+ abstract = {Open Radio Access Networks (RANs) leverage disaggregated and programmable RAN functions and open interfaces to enable closed-loop, data-driven radio resource management. This is performed through custom intelligent applications on the RAN Intelligent Controllers (RICs), optimizing RAN policy scheduling, network slicing, user session management, and medium access control, among others. In this context, we have proposed dApps as a key extension of the O-RAN architecture into the real-time and user-plane domains. Deployed directly on RAN nodes, dApps access data otherwise unavailable to RICs due to privacy or timing constraints, enabling the execution of control actions within shorter time intervals. In this paper, we propose for the first time a reference architecture for dApps, defining their life cycle from deployment by the Service Management and Orchestration (SMO) to real-time control loop interactions with the RAN nodes where they are hosted. We introduce a new dApp interface, E3, along with an Application Protocol (AP) that supports structured message exchanges and extensible communication for various service models. By bridging E3 with the existing O-RAN E2 interface, we enable dApps, xApps, and rApps to coexist and coordinate. These applications can then collaborate on complex use cases and employ hierarchical control to resolve shared resource conflicts. Finally, we present and open-source a dApp framework based on OpenAirInterface (OAI). We benchmark its performance in two real-time control use cases, i.e., spectrum sharing and positioning in a 5th generation (5G) Next Generation Node Base (gNB) scenario. Our experimental results show that standardized real-time control loops via dApps are feasible, achieving average control latency below 450microseconds and allowing optimal use of shared spectral resources.}
69
+ }
70
+ ```
dapps-0.0.4/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.4
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Example script to showcase the Spectrum Sharing dApp
4
+ """
5
+
6
+ import argparse
7
+ import multiprocessing
8
+ import os
9
+ import time
10
+
11
+ from e3interface.e3_connector import E3LinkLayer, E3TransportLayer
12
+ from spectrum.spectrum_dapp import SpectrumSharingDApp
13
+
14
+ LOG_DIR = ('.' if os.geteuid() != 0 else '') + '/logs/'
15
+
16
+ def stop_program(time_to_wait, dapp: SpectrumSharingDApp):
17
+ time.sleep(time_to_wait)
18
+ print("Stop is called")
19
+ dapp.stop()
20
+ time.sleep(0.5) # to allow proper closure of the dApp threads, irrelevant to profiling
21
+ print("Test completed")
22
+
23
+ def main(args, time_to_wait: float = 60.0):
24
+ # with open(f"{LOG_DIR}/busy.txt", "w") as f:
25
+ # f.close()
26
+
27
+ if args.model:
28
+ try:
29
+ from libiq.classifier.cnn import Classifier
30
+ except ModuleNotFoundError:
31
+ print(
32
+ "Optional dependencies to run this example are not installed.\n"
33
+ "Fix this by running:\n\n"
34
+ " pip install libiq # OR\n"
35
+ " pip install dapps[cnn] (minimal) # OR\n"
36
+ " pip install dapps[all] (preferred)\n"
37
+ )
38
+ exit(-1)
39
+
40
+ # This value really depends on the RF conditions and the RU used and should be carefully calibrated
41
+ if args.noise_floor_threshold:
42
+ print(f'Using custom configuration')
43
+ noise_floor_threshold = args.noise_floor_threshold
44
+ else:
45
+ if args.ota:
46
+ print(f'Using OTA configuration')
47
+ noise_floor_threshold = 20
48
+ else: # Colosseum
49
+ print(f'Using Colosseum configuration')
50
+ noise_floor_threshold = 53
51
+
52
+ print(f'Threshold is {noise_floor_threshold}')
53
+
54
+ classifier = None
55
+ if args.model:
56
+ import math
57
+ ofdm_symbol_size = args.num_prbs * 12 # 12 subcarriers per PRB always fixed by LTE/NR standard
58
+ fft_size = 2 ** math.ceil(math.log2(ofdm_symbol_size)) # Next power of 2
59
+ classifier = Classifier(
60
+ time_window = args.time_window,
61
+ input_vector = fft_size,
62
+ moving_avg_window = args.moving_avg_window,
63
+ extraction_window = args.extraction_window,
64
+ model_path = args.model
65
+ )
66
+
67
+ dapp = SpectrumSharingDApp(noise_floor_threshold=noise_floor_threshold, save_iqs=args.save_iqs, control=args.control, link=args.link, transport=args.transport,
68
+ energyGui=args.energy_gui, iqPlotterGui=args.iq_plotter_gui, dashboard=args.demo_gui, classifier=classifier, center_freq=args.center_freq,
69
+ num_prbs=args.num_prbs, e_sampling=args.e, num_subcarrier_spacing= args.num_subcarrier_spacing)
70
+
71
+ dapp.setup_connection()
72
+
73
+ if args.timed:
74
+ timer = multiprocessing.Process(target=stop_program, args=(time_to_wait, dapp))
75
+ timer.start()
76
+ else:
77
+ timer = None
78
+
79
+ try:
80
+ dapp.control_loop()
81
+ finally:
82
+ if args.timed:
83
+ timer.kill()
84
+
85
+
86
+ if __name__ == "__main__":
87
+ parser = argparse.ArgumentParser(description="Example of a dApp for Spectrum Sharing")
88
+ parser.add_argument('--link', type=str, default='zmq', choices=[layer.value for layer in E3LinkLayer], help="Specify the link layer to be used")
89
+ parser.add_argument('--transport', type=str, default='ipc', choices=[layer.value for layer in E3TransportLayer], help="Specify the transport layer to be used")
90
+ parser.add_argument('--save-iqs', action='store_true', default=False, help="Specify if this is data collection run or not. In the first case I/Q samples will be saved")
91
+ parser.add_argument('--control', action='store_true', default=False, help="Set whether to perform control of PRB")
92
+ parser.add_argument('--noise-floor-threshold', type=int, default=None, help="Set the noise floor threshold for determining the presence of incumbents and for detecting the PRBs affected.")
93
+ parser.add_argument('--ota', action='store_true', default=False, help="Specify if the setup used is OTA or on Colosseum for determining the noise floor threshold. If the `noise_floor_threshold` parameter is specified, this parameter is ignored")
94
+ parser.add_argument('--energy-gui', action='store_true', default=False, help="Set whether to enable the energy GUI")
95
+ parser.add_argument('--iq-plotter-gui', action='store_true', default=False, help="Set whether to enable the IQ Plotter GUI")
96
+ parser.add_argument('--demo-gui', action='store_true', default=False, help="Set whether to enable the Demo GUI")
97
+ parser.add_argument('--num-prbs', type=int, default=106, help="Number of PRBs")
98
+ parser.add_argument('--num-subcarrier-spacing', type=int, default=30, help="Subcarrier spacing in kHz (FR1 is 30)")
99
+ parser.add_argument('--e', action='store_true', default=False, help="Set if 3/4 sampling for FFT size is set on the gNB (-E option on OAI)")
100
+ parser.add_argument('--center-freq', type=float, default=3.6192e9, help="Center frequency in Hz")
101
+ parser.add_argument('--timed', action='store_true', default=False, help="Run with a 5-minute time limit")
102
+ parser.add_argument('--model', type=str, default='', help="Path to the CNN model file to be used")
103
+ parser.add_argument('--time-window', type=int, default=5, help="Number of input vectors to pass to the CNN model.")
104
+ parser.add_argument('--moving-avg-window', type=int, default=30, help="Window size (in samples) for the moving average used to detect energy peaks in the spectrum.")
105
+ parser.add_argument('--extraction-window', type=int, default=600, help="Number of samples to retain after detecting an energy peak.")
106
+
107
+ args = parser.parse_args()
108
+ print("Start dApp")
109
+
110
+ main(args)
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = [ "hatchling" ]
3
+ build-backend = "hatchling.build"
4
+
5
+
6
+ [project]
7
+ name = "dapps"
8
+ dynamic = [ "version" ]
9
+ description = "A library for creating and connecting dApps to Open RAN units."
10
+ readme = "README.md"
11
+ requires-python = ">=3.8"
12
+ keywords = [ "dApps", "O-RAN", "5G", "cellular", "networks" ]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Programming Language :: Python",
16
+ "Programming Language :: Python :: 3.8",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: Implementation :: CPython",
22
+ "Programming Language :: Python :: Implementation :: PyPy"
23
+ ]
24
+ dependencies = [ "asn1tools", "typing_extensions", "numpy", "pyzmq" ]
25
+
26
+
27
+ [[project.authors]]
28
+ name = "Andrea Lacava"
29
+ email = "thecave003@gmail.com"
30
+
31
+
32
+ [project.urls]
33
+ Homepage = "https://openrangym.com/o-ran-frameworks/dapps"
34
+ Package = "https://pypi.org/project/dapps/"
35
+ Source = "https://github.com/wineslab/dApp-library"
36
+ "Bug Tracker" = "https://github.com/wineslab/dApp-library/issues"
37
+
38
+
39
+ [project.optional-dependencies]
40
+ api = [ "influxdb_client" ]
41
+ cnn = [ "libiq" ]
42
+ gui = [ "flask", "flask-socketio", "matplotlib" ]
43
+ network = [ "pysctp" ]
44
+ all = [
45
+ "pysctp",
46
+ "flask",
47
+ "flask-socketio",
48
+ "matplotlib",
49
+ "influxdb_client",
50
+ "libiq"
51
+ ]
52
+
53
+
54
+ [tool.hatch.version]
55
+ path = "VERSION"
56
+ pattern = "(?P<version>\\d+\\.\\d+\\.\\d+)"
57
+
58
+
59
+ [tool.hatch.build.targets.wheel]
60
+ packages = [
61
+ "src/dapp",
62
+ "src/e3interface",
63
+ "src/spectrum",
64
+ "src/visualization"
65
+ ]
66
+ sources = [ "src" ]
@@ -19,7 +19,7 @@ LOG_DIR = ('.' if os.geteuid() != 0 else '') + '/logs/'
19
19
  class DApp(ABC):
20
20
  e3_interface: E3Interface
21
21
 
22
- def __init__(self, id: int = 1, link: str = 'posix', transport:str = 'uds', callbacks: list = [], **kwargs):
22
+ def __init__(self, id: int = 1, link: str = 'posix', transport:str = 'ipc', callbacks: list = [], **kwargs):
23
23
  super().__init__()
24
24
  self.dapp_id = id
25
25
  self.e3_interface = E3Interface(link=link, transport=transport)
@@ -10,7 +10,7 @@ E3 DEFINITIONS AUTOMATIC TAGS ::= BEGIN
10
10
  }
11
11
 
12
12
  E3-IndicationMessage ::= SEQUENCE {
13
- protocolData OCTET STRING (SIZE(1..8192))
13
+ protocolData OCTET STRING (SIZE(1..16384))
14
14
  }
15
15
 
16
16
  E3-ControlAction ::= SEQUENCE {
@@ -15,26 +15,36 @@ from e3interface.e3_logging import dapp_logger, LOG_DIR
15
15
 
16
16
  class SpectrumSharingDApp(DApp):
17
17
 
18
- ### Configuration ###
19
- # gNB runs with BW = 40 MHz, with -E (3/4 sampling)
18
+ ### Default Configuration ###
19
+ # gNB runs with BW = ~40 MHz
20
20
  # Center frequency = 3.6192 GHz
21
- # OAI sampling frequency 46.08e6
21
+ # OAI sampling frequency 46.08e6 # not useful for visualization
22
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, noise_floor_threshold: int = 53, 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
23
+ # gNB->frame_parms.ofdm_symbol_size = 1536 # FFT size (even though is called ofdm_symbol_size) for USRP, with -E flag (3/4 sampling)
24
+ # gNB->frame_parms.ofdm_symbol_size = 2048 # FFT size (even though is called ofdm_symbol_size) for RUs, with no -E flag
25
+ # Noise floor threshold needs to be calibrated according to the RU
26
+ # We receive the symbols, average them over 63 frames and do PRB thresholding.
27
+
28
+ def __init__(self, id: int = 1, link: str = 'posix', transport: str = 'ipc', noise_floor_threshold: int = 53, save_iqs: bool = False, control: bool = False,
29
+ center_freq: float = 3.6192e9, num_prbs: int = 106, num_subcarrier_spacing: int = 30, e_sampling: bool = False, **kwargs):
30
+ super().__init__(id=id, link=link, transport=transport, **kwargs)
31
+
32
+ # gNB radio configuration used to compute exact values
33
+ self.num_consecutive_subcarriers_for_prb: int = 12 # Always fixed by the LTE/NR standard to 12
34
+ self.num_prbs = num_prbs
35
+ self.num_subcarrier_spacing = num_subcarrier_spacing # subcarrier spacing in kHz (FR1 is 30)
36
+ self.ofdm_symbol_size = num_prbs * self.num_consecutive_subcarriers_for_prb
37
+ self.bw = self.num_prbs * self.num_consecutive_subcarriers_for_prb * self.num_subcarrier_spacing * 1e3 # Bandwidth in Hz
38
+ self.center_freq = center_freq # Center frequency in Hz
39
+ self.fft_size = 2 ** int(np.ceil(np.log2(self.ofdm_symbol_size))) # Next power of 2 to include guard bands
40
+ if e_sampling:
41
+ self.fft_size = int(self.fft_size * 3 / 4) # 3/4 of sampling for USRPs (-E flag)
42
+ self.first_carrier_offset = self.fft_size - (self.ofdm_symbol_size // 2)
43
+
44
+ # dApp configuration
45
+ # The first 76 PRB usually have channels that should not be nulled (but we should investigate more options to enable this)
46
+ self.prb_thrs = 75 # This avoids blacklisting PRBs where the BWP is scheduled (it’s a workaround since 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)
47
+ self.average_over_frames = 63
38
48
  self.noise_floor_threshold = noise_floor_threshold
39
49
  self.save_iqs = save_iqs
40
50
  self.e3_interface.add_callback(self.get_iqs_from_ran)
@@ -50,25 +60,24 @@ class SpectrumSharingDApp(DApp):
50
60
  self.dashboard = kwargs.get('dashboard', False)
51
61
 
52
62
  self.control_count = 1
53
- self.abs_iq_av = np.zeros(self.FFT_SIZE)
63
+ self.abs_iq_av = np.zeros(self.fft_size)
54
64
 
55
65
  if self.energyGui:
56
66
  from visualization.energy import EnergyPlotter
57
67
  self.sig_queue = multiprocessing.Queue()
58
- self.energyPlotter = EnergyPlotter(self.FFT_SIZE, bw=self.bw, center_freq=self.center_freq)
68
+ self.energyPlotter = EnergyPlotter(self.fft_size, bw=self.bw, center_freq=self.center_freq)
59
69
 
60
70
  if self.iqPlotterGui:
61
71
  from visualization.iq import IQPlotter
62
72
  self.iq_queue = multiprocessing.Queue()
63
- iq_size = self.FFT_SIZE * 2 # double the size of ofdm_symbol_size since real and imaginary parts are interleaved
64
- self.iqPlotter = IQPlotter(buffer_size=500, iq_size=iq_size, bw=self.bw, center_freq=self.center_freq)
73
+ self.iqPlotter = IQPlotter(buffer_size=500, fft_size=self.fft_size, bw=self.bw, center_freq=self.center_freq)
65
74
 
66
75
  if self.dashboard:
67
76
  from visualization.dashboard import Dashboard
68
77
  self.demo_queue = multiprocessing.Queue()
69
- iq_size = self.FFT_SIZE * 2 # double the size of ofdm_symbol_size since real and imaginary parts are interleaved
70
78
  classifier = kwargs.get('classifier', None)
71
- self.demo = Dashboard(buffer_size=100, iq_size=iq_size, classifier=classifier)
79
+ self.demo = Dashboard(buffer_size=100, ofdm_symbol_size=self.ofdm_symbol_size, first_carrier_offset=self.first_carrier_offset,
80
+ bw=self.bw, center_freq=self.center_freq, num_prbs=num_prbs, classifier=classifier)
72
81
 
73
82
  def get_iqs_from_ran(self, data):
74
83
  dapp_logger.debug(f'Triggered callback')
@@ -83,7 +92,7 @@ class SpectrumSharingDApp(DApp):
83
92
 
84
93
  iq_arr = np.frombuffer(data, dtype=np.int16)
85
94
  dapp_logger.debug(f"Shape of iq_arr {iq_arr.shape}")
86
-
95
+
87
96
  if self.iqPlotterGui:
88
97
  self.iq_queue.put(iq_arr)
89
98
 
@@ -100,9 +109,9 @@ class SpectrumSharingDApp(DApp):
100
109
  self.control_count += 1
101
110
  dapp_logger.debug(f"Control count is: {self.control_count}")
102
111
 
103
- if self.control_count == self.Average_over_frames:
104
- abs_iq_av_db = 20 * np.log10(1 + (self.abs_iq_av/(self.Average_over_frames)))
105
- 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])
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])
106
115
  dapp_logger.info(f'--- AVG VALUES ----')
107
116
  dapp_logger.info(f'abs_iq_av_db_offset_correct: {abs_iq_av_db_offset_correct.mean()}')
108
117
  dapp_logger.info(f'--- MAX VALUES ----')
@@ -111,11 +120,11 @@ class SpectrumSharingDApp(DApp):
111
120
  # dapp_logger.info(f'Last 20 max values (abs_iq_av_db_offset_correct): {last_5_max_abs_iq_av_db_offset_correct}')
112
121
 
113
122
  # PRB blocking based on the noise floor threshold
114
- f_ind = np.arange(self.FFT_SIZE)
123
+ f_ind = np.arange(self.fft_size)
115
124
  blklist_sub_carrier = f_ind[abs_iq_av_db_offset_correct > self.noise_floor_threshold]
116
125
  np.sort(blklist_sub_carrier)
117
126
  dapp_logger.info(f'blklist_sub_carrier: {blklist_sub_carrier}')
118
- prb_blk_list = np.unique((np.floor(blklist_sub_carrier/self.Num_car_prb))).astype(np.uint16)
127
+ prb_blk_list = np.unique((np.floor(blklist_sub_carrier/self.num_consecutive_subcarriers_for_prb))).astype(np.uint16)
119
128
  dapp_logger.info(f'prb_blk_list: {prb_blk_list}')
120
129
  prb_blk_list = prb_blk_list[prb_blk_list > self.prb_thrs]
121
130
  # prb_blk_list = np.array([76, 77, 78, 79, 80, 81, 82, 83], dtype=np.uint16) # 76, 77, 78, 79, 80, 81, 82, 83
@@ -137,7 +146,7 @@ class SpectrumSharingDApp(DApp):
137
146
  self.demo_queue.put(("prb_list", prb_blk_list))
138
147
 
139
148
  # Reset the variables
140
- self.abs_iq_av = np.zeros(self.FFT_SIZE)
149
+ self.abs_iq_av = np.zeros(self.fft_size)
141
150
  self.control_count = 1
142
151
 
143
152
  def _control_loop(self):
@@ -14,18 +14,21 @@ except ModuleNotFoundError:
14
14
  import numpy as np
15
15
 
16
16
  class Dashboard:
17
- def __init__(self, buffer_size=100, iq_size=1536, bw=46.08e6, center_freq=3.6192e9, classifier=None):
18
- # TODO double check killing conditions
17
+ # The default values of the dashboard are based on the default configuration
18
+ def __init__(self, buffer_size: int = 100, ofdm_symbol_size: int = 1272, bw: float = 38.16e6, center_freq: float = 3.6192e9,
19
+ num_prbs: int = 106, first_carrier_offset: int = 900, classifier = None):
19
20
  # Flask app setup
20
21
  self.app = Flask(__name__)
21
22
  self.app.config['TEMPLATES_AUTO_RELOAD'] = True
22
23
  self.socketio = SocketIO(self.app)
23
24
 
24
25
  # Parameters
25
- self.BUFFER_SIZE = buffer_size
26
- self.iq_size = iq_size
27
- self.BW = bw
28
- self.CENTER_FREQ = center_freq
26
+ self.buffer_size = buffer_size
27
+ self.ofdm_symbol_size = ofdm_symbol_size
28
+ self.bw = bw
29
+ self.center_freq = center_freq
30
+ self.num_prbs = num_prbs
31
+ self.first_carrier_offset = first_carrier_offset
29
32
 
30
33
  # Route setup
31
34
  self.app.add_url_rule("/", view_func=self.index)
@@ -51,20 +54,12 @@ class Dashboard:
51
54
  self.run_thread.start()
52
55
 
53
56
  def handle_initial_connection(self):
54
- num_prbs = 106
55
- iqs_to_show = int(
56
- self.iq_size / 2
57
- ) # / 2 since the size received is real and imaginary part
58
- # Each PRB corresponds to 12 subcarriers
59
- # Parameter first_carrier_offset (currently set to 900) is the first subcarrier.
60
- # So PRB 0 is [900-911], 1 is [912-923], etc. They wrap around fft_size (now at 1536)
61
- # The carrier frequency is in the middle PRB (including DC leak on some radios), this means it falls at (first_carrier_offset + 106*12/2) % fft_size? where % is the modulo operation
62
- # The first 76 PRB usually have channels that should not be nulled (but we should investigate more options to enable this)
63
- # So if you want the spectrum waterfall to correspond to the PRB masking plot, you can rotate them as per bullet 2 above and drop [635-899]
64
- iqs_to_show -= 264 # is the size of the array to drop
65
57
  data_buffer = {
66
- "magnitude": np.zeros((self.BUFFER_SIZE, iqs_to_show)).tolist(),
67
- "num_prbs": num_prbs,
58
+ "magnitude": np.zeros((self.buffer_size, self.ofdm_symbol_size)).tolist(),
59
+ "center_freq": self.center_freq,
60
+ "bw": self.bw,
61
+ "num_prbs": self.num_prbs,
62
+ "first_carrier_offset": self.first_carrier_offset,
68
63
  "predicted_label": self.classifier is not None
69
64
  }
70
65
  self.socketio.emit("initialize_plot", data_buffer)
@@ -84,8 +79,6 @@ class Dashboard:
84
79
  plot, payload = message
85
80
  if plot == "iq_data":
86
81
  iq_data = payload
87
- if iq_data.size != self.iq_size:
88
- raise ValueError(f'Expected IQ data of size {self.iq_size}, but got {iq_data.size}')
89
82
 
90
83
  self.sampling_counter += 1
91
84
  if self.sampling_counter >= self.sampling_threshold:
@@ -105,7 +98,6 @@ class Dashboard:
105
98
  def stop(self):
106
99
  """Stops the server and kills the thread."""
107
100
  if self.run_thread and self.run_thread.is_alive():
108
- self.socketio.stop()
109
101
  self.run_thread.join()
110
102
 
111
103
  if __name__ == "__main__":