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.
- {dapps-0.0.2 → dapps-0.0.4}/.github/workflows/tag-and-release.yml +10 -9
- dapps-0.0.4/PKG-INFO +114 -0
- dapps-0.0.4/README.md +70 -0
- dapps-0.0.4/VERSION +1 -0
- dapps-0.0.4/examples/spectrum_dapp.py +110 -0
- dapps-0.0.4/pyproject.toml +66 -0
- {dapps-0.0.2 → dapps-0.0.4}/src/dapp/dapp.py +1 -1
- {dapps-0.0.2 → dapps-0.0.4}/src/e3interface/defs/e3.asn +1 -1
- {dapps-0.0.2 → dapps-0.0.4}/src/spectrum/spectrum_dapp.py +40 -31
- {dapps-0.0.2 → dapps-0.0.4}/src/visualization/dashboard.py +14 -22
- {dapps-0.0.2 → dapps-0.0.4}/src/visualization/iq.py +4 -4
- dapps-0.0.4/src/visualization/templates/index.html +308 -0
- dapps-0.0.2/PKG-INFO +0 -83
- dapps-0.0.2/README.md +0 -42
- dapps-0.0.2/VERSION +0 -1
- dapps-0.0.2/examples/spectrum_dapp.py +0 -69
- dapps-0.0.2/pyproject.toml +0 -58
- dapps-0.0.2/src/visualization/templates/index.html +0 -214
- {dapps-0.0.2 → dapps-0.0.4}/.github/workflows/mirror.yml +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/.gitignore +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/LICENSE +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/MANIFEST.in +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/docker/Dockerfile +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/docker/build.sh +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/docs/dapp_uml.txt +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/docs/gui_uml.txt +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/e3protocol/README.md +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/e3protocol/build_asn.sh +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/e3protocol/client/client.c +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/e3protocol/dump_packets.sh +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/e3protocol/server/server.py +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/e3protocol/wrapper/e3.c +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/e3protocol/wrapper/e3.h +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/src/dapp/__init__.py +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/src/e3interface/__init__.py +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/src/e3interface/e3_connector.py +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/src/e3interface/e3_interface.py +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/src/e3interface/e3_logging.py +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/src/spectrum/__init__.py +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/src/visualization/__init__.py +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/src/visualization/energy.py +0 -0
- {dapps-0.0.2 → dapps-0.0.4}/src/visualization/grafana.py +0 -0
- {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.
|
|
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:
|
|
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:
|
|
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:
|
|
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 = '
|
|
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)
|
|
@@ -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
|
|
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.
|
|
25
|
-
# Noise floor threshold needs to be calibrated
|
|
26
|
-
# We receive the symbols
|
|
27
|
-
|
|
28
|
-
def __init__(self, id: int = 1,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
self.
|
|
34
|
-
self.
|
|
35
|
-
self.
|
|
36
|
-
self.
|
|
37
|
-
self.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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.
|
|
104
|
-
abs_iq_av_db = 20 * np.log10(1 + (self.abs_iq_av/(self.
|
|
105
|
-
abs_iq_av_db_offset_correct = np.append(abs_iq_av_db[self.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
18
|
-
|
|
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.
|
|
26
|
-
self.
|
|
27
|
-
self.
|
|
28
|
-
self.
|
|
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.
|
|
67
|
-
"
|
|
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__":
|