dapps 0.2.1__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.1 → dapps-0.3.0}/PKG-INFO +31 -13
- {dapps-0.2.1 → dapps-0.3.0}/README.md +29 -12
- dapps-0.3.0/VERSION +1 -0
- {dapps-0.2.1 → dapps-0.3.0}/docs/ground_truth_labeling.md +3 -3
- dapps-0.3.0/examples/hackathon_policy.py +27 -0
- {dapps-0.2.1 → dapps-0.3.0}/examples/spectrum_dapp.py +93 -38
- {dapps-0.2.1 → dapps-0.3.0}/pyproject.toml +6 -5
- dapps-0.3.0/src/dapp/control_server.py +126 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/dapp/dapp.py +9 -1
- {dapps-0.2.1 → dapps-0.3.0}/src/e3interface/e3_interface.py +66 -9
- dapps-0.3.0/src/e3interface/latrec.py +78 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/e3interface/libe3_agent.py +74 -14
- {dapps-0.2.1 → dapps-0.3.0}/src/simple/simple_dapp.py +12 -2
- dapps-0.3.0/src/spectrum/README.md +104 -0
- dapps-0.3.0/src/spectrum/control_handlers.py +150 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum.asn +77 -3
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum.json +26 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum.proto +7 -1
- dapps-0.3.0/src/spectrum/defs/e3sm_spectrum_pb2.py +43 -0
- dapps-0.3.0/src/spectrum/policy.py +171 -0
- dapps-0.3.0/src/spectrum/ru_profiles.py +130 -0
- dapps-0.3.0/src/spectrum/slot_recorder.py +263 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/spectrum_dapp.py +328 -72
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/threshold_detector.py +54 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/visualization/subcarrier/start_subcarrier_visualizer.sh +10 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/visualization/subcarrier/subcarrier_visualizer.py +268 -33
- {dapps-0.2.1 → 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.1 → dapps-0.3.0}/tests/test_indication_dispatch.py +28 -3
- dapps-0.3.0/tests/test_latrec.py +66 -0
- dapps-0.3.0/tests/test_latrec_stamps.py +211 -0
- {dapps-0.2.1 → dapps-0.3.0}/tests/test_libe3_agent.py +2 -2
- dapps-0.3.0/tests/test_outbound_send_result.py +55 -0
- dapps-0.3.0/tests/test_policy.py +120 -0
- dapps-0.3.0/tests/test_ru_profiles.py +98 -0
- dapps-0.3.0/tests/test_spectrum_sm_timestamps_mask.py +80 -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.1/.github/workflows/test.yml +0 -29
- 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/README.md +0 -56
- 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.3.0}/.github/CODEOWNERS +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/documentation.yml +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/new_sm.yml +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/question.yml +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/.github/workflows/mirror.yml +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/.github/workflows/tag-and-release.yml +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/.gitignore +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/CMakeLists.txt +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/CONTRIBUTING.md +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/CONTRIBUTORS +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/LICENSE +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/MANIFEST.in +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/SECURITY.md +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/docs/dapp_uml.txt +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/docs/gui_uml.txt +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/examples/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/examples/simple_dapp.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/CMakeLists.txt +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/cmake/libiqsaver.pc.in +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/cmake/libiqsaverConfig.cmake.in +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/include/iqsaver/annotation.hpp +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/include/iqsaver/iq_saver_writer.hpp +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/include/iqsaver/iqsaver.hpp +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/include/iqsaver/types.hpp +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/include/iqsaver/version.hpp.in +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/src/iq_saver_writer.cpp +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/swig/iqsaver.i +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/swig/iqsaver_swig.cpp +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/libiqsaver/swig/iqsaver_swig.hpp +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/dapp/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/e3interface/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/e3interface/e3_logging.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/e3interface/e3_transport.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/e3interface/sm_helpers.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/iq_saver/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/iq_saver/iq_saver.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/simple/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/simple/defs/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/simple/defs/e3sm_simple.proto +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/simple/defs/e3sm_simple_pb2.py +0 -0
- {dapps-0.2.1/src/nvidia → dapps-0.3.0/src/spectrum}/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/adaptive_noise_floor.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/defs/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/defs/e3sm_oai_l1_kpm.asn +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/defs/e3sm_oai_l1_kpm.proto +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/defs/e3sm_oai_l1_kpm_pb2.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/e3_l2_sensing_reader.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/e3_ran_buffers_reader.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/spectrum/embargo_manager.py +0 -0
- {dapps-0.2.1/src/spectrum → dapps-0.3.0/src/visualization}/__init__.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/visualization/energy.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/visualization/grafana.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/visualization/influx.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/visualization/iq.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/src/visualization/subcarrier/insi_white.png +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/start_dapp.sh +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/tests/test_e3_concurrency.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/tests/test_iq_saver.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/tests/test_protobuf_sm.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/tests/test_sensing_policy.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/tests/test_spectrum_telemetry.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/tools/README.md +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/tools/asntojson.py +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/tools/generate_sm.sh +0 -0
- {dapps-0.2.1 → dapps-0.3.0}/tools/read_recording.py +0 -0
- {dapps-0.2.1 → 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,26 +52,39 @@ 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
|
|
|
78
|
+
#### Latency recording (optional)
|
|
79
|
+
|
|
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,
|
|
83
|
+
processing, output creation, E3SM encode and xApp-policy apply stages into libe3's
|
|
84
|
+
rings, joinable with libe3's own by `tools/latrec2csv.py`. There is no runtime
|
|
85
|
+
switch; against a binding built without it every stamp is a no-op. See libe3's
|
|
86
|
+
`docs/latrec.md`.
|
|
87
|
+
|
|
74
88
|
### Python package installation (recommended)
|
|
75
89
|
|
|
76
90
|
```
|
|
@@ -143,23 +157,27 @@ This dApp implements a spectrum sharing use case discussed in [our paper](https:
|
|
|
143
157
|
|
|
144
158
|
**Spectrum Sensing Configuration:**
|
|
145
159
|
|
|
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
|
-
- `--
|
|
160
|
+
- `--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
|
|
161
|
+
- `--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
162
|
|
|
149
163
|
**Visualization Options:**
|
|
150
164
|
|
|
151
165
|
- `--energy-gui` - Enable the energy spectrum visualization GUI
|
|
152
166
|
- `--iq-plotter-gui` - Enable the I/Q constellation plotter GUI
|
|
153
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"
|
|
154
169
|
- `--show-controls` - Show tunable controls (sampling threshold) in the dashboard GUI. Only meaningful with `--demo-gui`
|
|
155
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
|
|
156
171
|
|
|
157
172
|
**Radio Configuration:**
|
|
158
173
|
|
|
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)
|
|
174
|
+
- `--num-prbs` - Number of Physical Resource Blocks in the channel (default: from `--ru`, 106 for `usrp`)
|
|
175
|
+
- `--num-subcarrier-spacing` - Subcarrier spacing in kHz. Use 30 for FR1 (sub-6 GHz) (default: from `--ru`, 30)
|
|
161
176
|
- `--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
|
|
177
|
+
- `--center-freq` - Center frequency in Hz (default: from `--ru`, 3.6192e9 for `usrp`)
|
|
178
|
+
- `--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)
|
|
179
|
+
- `--fp16-beta` - FP16 IQ rescale factor, must match the gNB build/config (default: from `--ru`, 1/128 for USRPs and 1/2048 otherwise)
|
|
180
|
+
- `--max-samples-per-file` - SigMF segment length in samples (default: one second of the write rate, `num_prbs * 12 * SCS`)
|
|
163
181
|
|
|
164
182
|
**Execution Control:**
|
|
165
183
|
|
|
@@ -4,26 +4,39 @@ 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
|
|
|
30
|
+
#### Latency recording (optional)
|
|
31
|
+
|
|
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,
|
|
35
|
+
processing, output creation, E3SM encode and xApp-policy apply stages into libe3's
|
|
36
|
+
rings, joinable with libe3's own by `tools/latrec2csv.py`. There is no runtime
|
|
37
|
+
switch; against a binding built without it every stamp is a no-op. See libe3's
|
|
38
|
+
`docs/latrec.md`.
|
|
39
|
+
|
|
27
40
|
### Python package installation (recommended)
|
|
28
41
|
|
|
29
42
|
```
|
|
@@ -96,23 +109,27 @@ This dApp implements a spectrum sharing use case discussed in [our paper](https:
|
|
|
96
109
|
|
|
97
110
|
**Spectrum Sensing Configuration:**
|
|
98
111
|
|
|
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
|
-
- `--
|
|
112
|
+
- `--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
|
|
113
|
+
- `--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
114
|
|
|
102
115
|
**Visualization Options:**
|
|
103
116
|
|
|
104
117
|
- `--energy-gui` - Enable the energy spectrum visualization GUI
|
|
105
118
|
- `--iq-plotter-gui` - Enable the I/Q constellation plotter GUI
|
|
106
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"
|
|
107
121
|
- `--show-controls` - Show tunable controls (sampling threshold) in the dashboard GUI. Only meaningful with `--demo-gui`
|
|
108
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
|
|
109
123
|
|
|
110
124
|
**Radio Configuration:**
|
|
111
125
|
|
|
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)
|
|
126
|
+
- `--num-prbs` - Number of Physical Resource Blocks in the channel (default: from `--ru`, 106 for `usrp`)
|
|
127
|
+
- `--num-subcarrier-spacing` - Subcarrier spacing in kHz. Use 30 for FR1 (sub-6 GHz) (default: from `--ru`, 30)
|
|
114
128
|
- `--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
|
|
129
|
+
- `--center-freq` - Center frequency in Hz (default: from `--ru`, 3.6192e9 for `usrp`)
|
|
130
|
+
- `--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)
|
|
131
|
+
- `--fp16-beta` - FP16 IQ rescale factor, must match the gNB build/config (default: from `--ru`, 1/128 for USRPs and 1/2048 otherwise)
|
|
132
|
+
- `--max-samples-per-file` - SigMF segment length in samples (default: one second of the write rate, `num_prbs * 12 * SCS`)
|
|
116
133
|
|
|
117
134
|
**Execution Control:**
|
|
118
135
|
|
dapps-0.3.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.0
|
|
@@ -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.
|
|
@@ -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``."""
|
|
@@ -14,10 +14,20 @@ 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
|
|
24
|
+
from spectrum.policy import PolicyDetector, load_policy
|
|
18
25
|
|
|
19
26
|
LOG_DIR = '/tmp/'
|
|
20
27
|
|
|
28
|
+
# Not 5560: that is where a co-resident C++ dApp serves its dashboard control.
|
|
29
|
+
DEFAULT_CTRL_PORT = 5570
|
|
30
|
+
|
|
21
31
|
def stop_program(time_to_wait, dapp: SpectrumSharingDApp):
|
|
22
32
|
time.sleep(time_to_wait)
|
|
23
33
|
print(f"[INFO] Timer elapsed after {time_to_wait} seconds")
|
|
@@ -37,28 +47,47 @@ def main(args):
|
|
|
37
47
|
"--time-window/--moving-avg-window/--extraction-window flags."
|
|
38
48
|
)
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
noise_floor_threshold
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
settings = resolve_ru_settings(args.ru, {
|
|
51
|
+
'num_prbs': args.num_prbs,
|
|
52
|
+
'num_subcarrier_spacing': args.num_subcarrier_spacing,
|
|
53
|
+
'center_freq': args.center_freq,
|
|
54
|
+
'noise_floor_threshold': args.noise_floor_threshold,
|
|
55
|
+
'fp16_beta': args.fp16_beta,
|
|
56
|
+
'front_end_sample_rate': args.sample_rate,
|
|
57
|
+
'max_samples_per_file': args.max_samples_per_file,
|
|
58
|
+
})
|
|
59
|
+
print(f"Using radio-unit profile '{args.ru}': {settings}")
|
|
60
|
+
uncalibrated = [
|
|
61
|
+
key for key in uncalibrated_keys(args.ru)
|
|
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')
|
|
65
|
+
]
|
|
66
|
+
if uncalibrated:
|
|
67
|
+
print(f"[WARNING] Profile '{args.ru}' has no calibrated value for "
|
|
68
|
+
f"{', '.join(uncalibrated)}; using generic defaults")
|
|
69
|
+
|
|
70
|
+
# The noise floor depends on the RF conditions and the RU and should be
|
|
71
|
+
# carefully calibrated.
|
|
72
|
+
noise_floor_threshold = settings['noise_floor_threshold']
|
|
52
73
|
|
|
53
|
-
|
|
74
|
+
if not args.policy:
|
|
75
|
+
print(f'Threshold is {noise_floor_threshold}')
|
|
54
76
|
|
|
55
77
|
# ------------------------------------------------------------------
|
|
56
78
|
# Build the detection strategy explicitly so the example is the
|
|
57
79
|
# authoritative place for detector configuration.
|
|
58
80
|
# ------------------------------------------------------------------
|
|
59
|
-
fft_size = compute_fft_size(
|
|
81
|
+
fft_size = compute_fft_size(settings['num_prbs'], args.e)
|
|
60
82
|
|
|
61
|
-
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:
|
|
62
91
|
detector = AdaptiveThresholdDetector(
|
|
63
92
|
snr_threshold_db=noise_floor_threshold,
|
|
64
93
|
fft_size=fft_size,
|
|
@@ -83,6 +112,12 @@ def main(args):
|
|
|
83
112
|
f" | window: {args.average_over_frames} frames"
|
|
84
113
|
)
|
|
85
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
|
+
|
|
86
121
|
dapp = SpectrumSharingDApp(
|
|
87
122
|
detector=detector,
|
|
88
123
|
save_iqs=args.save_iqs,
|
|
@@ -95,13 +130,16 @@ def main(args):
|
|
|
95
130
|
viz_web_port=args.viz_web_port,
|
|
96
131
|
viz_zmq_port=args.viz_zmq_port,
|
|
97
132
|
external_viz=args.external_viz,
|
|
98
|
-
|
|
99
|
-
|
|
133
|
+
ctrl_port=ctrl_port,
|
|
134
|
+
ctrl_port_required=args.ctrl_port is not None and ctrl_port > 0,
|
|
135
|
+
center_freq=settings['center_freq'],
|
|
136
|
+
num_prbs=settings['num_prbs'],
|
|
100
137
|
e_sampling=args.e,
|
|
101
|
-
num_subcarrier_spacing=
|
|
138
|
+
num_subcarrier_spacing=settings['num_subcarrier_spacing'],
|
|
102
139
|
sampling_threshold=args.sampling_threshold,
|
|
103
|
-
max_samples_per_file=
|
|
104
|
-
fp16_beta=
|
|
140
|
+
max_samples_per_file=settings['max_samples_per_file'],
|
|
141
|
+
fp16_beta=settings['fp16_beta'],
|
|
142
|
+
front_end_sample_rate=settings['front_end_sample_rate'],
|
|
105
143
|
sensing_only=args.sensing_only,
|
|
106
144
|
strict_sensing=args.strict_sensing,
|
|
107
145
|
min_sensing_symbols=args.min_sensing_symbols,
|
|
@@ -191,16 +229,21 @@ if __name__ == "__main__":
|
|
|
191
229
|
help="Send PRB-block control messages to the gNB when "
|
|
192
230
|
"PRBs are detected above the noise threshold")
|
|
193
231
|
parser.add_argument('--noise-floor-threshold', type=int, default=None,
|
|
194
|
-
help="Detection threshold in dB (static) or dB above noise floor (adaptive)"
|
|
232
|
+
help="Detection threshold in dB (static) or dB above noise floor (adaptive); "
|
|
233
|
+
"default: from --ru")
|
|
195
234
|
parser.add_argument('--use-adaptive-noise-floor', action='store_true', default=False,
|
|
196
235
|
help="Use per-bin median noise floor estimation instead of a fixed threshold")
|
|
197
236
|
parser.add_argument('--embargo-timeout-secs', type=float, default=10.1,
|
|
198
237
|
help="Hold time in seconds for embargoed PRBs after last detection (adaptive mode)")
|
|
199
238
|
parser.add_argument('--average-over-frames', type=int, default=64,
|
|
200
239
|
help="Number of frames to average before each decision")
|
|
201
|
-
parser.add_argument('--
|
|
202
|
-
|
|
203
|
-
|
|
240
|
+
parser.add_argument('--ru', '-ru', type=str, default=DEFAULT_RU,
|
|
241
|
+
choices=RU_CHOICES,
|
|
242
|
+
help="Radio-unit profile seeding the defaults of --num-prbs, "
|
|
243
|
+
"--num-subcarrier-spacing, --center-freq, "
|
|
244
|
+
"--noise-floor-threshold, --fp16-beta, --sample-rate and "
|
|
245
|
+
"--max-samples-per-file. Explicit flags always win. "
|
|
246
|
+
f"Default: {DEFAULT_RU}.")
|
|
204
247
|
parser.add_argument('--energy-gui', action='store_true', default=False,
|
|
205
248
|
help="Enable energy spectrum visualization")
|
|
206
249
|
parser.add_argument('--iq-plotter-gui', action='store_true', default=False,
|
|
@@ -213,14 +256,22 @@ if __name__ == "__main__":
|
|
|
213
256
|
help="ZMQ port the dApp publishes on / the visualizer reads (default 5559)")
|
|
214
257
|
parser.add_argument('--external-viz', action='store_true', default=False,
|
|
215
258
|
help="Don't spawn the visualizer; publish to an already-running one")
|
|
216
|
-
parser.add_argument('--
|
|
217
|
-
help="
|
|
218
|
-
|
|
219
|
-
|
|
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.")
|
|
263
|
+
parser.add_argument('--num-prbs', type=int, default=None,
|
|
264
|
+
help="Number of PRBs (default: from --ru)")
|
|
265
|
+
parser.add_argument('--num-subcarrier-spacing', type=int, default=None,
|
|
266
|
+
help="Subcarrier spacing in kHz (FR1 = 30; default: from --ru)")
|
|
220
267
|
parser.add_argument('--e', action='store_true', default=False,
|
|
221
268
|
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")
|
|
269
|
+
parser.add_argument('--center-freq', type=float, default=None,
|
|
270
|
+
help="RF center frequency in Hz (default: from --ru)")
|
|
271
|
+
parser.add_argument('--sample-rate', type=float, default=None, metavar='HZ',
|
|
272
|
+
help="Front-end (ADC) sample rate in Hz, written as the SigMF "
|
|
273
|
+
"core:sample_rate (default: from --ru; when the profile has "
|
|
274
|
+
"none, the occupied bandwidth)")
|
|
224
275
|
parser.add_argument('--timed', type=int, default=0, metavar='SECONDS',
|
|
225
276
|
help="Stop automatically after SECONDS (0 = run indefinitely)")
|
|
226
277
|
parser.add_argument('--model', type=str, default='',
|
|
@@ -233,23 +284,22 @@ if __name__ == "__main__":
|
|
|
233
284
|
help="Samples to retain after CNN energy peak detection")
|
|
234
285
|
parser.add_argument('--sampling-threshold', type=int, default=5,
|
|
235
286
|
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=
|
|
287
|
+
parser.add_argument('--max-samples-per-file', type=int, default=None,
|
|
237
288
|
help="Rotate the SigMF capture file once a segment reaches "
|
|
238
|
-
"this many true IQ samples (default
|
|
289
|
+
"this many true IQ samples (default: one second of the "
|
|
290
|
+
"dApp's write rate, num_prbs * 12 * SCS). Each "
|
|
239
291
|
"indication is kept whole, so a segment may exceed the "
|
|
240
292
|
"threshold by up to one indication's worth of samples. "
|
|
241
|
-
"Segment wall-clock duration depends on the capture rate. "
|
|
242
293
|
"Only used with --save-iqs.")
|
|
243
294
|
parser.add_argument('--ground-truth', type=str, default='', metavar='LABEL',
|
|
244
295
|
help="Initial ground truth label written into IQ annotations "
|
|
245
296
|
"(only used with --save-iqs). "
|
|
246
297
|
"Updatable at runtime via the dashboard GUI "
|
|
247
298
|
"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).")
|
|
299
|
+
parser.add_argument('--fp16-beta', type=float, default=None,
|
|
300
|
+
help="FP16 IQ rescale factor; MUST match the gNB build/config "
|
|
301
|
+
"(the reader rescales by 1/beta). Default from --ru: "
|
|
302
|
+
"1/128 for USRPs, 1/2048 otherwise.")
|
|
253
303
|
parser.add_argument('--encoding-method', type=str, default='asn1',
|
|
254
304
|
choices=['asn1', 'json', 'protobuf'],
|
|
255
305
|
help="Wire encoding for Spectrum-* envelopes (default: asn1).")
|
|
@@ -276,6 +326,11 @@ if __name__ == "__main__":
|
|
|
276
326
|
"require fully-clean slots (equivalent to "
|
|
277
327
|
"--strict-sensing).")
|
|
278
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
|
+
|
|
279
334
|
# --- Optional sensing-policy toggle ----------------------------- #
|
|
280
335
|
# Drives the gNB's masked UL TDA selector via spectrum_sm ctrl_id=2.
|
|
281
336
|
# Disabled by default (toggle-period=0); set --toggle-period to a
|
|
@@ -19,12 +19,14 @@ classifiers = [
|
|
|
19
19
|
]
|
|
20
20
|
license = "Apache-2.0"
|
|
21
21
|
license-files = ["LICENSE"]
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
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().
|
|
26
27
|
# (asn1tools is still required for the in-Python service-model codecs.)
|
|
27
28
|
dependencies = [
|
|
29
|
+
"libe3py>=0.2.3,<0.3",
|
|
28
30
|
"asn1tools",
|
|
29
31
|
"typing_extensions",
|
|
30
32
|
"numpy",
|
|
@@ -76,7 +78,6 @@ wheel.packages = [
|
|
|
76
78
|
"src/dapp",
|
|
77
79
|
"src/e3interface",
|
|
78
80
|
"src/spectrum",
|
|
79
|
-
"src/nvidia",
|
|
80
81
|
"src/simple",
|
|
81
82
|
"src/iq_saver",
|
|
82
83
|
"src/visualization",
|
|
@@ -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
|