dapps 0.2.2__tar.gz → 0.3.0__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.3.0/.github/workflows/test.yml +38 -0
- {dapps-0.2.2 → dapps-0.3.0}/PKG-INFO +16 -10
- {dapps-0.2.2 → dapps-0.3.0}/README.md +14 -9
- dapps-0.3.0/VERSION +1 -0
- dapps-0.3.0/examples/hackathon_policy.py +27 -0
- {dapps-0.2.2 → dapps-0.3.0}/examples/spectrum_dapp.py +33 -2
- {dapps-0.2.2 → dapps-0.3.0}/pyproject.toml +6 -7
- dapps-0.3.0/src/dapp/control_server.py +126 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/libe3_agent.py +10 -10
- dapps-0.3.0/src/spectrum/README.md +104 -0
- dapps-0.3.0/src/spectrum/control_handlers.py +150 -0
- dapps-0.3.0/src/spectrum/policy.py +171 -0
- dapps-0.3.0/src/spectrum/slot_recorder.py +263 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/spectrum_dapp.py +231 -36
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/threshold_detector.py +54 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/visualization/subcarrier/start_subcarrier_visualizer.sh +10 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/visualization/subcarrier/subcarrier_visualizer.py +266 -32
- {dapps-0.2.2 → dapps-0.3.0}/src/visualization/subcarrier_pub.py +71 -8
- dapps-0.3.0/tests/test_control_handlers.py +331 -0
- dapps-0.3.0/tests/test_control_server.py +95 -0
- dapps-0.3.0/tests/test_detector_configure.py +60 -0
- dapps-0.3.0/tests/test_detector_granularity_pipeline.py +171 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_libe3_agent.py +2 -2
- dapps-0.3.0/tests/test_policy.py +120 -0
- dapps-0.3.0/tests/test_subcarrier_pub_viz.py +111 -0
- dapps-0.3.0/tests/test_visualizer_grid.py +70 -0
- dapps-0.2.2/.github/workflows/test.yml +0 -29
- dapps-0.2.2/VERSION +0 -1
- dapps-0.2.2/src/spectrum/README.md +0 -56
- {dapps-0.2.2 → dapps-0.3.0}/.github/CODEOWNERS +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/documentation.yml +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/new_sm.yml +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/question.yml +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/.github/workflows/mirror.yml +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/.github/workflows/tag-and-release.yml +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/.gitignore +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/CMakeLists.txt +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/CONTRIBUTING.md +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/CONTRIBUTORS +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/LICENSE +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/MANIFEST.in +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/SECURITY.md +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/docs/dapp_uml.txt +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/docs/ground_truth_labeling.md +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/docs/gui_uml.txt +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/examples/__init__.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/examples/simple_dapp.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/CMakeLists.txt +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/cmake/libiqsaver.pc.in +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/cmake/libiqsaverConfig.cmake.in +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/include/iqsaver/annotation.hpp +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/include/iqsaver/iq_saver_writer.hpp +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/include/iqsaver/iqsaver.hpp +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/include/iqsaver/types.hpp +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/include/iqsaver/version.hpp.in +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/src/iq_saver_writer.cpp +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/swig/iqsaver.i +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/swig/iqsaver_swig.cpp +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/swig/iqsaver_swig.hpp +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/dapp/__init__.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/dapp/dapp.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/__init__.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/e3_interface.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/e3_logging.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/e3_transport.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/latrec.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/sm_helpers.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/iq_saver/__init__.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/iq_saver/iq_saver.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/simple/__init__.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/simple/defs/__init__.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/simple/defs/e3sm_simple.proto +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/simple/defs/e3sm_simple_pb2.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/simple/simple_dapp.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/__init__.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/adaptive_noise_floor.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/__init__.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_oai_l1_kpm.asn +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_oai_l1_kpm.proto +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_oai_l1_kpm_pb2.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum.asn +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum.json +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum.proto +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum_pb2.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/e3_l2_sensing_reader.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/e3_ran_buffers_reader.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/embargo_manager.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/ru_profiles.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/visualization/__init__.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/visualization/energy.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/visualization/grafana.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/visualization/influx.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/visualization/iq.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/src/visualization/subcarrier/insi_white.png +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/start_dapp.sh +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_e3_concurrency.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_indication_dispatch.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_iq_saver.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_latrec.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_latrec_stamps.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_outbound_send_result.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_protobuf_sm.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_ru_profiles.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_sensing_policy.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_spectrum_sm_timestamps_mask.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tests/test_spectrum_telemetry.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tools/README.md +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tools/asntojson.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tools/generate_sm.sh +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tools/read_recording.py +0 -0
- {dapps-0.2.2 → dapps-0.3.0}/tools/regen_proto.sh +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Run tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
if: github.repository != 'wineslab/dApp-library'
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v6
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
|
|
20
|
+
- name: Install build prerequisites
|
|
21
|
+
run: |
|
|
22
|
+
sudo apt-get update
|
|
23
|
+
sudo apt-get install -y cmake swig nlohmann-json3-dev libzmq3-dev libprotobuf-dev protobuf-compiler
|
|
24
|
+
|
|
25
|
+
# libe3py (a dependency) is built against an installed libe3 of the same version.
|
|
26
|
+
- name: Install libe3
|
|
27
|
+
env:
|
|
28
|
+
GH_TOKEN: ${{ github.token }}
|
|
29
|
+
run: |
|
|
30
|
+
ver=$(python3 -c "import re; print(re.search(r'libe3py>=([0-9.]+)', open('pyproject.toml').read()).group(1))")
|
|
31
|
+
gh release download "$ver" --repo wineslab/libe3 --pattern "libe3_${ver}_amd64.deb" --dir "$RUNNER_TEMP"
|
|
32
|
+
sudo apt-get install -y "$RUNNER_TEMP/libe3_${ver}_amd64.deb"
|
|
33
|
+
|
|
34
|
+
- name: Install package (editable, with test deps)
|
|
35
|
+
run: pip install -e ".[all]" pytest
|
|
36
|
+
|
|
37
|
+
- name: Run tests
|
|
38
|
+
run: pytest tests/ -v
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dapps
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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>
|
|
@@ -16,6 +16,7 @@ Project-URL: Package, https://pypi.org/project/dapps/
|
|
|
16
16
|
Project-URL: Source, https://github.com/wineslab/dApp-library
|
|
17
17
|
Project-URL: Bug Tracker, https://github.com/wineslab/dApp-library/issues
|
|
18
18
|
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: libe3py<0.3,>=0.2.3
|
|
19
20
|
Requires-Dist: asn1tools
|
|
20
21
|
Requires-Dist: typing_extensions
|
|
21
22
|
Requires-Dist: numpy
|
|
@@ -51,30 +52,34 @@ A complete tutorial on how to deploy a dApp can be found on the [OpenRAN Gym web
|
|
|
51
52
|
|
|
52
53
|
## Installation
|
|
53
54
|
|
|
54
|
-
### Prerequisite: libe3
|
|
55
|
+
### Prerequisite: libe3
|
|
55
56
|
|
|
56
57
|
All **E3AP** operations (transport, setup handshake, subscribe, indication/control
|
|
57
58
|
framing, and the ASN.1/JSON/Protobuf wire encoding) are handled by
|
|
58
59
|
[**libe3**](https://github.com/wineslab/libe3); this library keeps only the
|
|
59
60
|
**E3SM** (service-model) encode/decode in Python — the same split OAI uses on the
|
|
60
|
-
RAN side.
|
|
61
|
-
same
|
|
61
|
+
RAN side. Install libe3 once, like any other library (it asks for `sudo`), at the
|
|
62
|
+
same version as the `libe3py` this package pulls in:
|
|
62
63
|
|
|
63
64
|
```bash
|
|
64
|
-
cd
|
|
65
|
-
./build_libe3
|
|
66
|
-
|
|
67
|
-
python3 -c "import libe3py; print('libe3py OK')"
|
|
65
|
+
git clone --branch 0.2.3 https://github.com/wineslab/libe3 && cd libe3
|
|
66
|
+
./build_libe3 -I # build dependencies, once
|
|
67
|
+
./build_libe3 --all-encodings --install # -> /usr/local
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
Its Python binding, `libe3py`, is a dependency of this package: pip builds it
|
|
71
|
+
against the installed libe3 when you install the dApp library, in any virtual
|
|
72
|
+
environment and without `sudo`.
|
|
73
|
+
|
|
70
74
|
The dApp's `--link` / `--transport` / `--encoding` must match the gNB's
|
|
71
75
|
`E3Configuration`. See [libe3's `swig/README.md`](https://github.com/wineslab/libe3/blob/main/swig/README.md)
|
|
72
76
|
for the binding architecture.
|
|
73
77
|
|
|
74
78
|
#### Latency recording (optional)
|
|
75
79
|
|
|
76
|
-
Per-stage latency records (latrec) come from libe3 itself:
|
|
77
|
-
`--cmake-opt "-DLIBE3_ENABLE_LATREC=ON"
|
|
80
|
+
Per-stage latency records (latrec) come from libe3 itself: install libe3 with
|
|
81
|
+
`--cmake-opt "-DLIBE3_ENABLE_LATREC=ON"`, then rebuild the binding
|
|
82
|
+
(`pip install --force-reinstall --no-cache-dir libe3py`), and the dApp stamps its E3SM decode,
|
|
78
83
|
processing, output creation, E3SM encode and xApp-policy apply stages into libe3's
|
|
79
84
|
rings, joinable with libe3's own by `tools/latrec2csv.py`. There is no runtime
|
|
80
85
|
switch; against a binding built without it every stamp is a no-op. See libe3's
|
|
@@ -160,6 +165,7 @@ This dApp implements a spectrum sharing use case discussed in [our paper](https:
|
|
|
160
165
|
- `--energy-gui` - Enable the energy spectrum visualization GUI
|
|
161
166
|
- `--iq-plotter-gui` - Enable the I/Q constellation plotter GUI
|
|
162
167
|
- `--demo-gui` - Enable the demonstration GUI showing real-time spectrum occupancy
|
|
168
|
+
- `--ctrl-port N` - Dashboard control port on loopback (default 5570 with `--demo-gui`, off otherwise; `0` disables). See `src/spectrum/README.md`, "Dashboard control"
|
|
163
169
|
- `--show-controls` - Show tunable controls (sampling threshold) in the dashboard GUI. Only meaningful with `--demo-gui`
|
|
164
170
|
- `--ground-truth LABEL` - Initial ground truth label written into IQ annotations (only used with `--save-iqs`). Updatable at runtime via the dashboard GUI when `--demo-gui` is also set. When no PRBs are detected the label is forced to `no_rfi` regardless of what is set
|
|
165
171
|
|
|
@@ -4,30 +4,34 @@ A complete tutorial on how to deploy a dApp can be found on the [OpenRAN Gym web
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
### Prerequisite: libe3
|
|
7
|
+
### Prerequisite: libe3
|
|
8
8
|
|
|
9
9
|
All **E3AP** operations (transport, setup handshake, subscribe, indication/control
|
|
10
10
|
framing, and the ASN.1/JSON/Protobuf wire encoding) are handled by
|
|
11
11
|
[**libe3**](https://github.com/wineslab/libe3); this library keeps only the
|
|
12
12
|
**E3SM** (service-model) encode/decode in Python — the same split OAI uses on the
|
|
13
|
-
RAN side.
|
|
14
|
-
same
|
|
13
|
+
RAN side. Install libe3 once, like any other library (it asks for `sudo`), at the
|
|
14
|
+
same version as the `libe3py` this package pulls in:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
cd
|
|
18
|
-
./build_libe3
|
|
19
|
-
|
|
20
|
-
python3 -c "import libe3py; print('libe3py OK')"
|
|
17
|
+
git clone --branch 0.2.3 https://github.com/wineslab/libe3 && cd libe3
|
|
18
|
+
./build_libe3 -I # build dependencies, once
|
|
19
|
+
./build_libe3 --all-encodings --install # -> /usr/local
|
|
21
20
|
```
|
|
22
21
|
|
|
22
|
+
Its Python binding, `libe3py`, is a dependency of this package: pip builds it
|
|
23
|
+
against the installed libe3 when you install the dApp library, in any virtual
|
|
24
|
+
environment and without `sudo`.
|
|
25
|
+
|
|
23
26
|
The dApp's `--link` / `--transport` / `--encoding` must match the gNB's
|
|
24
27
|
`E3Configuration`. See [libe3's `swig/README.md`](https://github.com/wineslab/libe3/blob/main/swig/README.md)
|
|
25
28
|
for the binding architecture.
|
|
26
29
|
|
|
27
30
|
#### Latency recording (optional)
|
|
28
31
|
|
|
29
|
-
Per-stage latency records (latrec) come from libe3 itself:
|
|
30
|
-
`--cmake-opt "-DLIBE3_ENABLE_LATREC=ON"
|
|
32
|
+
Per-stage latency records (latrec) come from libe3 itself: install libe3 with
|
|
33
|
+
`--cmake-opt "-DLIBE3_ENABLE_LATREC=ON"`, then rebuild the binding
|
|
34
|
+
(`pip install --force-reinstall --no-cache-dir libe3py`), and the dApp stamps its E3SM decode,
|
|
31
35
|
processing, output creation, E3SM encode and xApp-policy apply stages into libe3's
|
|
32
36
|
rings, joinable with libe3's own by `tools/latrec2csv.py`. There is no runtime
|
|
33
37
|
switch; against a binding built without it every stamp is a no-op. See libe3's
|
|
@@ -113,6 +117,7 @@ This dApp implements a spectrum sharing use case discussed in [our paper](https:
|
|
|
113
117
|
- `--energy-gui` - Enable the energy spectrum visualization GUI
|
|
114
118
|
- `--iq-plotter-gui` - Enable the I/Q constellation plotter GUI
|
|
115
119
|
- `--demo-gui` - Enable the demonstration GUI showing real-time spectrum occupancy
|
|
120
|
+
- `--ctrl-port N` - Dashboard control port on loopback (default 5570 with `--demo-gui`, off otherwise; `0` disables). See `src/spectrum/README.md`, "Dashboard control"
|
|
116
121
|
- `--show-controls` - Show tunable controls (sampling threshold) in the dashboard GUI. Only meaningful with `--demo-gui`
|
|
117
122
|
- `--ground-truth LABEL` - Initial ground truth label written into IQ annotations (only used with `--save-iqs`). Updatable at runtime via the dashboard GUI when `--demo-gui` is also set. When no PRBs are detected the label is forced to `no_rfi` regardless of what is set
|
|
118
123
|
|
dapps-0.3.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.0
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Starting point of the RAN control dApps hackathon: write your policy here.
|
|
2
|
+
|
|
3
|
+
Run it with::
|
|
4
|
+
|
|
5
|
+
python3 -m examples.spectrum_dapp --link zmq --transport tcp --encoding-method asn1 \\
|
|
6
|
+
--control -ru rfsim --num-prbs 106 --center-freq 3619200000 --e \\
|
|
7
|
+
--policy examples.hackathon_policy:MyPolicy
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from spectrum.policy import BaselinePolicy, Observation, Policy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MyPolicy(Policy):
|
|
14
|
+
"""Return the set of PRBs to block; the PRBs you leave out are released.
|
|
15
|
+
|
|
16
|
+
``obs.prb_power_db`` has one level per PRB (dB, relative to the gNB's FFT counts),
|
|
17
|
+
``obs.power_db`` one per subcarrier, ``obs.valid`` marks what was measured, and
|
|
18
|
+
``self.history`` holds the previous observations. State lives on ``self``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def update(self, obs: Observation) -> set[int]:
|
|
22
|
+
# TODO: replace this with your own policy.
|
|
23
|
+
return set()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ReferencePolicy(BaselinePolicy):
|
|
27
|
+
"""A working but simple policy to compare against: ``--policy examples.hackathon_policy:ReferencePolicy``."""
|
|
@@ -21,9 +21,13 @@ from spectrum.ru_profiles import (
|
|
|
21
21
|
uncalibrated_keys,
|
|
22
22
|
)
|
|
23
23
|
from spectrum.threshold_detector import StaticThresholdDetector, AdaptiveThresholdDetector
|
|
24
|
+
from spectrum.policy import PolicyDetector, load_policy
|
|
24
25
|
|
|
25
26
|
LOG_DIR = '/tmp/'
|
|
26
27
|
|
|
28
|
+
# Not 5560: that is where a co-resident C++ dApp serves its dashboard control.
|
|
29
|
+
DEFAULT_CTRL_PORT = 5570
|
|
30
|
+
|
|
27
31
|
def stop_program(time_to_wait, dapp: SpectrumSharingDApp):
|
|
28
32
|
time.sleep(time_to_wait)
|
|
29
33
|
print(f"[INFO] Timer elapsed after {time_to_wait} seconds")
|
|
@@ -56,6 +60,8 @@ def main(args):
|
|
|
56
60
|
uncalibrated = [
|
|
57
61
|
key for key in uncalibrated_keys(args.ru)
|
|
58
62
|
if getattr(args, key, None) is None
|
|
63
|
+
# a --policy decides the blacklist itself; the threshold is not used
|
|
64
|
+
and not (args.policy and key == 'noise_floor_threshold')
|
|
59
65
|
]
|
|
60
66
|
if uncalibrated:
|
|
61
67
|
print(f"[WARNING] Profile '{args.ru}' has no calibrated value for "
|
|
@@ -65,7 +71,8 @@ def main(args):
|
|
|
65
71
|
# carefully calibrated.
|
|
66
72
|
noise_floor_threshold = settings['noise_floor_threshold']
|
|
67
73
|
|
|
68
|
-
|
|
74
|
+
if not args.policy:
|
|
75
|
+
print(f'Threshold is {noise_floor_threshold}')
|
|
69
76
|
|
|
70
77
|
# ------------------------------------------------------------------
|
|
71
78
|
# Build the detection strategy explicitly so the example is the
|
|
@@ -73,7 +80,14 @@ def main(args):
|
|
|
73
80
|
# ------------------------------------------------------------------
|
|
74
81
|
fft_size = compute_fft_size(settings['num_prbs'], args.e)
|
|
75
82
|
|
|
76
|
-
if args.
|
|
83
|
+
if args.policy:
|
|
84
|
+
detector = PolicyDetector(
|
|
85
|
+
load_policy(args.policy),
|
|
86
|
+
fft_size=fft_size,
|
|
87
|
+
num_prbs=settings['num_prbs'],
|
|
88
|
+
)
|
|
89
|
+
print(f"[INFO] Detector: PolicyDetector | policy: {args.policy}")
|
|
90
|
+
elif args.use_adaptive_noise_floor:
|
|
77
91
|
detector = AdaptiveThresholdDetector(
|
|
78
92
|
snr_threshold_db=noise_floor_threshold,
|
|
79
93
|
fft_size=fft_size,
|
|
@@ -98,6 +112,12 @@ def main(args):
|
|
|
98
112
|
f" | window: {args.average_over_frames} frames"
|
|
99
113
|
)
|
|
100
114
|
|
|
115
|
+
# Dashboard control: on with --demo-gui at the default port, off otherwise.
|
|
116
|
+
# An explicit --ctrl-port that cannot be bound is fatal; the default is not.
|
|
117
|
+
ctrl_port = args.ctrl_port
|
|
118
|
+
if ctrl_port is None:
|
|
119
|
+
ctrl_port = DEFAULT_CTRL_PORT if args.demo_gui else 0
|
|
120
|
+
|
|
101
121
|
dapp = SpectrumSharingDApp(
|
|
102
122
|
detector=detector,
|
|
103
123
|
save_iqs=args.save_iqs,
|
|
@@ -110,6 +130,8 @@ def main(args):
|
|
|
110
130
|
viz_web_port=args.viz_web_port,
|
|
111
131
|
viz_zmq_port=args.viz_zmq_port,
|
|
112
132
|
external_viz=args.external_viz,
|
|
133
|
+
ctrl_port=ctrl_port,
|
|
134
|
+
ctrl_port_required=args.ctrl_port is not None and ctrl_port > 0,
|
|
113
135
|
center_freq=settings['center_freq'],
|
|
114
136
|
num_prbs=settings['num_prbs'],
|
|
115
137
|
e_sampling=args.e,
|
|
@@ -234,6 +256,10 @@ if __name__ == "__main__":
|
|
|
234
256
|
help="ZMQ port the dApp publishes on / the visualizer reads (default 5559)")
|
|
235
257
|
parser.add_argument('--external-viz', action='store_true', default=False,
|
|
236
258
|
help="Don't spawn the visualizer; publish to an already-running one")
|
|
259
|
+
parser.add_argument('--ctrl-port', type=int, default=None,
|
|
260
|
+
help=f"Dashboard control port (loopback). Default {DEFAULT_CTRL_PORT} "
|
|
261
|
+
"with --demo-gui, off otherwise; 0 disables. With --external-viz, "
|
|
262
|
+
"point the visualizer's --ctrl-port at it.")
|
|
237
263
|
parser.add_argument('--num-prbs', type=int, default=None,
|
|
238
264
|
help="Number of PRBs (default: from --ru)")
|
|
239
265
|
parser.add_argument('--num-subcarrier-spacing', type=int, default=None,
|
|
@@ -300,6 +326,11 @@ if __name__ == "__main__":
|
|
|
300
326
|
"require fully-clean slots (equivalent to "
|
|
301
327
|
"--strict-sensing).")
|
|
302
328
|
|
|
329
|
+
parser.add_argument('--policy', type=str, default=None, metavar='MODULE:CLASS',
|
|
330
|
+
help="Decide the PRB blacklist with a spectrum.policy.Policy subclass "
|
|
331
|
+
"(e.g. examples.hackathon_policy:MyPolicy) instead of a threshold "
|
|
332
|
+
"detector. Overrides --use-adaptive-noise-floor.")
|
|
333
|
+
|
|
303
334
|
# --- Optional sensing-policy toggle ----------------------------- #
|
|
304
335
|
# Drives the gNB's masked UL TDA selector via spectrum_sm ctrl_id=2.
|
|
305
336
|
# Disabled by default (toggle-period=0); set --toggle-period to a
|
|
@@ -19,15 +19,14 @@ classifiers = [
|
|
|
19
19
|
]
|
|
20
20
|
license = "Apache-2.0"
|
|
21
21
|
license-files = ["LICENSE"]
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# cd ~/libe3 && ./build_libe3 --install --enable-swig \
|
|
28
|
-
# --cmake-opt "-DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=ON"
|
|
22
|
+
# E3AP transport is libe3's Python binding, `libe3py` (on PyPI). pip builds it
|
|
23
|
+
# against the libe3 installed on the machine, which must be the same version:
|
|
24
|
+
# install libe3 once first (`./build_libe3 --all-encodings --install`, see
|
|
25
|
+
# https://github.com/wineslab/libe3). The 0.2 series is required: 0.2.0 made the
|
|
26
|
+
# correlation id a mandatory argument of DAppSession.send_report().
|
|
29
27
|
# (asn1tools is still required for the in-Python service-model codecs.)
|
|
30
28
|
dependencies = [
|
|
29
|
+
"libe3py>=0.2.3,<0.3",
|
|
31
30
|
"asn1tools",
|
|
32
31
|
"typing_extensions",
|
|
33
32
|
"numpy",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""ZMQ REP server for the dashboard's control channel.
|
|
2
|
+
|
|
3
|
+
The subcarrier dashboard proxies every control click as one JSON request
|
|
4
|
+
(``{"cmd": ...}``) over a REQ socket and waits at most 2 s for one JSON reply.
|
|
5
|
+
This module only moves JSON: the semantics live in the handlers a dApp
|
|
6
|
+
registers, so it knows nothing about spectrum sensing.
|
|
7
|
+
|
|
8
|
+
Binds loopback only. The socket is bound in the constructor, so a port that is
|
|
9
|
+
already in use is reported to the caller before anything else is started.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import threading
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
from e3interface.e3_logging import dapp_logger
|
|
19
|
+
|
|
20
|
+
Handler = Callable[[dict], dict]
|
|
21
|
+
|
|
22
|
+
_POLL_MS = 200
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ControlBindError(RuntimeError):
|
|
26
|
+
"""The control port could not be bound (typically: already in use)."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ControlServer:
|
|
30
|
+
"""REP server; one daemon thread, one reply per request, always."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, port: int, host: str = "127.0.0.1",
|
|
33
|
+
tick: Callable[[], None] | None = None) -> None:
|
|
34
|
+
import zmq # lazy: pyzmq is an optional extra
|
|
35
|
+
|
|
36
|
+
self._zmq = zmq
|
|
37
|
+
self._handlers: dict[str, Handler] = {}
|
|
38
|
+
self._tick = tick
|
|
39
|
+
self._stop = threading.Event()
|
|
40
|
+
self._thread: threading.Thread | None = None
|
|
41
|
+
self._ctx = zmq.Context()
|
|
42
|
+
self._sock = self._ctx.socket(zmq.REP)
|
|
43
|
+
self._sock.setsockopt(zmq.LINGER, 0)
|
|
44
|
+
try:
|
|
45
|
+
if int(port) == 0:
|
|
46
|
+
# let the OS pick: there is no window between choosing a free port and binding it
|
|
47
|
+
port = self._sock.bind_to_random_port(f"tcp://{host}")
|
|
48
|
+
else:
|
|
49
|
+
self._sock.bind(f"tcp://{host}:{int(port)}")
|
|
50
|
+
except zmq.ZMQError as exc:
|
|
51
|
+
self._sock.close(0)
|
|
52
|
+
self._ctx.term()
|
|
53
|
+
raise ControlBindError(
|
|
54
|
+
f"cannot bind control port tcp://{host}:{port}: {exc}") from exc
|
|
55
|
+
self.host = host
|
|
56
|
+
self.port = int(port)
|
|
57
|
+
|
|
58
|
+
def register(self, cmd: str, handler: Handler) -> None:
|
|
59
|
+
self._handlers[cmd] = handler
|
|
60
|
+
|
|
61
|
+
def start(self) -> None:
|
|
62
|
+
if self._thread is not None:
|
|
63
|
+
return
|
|
64
|
+
self._thread = threading.Thread(
|
|
65
|
+
target=self._loop, name="dapp_ctrl", daemon=True)
|
|
66
|
+
self._thread.start()
|
|
67
|
+
|
|
68
|
+
def stop(self) -> None:
|
|
69
|
+
self._stop.set()
|
|
70
|
+
if self._thread is not None:
|
|
71
|
+
self._thread.join(timeout=2.0)
|
|
72
|
+
self._thread = None
|
|
73
|
+
try:
|
|
74
|
+
self._sock.close(0)
|
|
75
|
+
self._ctx.term()
|
|
76
|
+
except Exception:
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
def dispatch(self, raw: str) -> dict:
|
|
80
|
+
"""Turn one request string into a reply dict (never raises)."""
|
|
81
|
+
try:
|
|
82
|
+
req = json.loads(raw)
|
|
83
|
+
except ValueError as exc:
|
|
84
|
+
return {"ok": False, "error": f"parse: {exc}"}
|
|
85
|
+
if not isinstance(req, dict):
|
|
86
|
+
return {"ok": False, "error": "parse: request must be a JSON object"}
|
|
87
|
+
cmd = req.get("cmd")
|
|
88
|
+
handler = self._handlers.get(cmd) if isinstance(cmd, str) else None
|
|
89
|
+
if handler is None:
|
|
90
|
+
return {"ok": False, "error": f"unknown cmd: {cmd}"}
|
|
91
|
+
try:
|
|
92
|
+
reply = handler(req)
|
|
93
|
+
except ValueError as exc:
|
|
94
|
+
return {"ok": False, "error": str(exc)}
|
|
95
|
+
except Exception as exc:
|
|
96
|
+
dapp_logger.exception("control handler %r failed", cmd)
|
|
97
|
+
return {"ok": False, "error": f"internal: {exc}"}
|
|
98
|
+
return reply
|
|
99
|
+
|
|
100
|
+
def _loop(self) -> None:
|
|
101
|
+
zmq = self._zmq
|
|
102
|
+
poller = zmq.Poller()
|
|
103
|
+
poller.register(self._sock, zmq.POLLIN)
|
|
104
|
+
while not self._stop.is_set():
|
|
105
|
+
if self._tick is not None:
|
|
106
|
+
try:
|
|
107
|
+
self._tick()
|
|
108
|
+
except Exception:
|
|
109
|
+
dapp_logger.exception("control tick failed")
|
|
110
|
+
if not dict(poller.poll(_POLL_MS)):
|
|
111
|
+
continue
|
|
112
|
+
try:
|
|
113
|
+
raw = self._sock.recv_string()
|
|
114
|
+
except zmq.ZMQError:
|
|
115
|
+
continue
|
|
116
|
+
reply = self.dispatch(raw)
|
|
117
|
+
try:
|
|
118
|
+
self._sock.send_string(json.dumps(reply))
|
|
119
|
+
except (zmq.ZMQError, TypeError, ValueError):
|
|
120
|
+
# An unserialisable reply must still be answered, or the REP
|
|
121
|
+
# socket stays in the "must send" state and wedges.
|
|
122
|
+
try:
|
|
123
|
+
self._sock.send_string(json.dumps(
|
|
124
|
+
{"ok": False, "error": "internal: bad reply"}))
|
|
125
|
+
except zmq.ZMQError:
|
|
126
|
+
pass
|
|
@@ -8,12 +8,10 @@ drives. Service-model (E3SM) payloads stay opaque here as ``bytes`` and are
|
|
|
8
8
|
encoded/decoded in the dApp subclasses (spectrum/simple), mirroring the OAI
|
|
9
9
|
split where libe3 handles E3AP and the SM handles encode/decode.
|
|
10
10
|
|
|
11
|
-
``libe3py``
|
|
11
|
+
``libe3py`` comes from PyPI (a dependency of this package) and is built against
|
|
12
|
+
the libe3 installed on the machine; install libe3 once first::
|
|
12
13
|
|
|
13
|
-
./build_libe3 --
|
|
14
|
-
--cmake-opt "-DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=ON"
|
|
15
|
-
|
|
16
|
-
Import it into the same interpreter/venv that runs the dApp.
|
|
14
|
+
./build_libe3 --all-encodings --install
|
|
17
15
|
"""
|
|
18
16
|
|
|
19
17
|
from __future__ import annotations
|
|
@@ -64,13 +62,15 @@ def _libe3py_is_current() -> bool:
|
|
|
64
62
|
LIBE3PY_USABLE = LIBE3PY_AVAILABLE and _libe3py_is_current()
|
|
65
63
|
|
|
66
64
|
_BUILD_CMD = (
|
|
67
|
-
"
|
|
68
|
-
|
|
65
|
+
" # once, in a libe3 checkout of the same version (https://github.com/wineslab/libe3):\n"
|
|
66
|
+
" ./build_libe3 -I && ./build_libe3 --all-encodings --install\n"
|
|
67
|
+
" # then, in this environment:\n"
|
|
68
|
+
" pip install --force-reinstall --no-cache-dir libe3py"
|
|
69
69
|
)
|
|
70
70
|
|
|
71
71
|
_IMPORT_HINT = (
|
|
72
|
-
"libe3py is not importable.
|
|
73
|
-
"
|
|
72
|
+
"libe3py is not importable. Install libe3, then its Python binding into "
|
|
73
|
+
"this environment:\n" + _BUILD_CMD
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
_VERSION_HINT = (
|
|
@@ -78,7 +78,7 @@ _VERSION_HINT = (
|
|
|
78
78
|
"E3Event.sequence_id). That release made the correlation id a mandatory "
|
|
79
79
|
"argument of DAppSession.send_report(), so this binding cannot complete "
|
|
80
80
|
"the E3 setup handshake against a matching gNB -- the run fails with "
|
|
81
|
-
"'RAN refused Setup'.
|
|
81
|
+
"'RAN refused Setup'. Install libe3 and libe3py of the 0.2 series:\n" + _BUILD_CMD
|
|
82
82
|
)
|
|
83
83
|
|
|
84
84
|
# ErrorCode.SUCCESS is 0 in libe3.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Spectrum dApp pipeline — from gNB radio to browser waterfall
|
|
2
|
+
|
|
3
|
+
The spectrum dApp senses the uplink, optionally blacklists PRBs on the gNB, and
|
|
4
|
+
streams a live subcarrier-power waterfall to a browser. It talks to the gNB over
|
|
5
|
+
E3 (libe3) and reads bulk IQ / sensing ranges from POSIX shared memory.
|
|
6
|
+
|
|
7
|
+
## Data flow
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
gNB PHY ── L1-KPM SM (RF=2) ──► E3 indication: shm pointer + validSymbolMask
|
|
11
|
+
│ └► dApp reads IQ from /e3_ran_buffers (FP16)
|
|
12
|
+
└─ Spectrum SM (RF=1) ──► E3 indication: shm ref (shmWriteIdx, nRanges)
|
|
13
|
+
└► dApp reads ranges from /e3_l2_sensing ring
|
|
14
|
+
dApp: magnitude ─► detector (masked adaptive noise floor) ─► PRB blacklist control (RF=1)
|
|
15
|
+
└► ZMQ PUB (per-slot u8 power) ─► subcarrier_visualizer.py ─► browser (WebGL2 waterfall)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Service models (both ASN.1/APER and JSON)
|
|
19
|
+
|
|
20
|
+
- **RF=2 L1-KPM** — per-slot IQ telemetry. The indication carries an shm
|
|
21
|
+
reference (`/e3_ran_buffers`, `fhBufferIndex`, `fhWriteIndex`), `sfn`/`slot`,
|
|
22
|
+
and `validSymbolMask` (14-bit UL-symbol bitmap). IQ layout is
|
|
23
|
+
`[ant=4][sym=14][prb=273][sc=12]` FP16. See `e3_ran_buffers_reader.py`.
|
|
24
|
+
- **RF=1 Spectrum** — sensing-range telemetry + PRB-block / sensing-policy
|
|
25
|
+
control. The `Spectrum-SensingIndication` references the `/e3_l2_sensing` ring
|
|
26
|
+
(`shmWriteIdx`, `nRanges`); each record is a `(start_symbol, num_symbols,
|
|
27
|
+
rb_start, rb_size)` rectangle. See `e3_l2_sensing_reader.py`.
|
|
28
|
+
|
|
29
|
+
The ASN.1 grammars (`defs/e3sm_oai_l1_kpm.asn`, `defs/e3sm_spectrum.asn`) are
|
|
30
|
+
copied **verbatim** from the OAI gNB so the wire stays byte-identical.
|
|
31
|
+
|
|
32
|
+
## Sensing window
|
|
33
|
+
|
|
34
|
+
The dApp caches the RF=1 sensing ranges by `(sfn, slot)` and applies them to the
|
|
35
|
+
matching RF=2 IQ slot (`sensing_only`, default on) to build a 2D (symbol, PRB)
|
|
36
|
+
keep-mask. Out-of-window columns are excluded from the adaptive noise-floor
|
|
37
|
+
history (not fed as zeros), so ambient UE PRBs don't skew the floor.
|
|
38
|
+
|
|
39
|
+
## Visualizer
|
|
40
|
+
|
|
41
|
+
`--demo-gui` binds a ZMQ PUB and (by default) spawns the standalone
|
|
42
|
+
`visualization/subcarrier/subcarrier_visualizer.py` (Flask + WebSocket + WebGL2,
|
|
43
|
+
R8 textures via `texStorage2D`) on `--viz-web-port` (5001). The dApp publishes one
|
|
44
|
+
multipart frame per slot (`subcarrier_power|sfn|slot|n_ant|n_sym|n_sc|u8|db_min|
|
|
45
|
+
db_max|det=…|det_n=…|det_thr=…` + u8 power, plus a detection-mask frame when the
|
|
46
|
+
detector has flagged PRBs); see `visualization/subcarrier_pub.py`. The render
|
|
47
|
+
grid width follows the active carrier (`--num-prbs`×12 subcarriers, e.g. 1272 for
|
|
48
|
+
106 PRB), so the waterfall renders on any bandwidth. Use `--external-viz` to feed
|
|
49
|
+
an already-running visualizer. The dApp tells it the carrier centre and
|
|
50
|
+
subcarrier spacing, so the x axis is labelled in MHz (ticks above the waterfall,
|
|
51
|
+
and a PRB / subcarrier / MHz readout under the cursor); without `--center-freq`
|
|
52
|
+
information the axis shows indices only.
|
|
53
|
+
|
|
54
|
+
## Dashboard control (protocol v1)
|
|
55
|
+
|
|
56
|
+
With `--demo-gui` the dApp also serves the dashboard's control panels on a ZMQ
|
|
57
|
+
REP socket, `tcp://127.0.0.1:5570` by default. `--ctrl-port N` picks another
|
|
58
|
+
port, `--ctrl-port 0` turns it off. The default port is not 5560 so it never
|
|
59
|
+
collides with another dApp's control port; if it is already taken the dApp warns
|
|
60
|
+
and runs without control (panels greyed), whereas an explicit `--ctrl-port` that
|
|
61
|
+
cannot be bound stops the dApp. With `--external-viz`, start the visualizer with
|
|
62
|
+
the same `--ctrl-port` (and `--num-prbs`).
|
|
63
|
+
|
|
64
|
+
Requests are one JSON object per frame with a `"cmd"`; every reply has `"ok"`
|
|
65
|
+
and, on failure, `"error"`. The dashboard waits 2 s for the reply.
|
|
66
|
+
|
|
67
|
+
| cmd | request | effect |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `caps` | – | what this dApp supports: detector kind, per-field availability and the reason when a field is greyed out |
|
|
70
|
+
| `block` | `enabled` (bool); none = query | start/stop forwarding detections to the gNB; xApp-sourced blocks stay installed |
|
|
71
|
+
| `detect` | `config`: `enabled`, `granularity` (`sc`/`prb`), `threshold_db`, `hist_depth`, `embargo_secs`, `mask_with_sensing`; `{}` = query | change the active detector; all-or-nothing |
|
|
72
|
+
| `viz` | `config`: `freq_gran`, `time_gran`, `agg`, `subsample_n`; `{}` = query | what the dApp publishes to the dashboard |
|
|
73
|
+
| `start` / `stop` / `status` | `duration_s` (0, 86400], `notes` | SigMF recording |
|
|
74
|
+
|
|
75
|
+
`mask_with_sensing` (the L1xL2 filter, `sensing_only`) masks the dashboard as well
|
|
76
|
+
as the detector input: each slot's keep mask (sensing ranges and valid symbols)
|
|
77
|
+
is built once and the published frame has every other cell at the floor, so the
|
|
78
|
+
dashboard shows what the detector sees, slot by slot, without the UE's own
|
|
79
|
+
transmissions. A slot with no sensed cell is not published.
|
|
80
|
+
|
|
81
|
+
`detect` applies to the threshold detectors. Under `--policy` the policy decides
|
|
82
|
+
the blocks, so only `mask_with_sensing` (the L1xL2 filter, i.e. `sensing_only`)
|
|
83
|
+
can be changed; the other fields are greyed out. A static detector has no
|
|
84
|
+
embargo, and its `hist_depth` is the averaging window. Granularity `prb`
|
|
85
|
+
averages the 12 subcarriers of a PRB before detection and rebuilds the detector
|
|
86
|
+
(history is lost); the red strip still shows the result per subcarrier. A change
|
|
87
|
+
that resets the adaptive history (`hist_depth`, `granularity`) while a constant
|
|
88
|
+
incumbent is already on makes that incumbent the new noise floor, so it is no
|
|
89
|
+
longer detected; the adaptive floor suits bursty interference.
|
|
90
|
+
|
|
91
|
+
Recording (`start`) is independent of `--save-iqs`: it writes every antenna,
|
|
92
|
+
every symbol and the gNB's whole 273-PRB slot as `cf32_le`, one record per slot
|
|
93
|
+
laid out `[ant][sym][prb][sc][I,Q]` (PRBs beyond the cell bandwidth are zero),
|
|
94
|
+
with one annotation per slot (`sfn`, `slot`, sensing ranges, valid-symbol mask).
|
|
95
|
+
A full 4-antenna slot is about 1.8 MB, so the writer runs on its own thread and
|
|
96
|
+
reports `dropped` slots instead of stalling detection; `start` is refused with
|
|
97
|
+
less than 2 GiB free in the log directory.
|
|
98
|
+
|
|
99
|
+
## Robustness
|
|
100
|
+
|
|
101
|
+
- Both shm readers detect a gNB restart (the gNB recreates the regions with a
|
|
102
|
+
new inode) and remap.
|
|
103
|
+
- The FH staleness bound is derived from the ring depth in the shm header
|
|
104
|
+
(`num_fh_rows`), not a hardcoded value.
|