dapps 0.2.1__tar.gz → 0.2.2__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.2.1 → dapps-0.2.2}/PKG-INFO +18 -6
- {dapps-0.2.1 → dapps-0.2.2}/README.md +17 -5
- dapps-0.2.2/VERSION +1 -0
- {dapps-0.2.1 → dapps-0.2.2}/docs/ground_truth_labeling.md +3 -3
- {dapps-0.2.1 → dapps-0.2.2}/examples/spectrum_dapp.py +60 -36
- {dapps-0.2.1 → dapps-0.2.2}/pyproject.toml +4 -2
- {dapps-0.2.1 → dapps-0.2.2}/src/dapp/dapp.py +9 -1
- {dapps-0.2.1 → dapps-0.2.2}/src/e3interface/e3_interface.py +66 -9
- dapps-0.2.2/src/e3interface/latrec.py +78 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/e3interface/libe3_agent.py +68 -8
- {dapps-0.2.1 → dapps-0.2.2}/src/simple/simple_dapp.py +12 -2
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/defs/e3sm_spectrum.asn +77 -3
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/defs/e3sm_spectrum.json +26 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/defs/e3sm_spectrum.proto +7 -1
- dapps-0.2.2/src/spectrum/defs/e3sm_spectrum_pb2.py +43 -0
- dapps-0.2.2/src/spectrum/ru_profiles.py +130 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/spectrum_dapp.py +103 -42
- {dapps-0.2.1 → dapps-0.2.2}/src/visualization/subcarrier/subcarrier_visualizer.py +2 -1
- {dapps-0.2.1 → dapps-0.2.2}/tests/test_indication_dispatch.py +28 -3
- dapps-0.2.2/tests/test_latrec.py +66 -0
- dapps-0.2.2/tests/test_latrec_stamps.py +211 -0
- dapps-0.2.2/tests/test_outbound_send_result.py +55 -0
- dapps-0.2.2/tests/test_ru_profiles.py +98 -0
- dapps-0.2.2/tests/test_spectrum_sm_timestamps_mask.py +80 -0
- dapps-0.2.1/VERSION +0 -1
- dapps-0.2.1/examples/nvidia_dapp.py +0 -43
- dapps-0.2.1/src/nvidia/defs/e3sm_nvidia.asn +0 -48
- dapps-0.2.1/src/nvidia/defs/e3sm_nvidia.json +0 -40
- dapps-0.2.1/src/nvidia/nvidia_dapp.py +0 -74
- dapps-0.2.1/src/spectrum/defs/e3sm_spectrum_pb2.py +0 -43
- dapps-0.2.1/src/visualization/__init__.py +0 -4
- {dapps-0.2.1 → dapps-0.2.2}/.github/CODEOWNERS +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.github/ISSUE_TEMPLATE/documentation.yml +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.github/ISSUE_TEMPLATE/new_sm.yml +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.github/ISSUE_TEMPLATE/question.yml +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.github/workflows/mirror.yml +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.github/workflows/tag-and-release.yml +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.github/workflows/test.yml +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/.gitignore +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/CMakeLists.txt +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/CONTRIBUTING.md +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/CONTRIBUTORS +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/LICENSE +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/MANIFEST.in +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/SECURITY.md +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/docs/dapp_uml.txt +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/docs/gui_uml.txt +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/examples/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/examples/simple_dapp.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/CMakeLists.txt +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/cmake/libiqsaver.pc.in +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/cmake/libiqsaverConfig.cmake.in +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/include/iqsaver/annotation.hpp +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/include/iqsaver/iq_saver_writer.hpp +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/include/iqsaver/iqsaver.hpp +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/include/iqsaver/types.hpp +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/include/iqsaver/version.hpp.in +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/src/iq_saver_writer.cpp +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/swig/iqsaver.i +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/swig/iqsaver_swig.cpp +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/libiqsaver/swig/iqsaver_swig.hpp +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/dapp/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/e3interface/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/e3interface/e3_logging.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/e3interface/e3_transport.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/e3interface/sm_helpers.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/iq_saver/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/iq_saver/iq_saver.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/simple/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/simple/defs/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/simple/defs/e3sm_simple.proto +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/simple/defs/e3sm_simple_pb2.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/README.md +0 -0
- {dapps-0.2.1/src/nvidia → dapps-0.2.2/src/spectrum}/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/adaptive_noise_floor.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/defs/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/defs/e3sm_oai_l1_kpm.asn +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/defs/e3sm_oai_l1_kpm.proto +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/defs/e3sm_oai_l1_kpm_pb2.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/e3_l2_sensing_reader.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/e3_ran_buffers_reader.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/embargo_manager.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/spectrum/threshold_detector.py +0 -0
- {dapps-0.2.1/src/spectrum → dapps-0.2.2/src/visualization}/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/visualization/energy.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/visualization/grafana.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/visualization/influx.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/visualization/iq.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/visualization/subcarrier/insi_white.png +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/visualization/subcarrier/start_subcarrier_visualizer.sh +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/src/visualization/subcarrier_pub.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/start_dapp.sh +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tests/test_e3_concurrency.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tests/test_iq_saver.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tests/test_libe3_agent.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tests/test_protobuf_sm.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tests/test_sensing_policy.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tests/test_spectrum_telemetry.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tools/README.md +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tools/asntojson.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tools/generate_sm.sh +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tools/read_recording.py +0 -0
- {dapps-0.2.1 → dapps-0.2.2}/tools/regen_proto.sh +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dapps
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A library for creating and connecting dApps to Open RAN units.
|
|
5
5
|
Keywords: dApps,O-RAN,5G,cellular,networks
|
|
6
6
|
Author-Email: Andrea Lacava <thecave003@gmail.com>
|
|
@@ -71,6 +71,15 @@ The dApp's `--link` / `--transport` / `--encoding` must match the gNB's
|
|
|
71
71
|
`E3Configuration`. See [libe3's `swig/README.md`](https://github.com/wineslab/libe3/blob/main/swig/README.md)
|
|
72
72
|
for the binding architecture.
|
|
73
73
|
|
|
74
|
+
#### Latency recording (optional)
|
|
75
|
+
|
|
76
|
+
Per-stage latency records (latrec) come from libe3 itself: build the bindings with
|
|
77
|
+
`--cmake-opt "-DLIBE3_ENABLE_LATREC=ON"` and the dApp stamps its E3SM decode,
|
|
78
|
+
processing, output creation, E3SM encode and xApp-policy apply stages into libe3's
|
|
79
|
+
rings, joinable with libe3's own by `tools/latrec2csv.py`. There is no runtime
|
|
80
|
+
switch; against a binding built without it every stamp is a no-op. See libe3's
|
|
81
|
+
`docs/latrec.md`.
|
|
82
|
+
|
|
74
83
|
### Python package installation (recommended)
|
|
75
84
|
|
|
76
85
|
```
|
|
@@ -143,8 +152,8 @@ This dApp implements a spectrum sharing use case discussed in [our paper](https:
|
|
|
143
152
|
|
|
144
153
|
**Spectrum Sensing Configuration:**
|
|
145
154
|
|
|
146
|
-
- `--noise-floor-threshold` - Set the noise floor threshold (in dB) for determining the presence of incumbents and for detecting the PRBs affected. If not specified,
|
|
147
|
-
- `--
|
|
155
|
+
- `--noise-floor-threshold` - Set the noise floor threshold (in dB) for determining the presence of incumbents and for detecting the PRBs affected. If not specified, the `--ru` profile value is used
|
|
156
|
+
- `--ru` / `-ru` - Radio-unit profile (default: `usrp`) seeding the defaults of `--num-prbs`, `--num-subcarrier-spacing`, `--center-freq`, `--noise-floor-threshold`, `--fp16-beta`, `--sample-rate` and `--max-samples-per-file`. Explicit flags always win. Profiles: `usrp` (alias `x310`), `x410`, `x310-3p58`, `colosseum`, `foxconn`, `foxconn-273`, `benetel`, `rfsim`; see `src/spectrum/ru_profiles.py`. Settings a profile does not calibrate fall back to generic defaults with a warning. This replaces the former `--ota` flag
|
|
148
157
|
|
|
149
158
|
**Visualization Options:**
|
|
150
159
|
|
|
@@ -156,10 +165,13 @@ This dApp implements a spectrum sharing use case discussed in [our paper](https:
|
|
|
156
165
|
|
|
157
166
|
**Radio Configuration:**
|
|
158
167
|
|
|
159
|
-
- `--num-prbs` - Number of Physical Resource Blocks in the channel (default: 106)
|
|
160
|
-
- `--num-subcarrier-spacing` - Subcarrier spacing in kHz. Use 30 for FR1 (sub-6 GHz) (default: 30)
|
|
168
|
+
- `--num-prbs` - Number of Physical Resource Blocks in the channel (default: from `--ru`, 106 for `usrp`)
|
|
169
|
+
- `--num-subcarrier-spacing` - Subcarrier spacing in kHz. Use 30 for FR1 (sub-6 GHz) (default: from `--ru`, 30)
|
|
161
170
|
- `--e` - Enable 3/4 FFT sampling mode. Set this flag if the gNB was started with the `-E` option
|
|
162
|
-
- `--center-freq` - Center frequency in Hz (default: 3.6192e9
|
|
171
|
+
- `--center-freq` - Center frequency in Hz (default: from `--ru`, 3.6192e9 for `usrp`)
|
|
172
|
+
- `--sample-rate` - Front-end (ADC) sample rate in Hz, written as the SigMF `core:sample_rate` (default: from `--ru`; the occupied bandwidth when the profile has none)
|
|
173
|
+
- `--fp16-beta` - FP16 IQ rescale factor, must match the gNB build/config (default: from `--ru`, 1/128 for USRPs and 1/2048 otherwise)
|
|
174
|
+
- `--max-samples-per-file` - SigMF segment length in samples (default: one second of the write rate, `num_prbs * 12 * SCS`)
|
|
163
175
|
|
|
164
176
|
**Execution Control:**
|
|
165
177
|
|
|
@@ -24,6 +24,15 @@ The dApp's `--link` / `--transport` / `--encoding` must match the gNB's
|
|
|
24
24
|
`E3Configuration`. See [libe3's `swig/README.md`](https://github.com/wineslab/libe3/blob/main/swig/README.md)
|
|
25
25
|
for the binding architecture.
|
|
26
26
|
|
|
27
|
+
#### Latency recording (optional)
|
|
28
|
+
|
|
29
|
+
Per-stage latency records (latrec) come from libe3 itself: build the bindings with
|
|
30
|
+
`--cmake-opt "-DLIBE3_ENABLE_LATREC=ON"` and the dApp stamps its E3SM decode,
|
|
31
|
+
processing, output creation, E3SM encode and xApp-policy apply stages into libe3's
|
|
32
|
+
rings, joinable with libe3's own by `tools/latrec2csv.py`. There is no runtime
|
|
33
|
+
switch; against a binding built without it every stamp is a no-op. See libe3's
|
|
34
|
+
`docs/latrec.md`.
|
|
35
|
+
|
|
27
36
|
### Python package installation (recommended)
|
|
28
37
|
|
|
29
38
|
```
|
|
@@ -96,8 +105,8 @@ This dApp implements a spectrum sharing use case discussed in [our paper](https:
|
|
|
96
105
|
|
|
97
106
|
**Spectrum Sensing Configuration:**
|
|
98
107
|
|
|
99
|
-
- `--noise-floor-threshold` - Set the noise floor threshold (in dB) for determining the presence of incumbents and for detecting the PRBs affected. If not specified,
|
|
100
|
-
- `--
|
|
108
|
+
- `--noise-floor-threshold` - Set the noise floor threshold (in dB) for determining the presence of incumbents and for detecting the PRBs affected. If not specified, the `--ru` profile value is used
|
|
109
|
+
- `--ru` / `-ru` - Radio-unit profile (default: `usrp`) seeding the defaults of `--num-prbs`, `--num-subcarrier-spacing`, `--center-freq`, `--noise-floor-threshold`, `--fp16-beta`, `--sample-rate` and `--max-samples-per-file`. Explicit flags always win. Profiles: `usrp` (alias `x310`), `x410`, `x310-3p58`, `colosseum`, `foxconn`, `foxconn-273`, `benetel`, `rfsim`; see `src/spectrum/ru_profiles.py`. Settings a profile does not calibrate fall back to generic defaults with a warning. This replaces the former `--ota` flag
|
|
101
110
|
|
|
102
111
|
**Visualization Options:**
|
|
103
112
|
|
|
@@ -109,10 +118,13 @@ This dApp implements a spectrum sharing use case discussed in [our paper](https:
|
|
|
109
118
|
|
|
110
119
|
**Radio Configuration:**
|
|
111
120
|
|
|
112
|
-
- `--num-prbs` - Number of Physical Resource Blocks in the channel (default: 106)
|
|
113
|
-
- `--num-subcarrier-spacing` - Subcarrier spacing in kHz. Use 30 for FR1 (sub-6 GHz) (default: 30)
|
|
121
|
+
- `--num-prbs` - Number of Physical Resource Blocks in the channel (default: from `--ru`, 106 for `usrp`)
|
|
122
|
+
- `--num-subcarrier-spacing` - Subcarrier spacing in kHz. Use 30 for FR1 (sub-6 GHz) (default: from `--ru`, 30)
|
|
114
123
|
- `--e` - Enable 3/4 FFT sampling mode. Set this flag if the gNB was started with the `-E` option
|
|
115
|
-
- `--center-freq` - Center frequency in Hz (default: 3.6192e9
|
|
124
|
+
- `--center-freq` - Center frequency in Hz (default: from `--ru`, 3.6192e9 for `usrp`)
|
|
125
|
+
- `--sample-rate` - Front-end (ADC) sample rate in Hz, written as the SigMF `core:sample_rate` (default: from `--ru`; the occupied bandwidth when the profile has none)
|
|
126
|
+
- `--fp16-beta` - FP16 IQ rescale factor, must match the gNB build/config (default: from `--ru`, 1/128 for USRPs and 1/2048 otherwise)
|
|
127
|
+
- `--max-samples-per-file` - SigMF segment length in samples (default: one second of the write rate, `num_prbs * 12 * SCS`)
|
|
116
128
|
|
|
117
129
|
**Execution Control:**
|
|
118
130
|
|
dapps-0.2.2/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.2
|
|
@@ -44,9 +44,9 @@ I/Q recording is only active when `--save-iqs` is passed. Without it, `--ground-
|
|
|
44
44
|
## Commands
|
|
45
45
|
|
|
46
46
|
> [!NOTE]
|
|
47
|
-
> `--
|
|
48
|
-
> is **ignored** whenever `--noise-floor-threshold` is explicitly provided — the
|
|
49
|
-
> always takes precedence regardless of `--
|
|
47
|
+
> `--ru usrp` (the default) sets the noise floor to 20 dB, a value calibrated for the X310 USRP.
|
|
48
|
+
> That default is **ignored** whenever `--noise-floor-threshold` is explicitly provided — the
|
|
49
|
+
> explicit value always takes precedence regardless of `--ru`.
|
|
50
50
|
|
|
51
51
|
The commands below use 53 dB as the noise floor threshold, which matches both the Colosseum
|
|
52
52
|
testbed and the Foxconn RU calibration. Center frequency is 3.75 GHz with 106 PRBs.
|
|
@@ -14,6 +14,12 @@ from spectrum.spectrum_dapp import (
|
|
|
14
14
|
compute_fft_size,
|
|
15
15
|
make_periodic_toggle_callback,
|
|
16
16
|
)
|
|
17
|
+
from spectrum.ru_profiles import (
|
|
18
|
+
DEFAULT_RU,
|
|
19
|
+
RU_CHOICES,
|
|
20
|
+
resolve_ru_settings,
|
|
21
|
+
uncalibrated_keys,
|
|
22
|
+
)
|
|
17
23
|
from spectrum.threshold_detector import StaticThresholdDetector, AdaptiveThresholdDetector
|
|
18
24
|
|
|
19
25
|
LOG_DIR = '/tmp/'
|
|
@@ -37,18 +43,27 @@ def main(args):
|
|
|
37
43
|
"--time-window/--moving-avg-window/--extraction-window flags."
|
|
38
44
|
)
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
noise_floor_threshold
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
settings = resolve_ru_settings(args.ru, {
|
|
47
|
+
'num_prbs': args.num_prbs,
|
|
48
|
+
'num_subcarrier_spacing': args.num_subcarrier_spacing,
|
|
49
|
+
'center_freq': args.center_freq,
|
|
50
|
+
'noise_floor_threshold': args.noise_floor_threshold,
|
|
51
|
+
'fp16_beta': args.fp16_beta,
|
|
52
|
+
'front_end_sample_rate': args.sample_rate,
|
|
53
|
+
'max_samples_per_file': args.max_samples_per_file,
|
|
54
|
+
})
|
|
55
|
+
print(f"Using radio-unit profile '{args.ru}': {settings}")
|
|
56
|
+
uncalibrated = [
|
|
57
|
+
key for key in uncalibrated_keys(args.ru)
|
|
58
|
+
if getattr(args, key, None) is None
|
|
59
|
+
]
|
|
60
|
+
if uncalibrated:
|
|
61
|
+
print(f"[WARNING] Profile '{args.ru}' has no calibrated value for "
|
|
62
|
+
f"{', '.join(uncalibrated)}; using generic defaults")
|
|
63
|
+
|
|
64
|
+
# The noise floor depends on the RF conditions and the RU and should be
|
|
65
|
+
# carefully calibrated.
|
|
66
|
+
noise_floor_threshold = settings['noise_floor_threshold']
|
|
52
67
|
|
|
53
68
|
print(f'Threshold is {noise_floor_threshold}')
|
|
54
69
|
|
|
@@ -56,7 +71,7 @@ def main(args):
|
|
|
56
71
|
# Build the detection strategy explicitly so the example is the
|
|
57
72
|
# authoritative place for detector configuration.
|
|
58
73
|
# ------------------------------------------------------------------
|
|
59
|
-
fft_size = compute_fft_size(
|
|
74
|
+
fft_size = compute_fft_size(settings['num_prbs'], args.e)
|
|
60
75
|
|
|
61
76
|
if args.use_adaptive_noise_floor:
|
|
62
77
|
detector = AdaptiveThresholdDetector(
|
|
@@ -95,13 +110,14 @@ def main(args):
|
|
|
95
110
|
viz_web_port=args.viz_web_port,
|
|
96
111
|
viz_zmq_port=args.viz_zmq_port,
|
|
97
112
|
external_viz=args.external_viz,
|
|
98
|
-
center_freq=
|
|
99
|
-
num_prbs=
|
|
113
|
+
center_freq=settings['center_freq'],
|
|
114
|
+
num_prbs=settings['num_prbs'],
|
|
100
115
|
e_sampling=args.e,
|
|
101
|
-
num_subcarrier_spacing=
|
|
116
|
+
num_subcarrier_spacing=settings['num_subcarrier_spacing'],
|
|
102
117
|
sampling_threshold=args.sampling_threshold,
|
|
103
|
-
max_samples_per_file=
|
|
104
|
-
fp16_beta=
|
|
118
|
+
max_samples_per_file=settings['max_samples_per_file'],
|
|
119
|
+
fp16_beta=settings['fp16_beta'],
|
|
120
|
+
front_end_sample_rate=settings['front_end_sample_rate'],
|
|
105
121
|
sensing_only=args.sensing_only,
|
|
106
122
|
strict_sensing=args.strict_sensing,
|
|
107
123
|
min_sensing_symbols=args.min_sensing_symbols,
|
|
@@ -191,16 +207,21 @@ if __name__ == "__main__":
|
|
|
191
207
|
help="Send PRB-block control messages to the gNB when "
|
|
192
208
|
"PRBs are detected above the noise threshold")
|
|
193
209
|
parser.add_argument('--noise-floor-threshold', type=int, default=None,
|
|
194
|
-
help="Detection threshold in dB (static) or dB above noise floor (adaptive)"
|
|
210
|
+
help="Detection threshold in dB (static) or dB above noise floor (adaptive); "
|
|
211
|
+
"default: from --ru")
|
|
195
212
|
parser.add_argument('--use-adaptive-noise-floor', action='store_true', default=False,
|
|
196
213
|
help="Use per-bin median noise floor estimation instead of a fixed threshold")
|
|
197
214
|
parser.add_argument('--embargo-timeout-secs', type=float, default=10.1,
|
|
198
215
|
help="Hold time in seconds for embargoed PRBs after last detection (adaptive mode)")
|
|
199
216
|
parser.add_argument('--average-over-frames', type=int, default=64,
|
|
200
217
|
help="Number of frames to average before each decision")
|
|
201
|
-
parser.add_argument('--
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
parser.add_argument('--ru', '-ru', type=str, default=DEFAULT_RU,
|
|
219
|
+
choices=RU_CHOICES,
|
|
220
|
+
help="Radio-unit profile seeding the defaults of --num-prbs, "
|
|
221
|
+
"--num-subcarrier-spacing, --center-freq, "
|
|
222
|
+
"--noise-floor-threshold, --fp16-beta, --sample-rate and "
|
|
223
|
+
"--max-samples-per-file. Explicit flags always win. "
|
|
224
|
+
f"Default: {DEFAULT_RU}.")
|
|
204
225
|
parser.add_argument('--energy-gui', action='store_true', default=False,
|
|
205
226
|
help="Enable energy spectrum visualization")
|
|
206
227
|
parser.add_argument('--iq-plotter-gui', action='store_true', default=False,
|
|
@@ -213,14 +234,18 @@ if __name__ == "__main__":
|
|
|
213
234
|
help="ZMQ port the dApp publishes on / the visualizer reads (default 5559)")
|
|
214
235
|
parser.add_argument('--external-viz', action='store_true', default=False,
|
|
215
236
|
help="Don't spawn the visualizer; publish to an already-running one")
|
|
216
|
-
parser.add_argument('--num-prbs', type=int, default=
|
|
217
|
-
help="Number of PRBs")
|
|
218
|
-
parser.add_argument('--num-subcarrier-spacing', type=int, default=
|
|
219
|
-
help="Subcarrier spacing in kHz (FR1 = 30)")
|
|
237
|
+
parser.add_argument('--num-prbs', type=int, default=None,
|
|
238
|
+
help="Number of PRBs (default: from --ru)")
|
|
239
|
+
parser.add_argument('--num-subcarrier-spacing', type=int, default=None,
|
|
240
|
+
help="Subcarrier spacing in kHz (FR1 = 30; default: from --ru)")
|
|
220
241
|
parser.add_argument('--e', action='store_true', default=False,
|
|
221
242
|
help="Enable 3/4 FFT sampling (OAI -E flag for USRPs)")
|
|
222
|
-
parser.add_argument('--center-freq', type=float, default=
|
|
223
|
-
help="RF center frequency in Hz")
|
|
243
|
+
parser.add_argument('--center-freq', type=float, default=None,
|
|
244
|
+
help="RF center frequency in Hz (default: from --ru)")
|
|
245
|
+
parser.add_argument('--sample-rate', type=float, default=None, metavar='HZ',
|
|
246
|
+
help="Front-end (ADC) sample rate in Hz, written as the SigMF "
|
|
247
|
+
"core:sample_rate (default: from --ru; when the profile has "
|
|
248
|
+
"none, the occupied bandwidth)")
|
|
224
249
|
parser.add_argument('--timed', type=int, default=0, metavar='SECONDS',
|
|
225
250
|
help="Stop automatically after SECONDS (0 = run indefinitely)")
|
|
226
251
|
parser.add_argument('--model', type=str, default='',
|
|
@@ -233,23 +258,22 @@ if __name__ == "__main__":
|
|
|
233
258
|
help="Samples to retain after CNN energy peak detection")
|
|
234
259
|
parser.add_argument('--sampling-threshold', type=int, default=5,
|
|
235
260
|
help="Render a new dashboard frame every N IQ batches (visualization only, does not affect IQ delivery or recording)")
|
|
236
|
-
parser.add_argument('--max-samples-per-file', type=int, default=
|
|
261
|
+
parser.add_argument('--max-samples-per-file', type=int, default=None,
|
|
237
262
|
help="Rotate the SigMF capture file once a segment reaches "
|
|
238
|
-
"this many true IQ samples (default
|
|
263
|
+
"this many true IQ samples (default: one second of the "
|
|
264
|
+
"dApp's write rate, num_prbs * 12 * SCS). Each "
|
|
239
265
|
"indication is kept whole, so a segment may exceed the "
|
|
240
266
|
"threshold by up to one indication's worth of samples. "
|
|
241
|
-
"Segment wall-clock duration depends on the capture rate. "
|
|
242
267
|
"Only used with --save-iqs.")
|
|
243
268
|
parser.add_argument('--ground-truth', type=str, default='', metavar='LABEL',
|
|
244
269
|
help="Initial ground truth label written into IQ annotations "
|
|
245
270
|
"(only used with --save-iqs). "
|
|
246
271
|
"Updatable at runtime via the dashboard GUI "
|
|
247
272
|
"when --demo-gui is also set.")
|
|
248
|
-
parser.add_argument('--fp16-beta', type=float, default=
|
|
249
|
-
help="FP16 IQ rescale factor; MUST match the gNB "
|
|
250
|
-
"
|
|
251
|
-
"1/
|
|
252
|
-
"the X410 sample conf overrides it to 0.0078125 (1/128).")
|
|
273
|
+
parser.add_argument('--fp16-beta', type=float, default=None,
|
|
274
|
+
help="FP16 IQ rescale factor; MUST match the gNB build/config "
|
|
275
|
+
"(the reader rescales by 1/beta). Default from --ru: "
|
|
276
|
+
"1/128 for USRPs, 1/2048 otherwise.")
|
|
253
277
|
parser.add_argument('--encoding-method', type=str, default='asn1',
|
|
254
278
|
choices=['asn1', 'json', 'protobuf'],
|
|
255
279
|
help="Wire encoding for Spectrum-* envelopes (default: asn1).")
|
|
@@ -20,7 +20,10 @@ classifiers = [
|
|
|
20
20
|
license = "Apache-2.0"
|
|
21
21
|
license-files = ["LICENSE"]
|
|
22
22
|
# NOTE: E3AP transport is provided by libe3's Python bindings (`libe3py`),
|
|
23
|
-
# which are not on PyPI.
|
|
23
|
+
# which are not on PyPI. **libe3 >= 0.2.0 is required**: 0.2.0 added the
|
|
24
|
+
# correlation id that DAppSession.send_report() now takes as a mandatory
|
|
25
|
+
# argument, so an older binding fails at the E3 setup handshake against a
|
|
26
|
+
# matching gNB. Install them into this environment with:
|
|
24
27
|
# cd ~/libe3 && ./build_libe3 --install --enable-swig \
|
|
25
28
|
# --cmake-opt "-DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=ON"
|
|
26
29
|
# (asn1tools is still required for the in-Python service-model codecs.)
|
|
@@ -76,7 +79,6 @@ wheel.packages = [
|
|
|
76
79
|
"src/dapp",
|
|
77
80
|
"src/e3interface",
|
|
78
81
|
"src/spectrum",
|
|
79
|
-
"src/nvidia",
|
|
80
82
|
"src/simple",
|
|
81
83
|
"src/iq_saver",
|
|
82
84
|
"src/visualization",
|
|
@@ -143,7 +143,15 @@ class DApp(ABC):
|
|
|
143
143
|
return scheduled
|
|
144
144
|
|
|
145
145
|
@abstractmethod
|
|
146
|
-
def _handle_xapp_control(self, dapp_identifier: int, data: bytes):
|
|
146
|
+
def _handle_xapp_control(self, dapp_identifier: int, data: bytes, sequence_id: int = 0):
|
|
147
|
+
"""Handle a control addressed to this dApp by an xApp.
|
|
148
|
+
|
|
149
|
+
``sequence_id`` is the correlation id of the xApp procedure. Echo it on
|
|
150
|
+
the control re-issued to the RAN so the RAN can report back when the
|
|
151
|
+
decision reached the air. Implementations written before the id existed
|
|
152
|
+
may still take two arguments; E3Interface detects that and calls them
|
|
153
|
+
the old way.
|
|
154
|
+
"""
|
|
147
155
|
# This in the future might become a class
|
|
148
156
|
pass
|
|
149
157
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import inspect
|
|
1
2
|
import queue
|
|
2
3
|
import threading
|
|
3
4
|
import time
|
|
4
5
|
|
|
6
|
+
from . import latrec
|
|
5
7
|
from .e3_logging import e3_logger
|
|
6
8
|
from .libe3_agent import (
|
|
7
9
|
Libe3Agent,
|
|
@@ -23,6 +25,34 @@ POLL_TIMEOUT_MS = 100
|
|
|
23
25
|
SETUP_WAIT_MS = 6000
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def _accepts_sequence_id(callback) -> bool:
|
|
29
|
+
"""True if ``callback`` takes the trailing correlation id.
|
|
30
|
+
|
|
31
|
+
Cached on the function object: this runs per xApp control, and
|
|
32
|
+
inspect.signature() is not cheap. A callback that takes *args is assumed to
|
|
33
|
+
want it.
|
|
34
|
+
"""
|
|
35
|
+
cached = getattr(callback, "_e3_accepts_seq", None)
|
|
36
|
+
if cached is not None:
|
|
37
|
+
return cached
|
|
38
|
+
try:
|
|
39
|
+
params = inspect.signature(callback).parameters
|
|
40
|
+
accepts = (
|
|
41
|
+
len([p for p in params.values()
|
|
42
|
+
if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)]) >= 3
|
|
43
|
+
or any(p.kind is p.VAR_POSITIONAL for p in params.values())
|
|
44
|
+
)
|
|
45
|
+
except (TypeError, ValueError):
|
|
46
|
+
# Builtins and C callables have no introspectable signature; assume the
|
|
47
|
+
# older two-argument form, which is what every dApp shipped before.
|
|
48
|
+
accepts = False
|
|
49
|
+
try:
|
|
50
|
+
callback._e3_accepts_seq = accepts
|
|
51
|
+
except AttributeError:
|
|
52
|
+
pass
|
|
53
|
+
return accepts
|
|
54
|
+
|
|
55
|
+
|
|
26
56
|
class E3Interface:
|
|
27
57
|
"""Singleton E3AP interface, backed by the libe3 dApp session (``libe3py``).
|
|
28
58
|
|
|
@@ -218,6 +248,7 @@ class E3Interface:
|
|
|
218
248
|
def _inbound_connection(self):
|
|
219
249
|
"""Batched drain of libe3 inbound events into the callback dispatchers."""
|
|
220
250
|
e3_logger.info("Start inbound loop")
|
|
251
|
+
latrec.open_ring("dapp.inbound")
|
|
221
252
|
last_dropped = 0
|
|
222
253
|
try:
|
|
223
254
|
while not self.stop_event.is_set():
|
|
@@ -228,11 +259,16 @@ class E3Interface:
|
|
|
228
259
|
try:
|
|
229
260
|
kind = ev.kind
|
|
230
261
|
if kind == EVENT_INDICATION:
|
|
262
|
+
if latrec.ENABLED:
|
|
263
|
+
latrec.ctx_set(ev.trace_seq)
|
|
231
264
|
self._handle_indication_data(
|
|
232
265
|
ev.dapp_id, ev.ran_function_id, ev.get_payload())
|
|
233
266
|
elif kind == EVENT_XAPP_CONTROL:
|
|
267
|
+
if latrec.ENABLED:
|
|
268
|
+
latrec.ctx_set(ev.request_id or ev.trace_seq)
|
|
234
269
|
self._handle_xapp_control_data(
|
|
235
|
-
ev.dapp_id, ev.ran_function_id, ev.get_payload()
|
|
270
|
+
ev.dapp_id, ev.ran_function_id, ev.get_payload(),
|
|
271
|
+
ev.sequence_id)
|
|
236
272
|
elif kind == EVENT_SUBSCRIPTION_RESPONSE:
|
|
237
273
|
self._handle_subscription_response({
|
|
238
274
|
'dAppIdentifier': ev.dapp_id,
|
|
@@ -269,6 +305,7 @@ class E3Interface:
|
|
|
269
305
|
def _outbound_connection(self):
|
|
270
306
|
"""Drain the outbound queue and forward each message through libe3."""
|
|
271
307
|
e3_logger.info("Start outbound loop")
|
|
308
|
+
latrec.open_ring("dapp.outbound")
|
|
272
309
|
try:
|
|
273
310
|
while not self.stop_event.is_set():
|
|
274
311
|
try:
|
|
@@ -283,23 +320,32 @@ class E3Interface:
|
|
|
283
320
|
try:
|
|
284
321
|
e3_logger.debug("Outbound queue has got '%s', %s", msg, data)
|
|
285
322
|
rc = SUCCESS
|
|
323
|
+
# libe3 stamps the producer's key into EMIT_ENTER from this
|
|
324
|
+
# thread's context; reset it so an ack does not inherit a
|
|
325
|
+
# stale one.
|
|
326
|
+
latrec.ctx_set(data.get("latSeq", 0))
|
|
286
327
|
match msg:
|
|
287
328
|
case "control":
|
|
288
329
|
rc = self.agent.send_control(
|
|
289
|
-
data["ranFunctionId"], data["controlId"], data["actionData"]
|
|
330
|
+
data["ranFunctionId"], data["controlId"], data["actionData"],
|
|
331
|
+
data.get("sequenceId", 0))
|
|
290
332
|
case "subscription":
|
|
291
333
|
self._send_subscription(data)
|
|
292
334
|
case "ack":
|
|
293
335
|
rc = self.agent.send_message_ack(data["requestId"], data["positive"])
|
|
294
336
|
case "report":
|
|
295
|
-
rc = self.agent.send_report(
|
|
337
|
+
rc = self.agent.send_report(
|
|
338
|
+
data["ranFunctionId"], data["reportData"],
|
|
339
|
+
data.get("sequenceId", 0))
|
|
296
340
|
case "release":
|
|
297
341
|
rc = self.agent.release()
|
|
298
342
|
case _:
|
|
299
343
|
e3_logger.error("Unrecognized outbound message: %r; dropping", msg)
|
|
300
344
|
continue
|
|
301
345
|
|
|
302
|
-
|
|
346
|
+
# A send returns the assigned message id (positive) on success
|
|
347
|
+
# and a negative ErrorCode on failure.
|
|
348
|
+
if rc < 0:
|
|
303
349
|
e3_logger.error("libe3 %s send failed (ErrorCode=%s)", msg, rc)
|
|
304
350
|
except Exception:
|
|
305
351
|
e3_logger.exception(
|
|
@@ -390,31 +436,42 @@ class E3Interface:
|
|
|
390
436
|
else:
|
|
391
437
|
e3_logger.warning(f"No indication callback registered for dApp {dapp_identifier}")
|
|
392
438
|
|
|
393
|
-
def _handle_xapp_control_data(self, dapp_identifier, ran_function_id, xapp_control_data
|
|
439
|
+
def _handle_xapp_control_data(self, dapp_identifier, ran_function_id, xapp_control_data,
|
|
440
|
+
sequence_id=0):
|
|
394
441
|
e3_logger.debug(
|
|
395
|
-
"Received xAppControlAction: dApp=%s, ranFunc=%s, payload=%d bytes",
|
|
396
|
-
dapp_identifier, ran_function_id, len(xapp_control_data),
|
|
442
|
+
"Received xAppControlAction: dApp=%s, ranFunc=%s, payload=%d bytes, seq=%s",
|
|
443
|
+
dapp_identifier, ran_function_id, len(xapp_control_data), sequence_id,
|
|
397
444
|
)
|
|
398
445
|
with self._callback_lock:
|
|
399
446
|
callbacks = list(self.xapp_control_callbacks.get(dapp_identifier, []))
|
|
400
447
|
if callbacks:
|
|
401
448
|
e3_logger.debug(f"Launch {len(callbacks)} xApp control callback(s) for dApp {dapp_identifier}")
|
|
402
449
|
for callback in callbacks:
|
|
403
|
-
|
|
450
|
+
# Callbacks predating the correlation id take two arguments. Decide
|
|
451
|
+
# from the signature rather than catching TypeError, which would
|
|
452
|
+
# re-invoke a callback that raised one of its own.
|
|
453
|
+
if _accepts_sequence_id(callback):
|
|
454
|
+
callback(dapp_identifier, xapp_control_data, sequence_id)
|
|
455
|
+
else:
|
|
456
|
+
callback(dapp_identifier, xapp_control_data)
|
|
404
457
|
else:
|
|
405
458
|
e3_logger.warning(f"No xApp control callback registered for dApp {dapp_identifier}")
|
|
406
459
|
|
|
407
|
-
def schedule_control(self, dappId: int, ranFunctionId: int, controlId: int, actionData: bytes = b""
|
|
460
|
+
def schedule_control(self, dappId: int, ranFunctionId: int, controlId: int, actionData: bytes = b"",
|
|
461
|
+
sequenceId: int = 0):
|
|
408
462
|
self.outbound_queue.put(('control', {
|
|
409
463
|
'ranFunctionId': ranFunctionId,
|
|
410
464
|
'controlId': controlId,
|
|
411
465
|
'actionData': actionData,
|
|
466
|
+
'sequenceId': sequenceId,
|
|
467
|
+
'latSeq': latrec.ctx(),
|
|
412
468
|
}))
|
|
413
469
|
|
|
414
470
|
def schedule_report(self, dappId: int, ranFunctionId: int, reportData: bytes):
|
|
415
471
|
self.outbound_queue.put(('report', {
|
|
416
472
|
'ranFunctionId': ranFunctionId,
|
|
417
473
|
'reportData': reportData,
|
|
474
|
+
'latSeq': latrec.ctx(),
|
|
418
475
|
}))
|
|
419
476
|
|
|
420
477
|
def add_subscription_callback(self, dapp_id: int, callback):
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""latrec stamping for the dApp, through libe3's own recorder.
|
|
2
|
+
|
|
3
|
+
libe3 owns the ring format, the stage catalog and the on/off decision (a build
|
|
4
|
+
option, LIBE3_ENABLE_LATREC; there is no runtime switch). This module only binds
|
|
5
|
+
the per-thread stamping calls and turns them into plain no-ops when the installed
|
|
6
|
+
libe3py was built without the recorder. Stage ids come from libe3py, never from a
|
|
7
|
+
local table, so they cannot drift from the catalog.
|
|
8
|
+
|
|
9
|
+
Rings are per thread: a thread must ``open_ring`` before its stamps and
|
|
10
|
+
``ctx_set`` calls take effect. Threads that never open one stamp nothing.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import libe3py as _l
|
|
15
|
+
except ImportError: # pragma: no cover - environment dependent
|
|
16
|
+
_l = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _recorder_built_in() -> bool:
|
|
20
|
+
# A build without the recorder compiles latrec_seq_next() to a constant 0.
|
|
21
|
+
if _l is None or not hasattr(_l, "latrec_tstamp_py"):
|
|
22
|
+
return False
|
|
23
|
+
try:
|
|
24
|
+
return _l.latrec_seq_next_py() != 0
|
|
25
|
+
except Exception:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ENABLED = _recorder_built_in()
|
|
30
|
+
|
|
31
|
+
_U64 = 0xFFFFFFFFFFFFFFFF
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _id(name: str) -> int:
|
|
35
|
+
return getattr(_l, name, 0)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
DECODE_E3SM_BEGIN = _id("LATREC_DECODE_E3SM_BEGIN")
|
|
39
|
+
DECODE_E3SM_DONE = _id("LATREC_DECODE_E3SM_DONE")
|
|
40
|
+
PROCESS_DONE = _id("LATREC_PROCESS_DONE")
|
|
41
|
+
CREATE_OUTPUT = _id("LATREC_CREATE_OUTPUT")
|
|
42
|
+
ENCODE_E3SM_BEGIN = _id("LATREC_ENCODE_E3SM_BEGIN")
|
|
43
|
+
ENCODE_E3SM_DONE = _id("LATREC_ENCODE_E3SM_DONE")
|
|
44
|
+
APPLY_POLICY_DONE = _id("LATREC_APPLY_POLICY_DONE")
|
|
45
|
+
|
|
46
|
+
PDU_INDICATION = _id("PduType_INDICATION_MESSAGE")
|
|
47
|
+
PDU_XAPP_CONTROL = _id("PduType_XAPP_CONTROL_ACTION")
|
|
48
|
+
PDU_CONTROL = _id("PduType_DAPP_CONTROL_ACTION")
|
|
49
|
+
PDU_REPORT = _id("PduType_DAPP_REPORT")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _noop_stamp(seq, stage, aux=0, aux2=0):
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _noop(_arg):
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _zero():
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if ENABLED:
|
|
65
|
+
_tstamp = _l.latrec_tstamp_py
|
|
66
|
+
|
|
67
|
+
def stamp(seq, stage, aux=0, aux2=0):
|
|
68
|
+
_tstamp(seq, stage, int(aux) & _U64, int(aux2) & _U64)
|
|
69
|
+
|
|
70
|
+
open_ring = _l.latrec_tls_open_as_py
|
|
71
|
+
ctx_set = _l.latrec_ctx_set_py
|
|
72
|
+
ctx = _l.latrec_ctx_py
|
|
73
|
+
seq_next = _l.latrec_seq_next_py
|
|
74
|
+
set_output_dir = _l.latrec_set_output_dir_py
|
|
75
|
+
else:
|
|
76
|
+
stamp = _noop_stamp
|
|
77
|
+
open_ring = ctx_set = set_output_dir = _noop
|
|
78
|
+
ctx = seq_next = _zero
|