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.
Files changed (115) hide show
  1. dapps-0.3.0/.github/workflows/test.yml +38 -0
  2. {dapps-0.2.2 → dapps-0.3.0}/PKG-INFO +16 -10
  3. {dapps-0.2.2 → dapps-0.3.0}/README.md +14 -9
  4. dapps-0.3.0/VERSION +1 -0
  5. dapps-0.3.0/examples/hackathon_policy.py +27 -0
  6. {dapps-0.2.2 → dapps-0.3.0}/examples/spectrum_dapp.py +33 -2
  7. {dapps-0.2.2 → dapps-0.3.0}/pyproject.toml +6 -7
  8. dapps-0.3.0/src/dapp/control_server.py +126 -0
  9. {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/libe3_agent.py +10 -10
  10. dapps-0.3.0/src/spectrum/README.md +104 -0
  11. dapps-0.3.0/src/spectrum/control_handlers.py +150 -0
  12. dapps-0.3.0/src/spectrum/policy.py +171 -0
  13. dapps-0.3.0/src/spectrum/slot_recorder.py +263 -0
  14. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/spectrum_dapp.py +231 -36
  15. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/threshold_detector.py +54 -0
  16. {dapps-0.2.2 → dapps-0.3.0}/src/visualization/subcarrier/start_subcarrier_visualizer.sh +10 -0
  17. {dapps-0.2.2 → dapps-0.3.0}/src/visualization/subcarrier/subcarrier_visualizer.py +266 -32
  18. {dapps-0.2.2 → dapps-0.3.0}/src/visualization/subcarrier_pub.py +71 -8
  19. dapps-0.3.0/tests/test_control_handlers.py +331 -0
  20. dapps-0.3.0/tests/test_control_server.py +95 -0
  21. dapps-0.3.0/tests/test_detector_configure.py +60 -0
  22. dapps-0.3.0/tests/test_detector_granularity_pipeline.py +171 -0
  23. {dapps-0.2.2 → dapps-0.3.0}/tests/test_libe3_agent.py +2 -2
  24. dapps-0.3.0/tests/test_policy.py +120 -0
  25. dapps-0.3.0/tests/test_subcarrier_pub_viz.py +111 -0
  26. dapps-0.3.0/tests/test_visualizer_grid.py +70 -0
  27. dapps-0.2.2/.github/workflows/test.yml +0 -29
  28. dapps-0.2.2/VERSION +0 -1
  29. dapps-0.2.2/src/spectrum/README.md +0 -56
  30. {dapps-0.2.2 → dapps-0.3.0}/.github/CODEOWNERS +0 -0
  31. {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  32. {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  33. {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/documentation.yml +0 -0
  34. {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  35. {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/new_sm.yml +0 -0
  36. {dapps-0.2.2 → dapps-0.3.0}/.github/ISSUE_TEMPLATE/question.yml +0 -0
  37. {dapps-0.2.2 → dapps-0.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  38. {dapps-0.2.2 → dapps-0.3.0}/.github/workflows/mirror.yml +0 -0
  39. {dapps-0.2.2 → dapps-0.3.0}/.github/workflows/tag-and-release.yml +0 -0
  40. {dapps-0.2.2 → dapps-0.3.0}/.gitignore +0 -0
  41. {dapps-0.2.2 → dapps-0.3.0}/CMakeLists.txt +0 -0
  42. {dapps-0.2.2 → dapps-0.3.0}/CONTRIBUTING.md +0 -0
  43. {dapps-0.2.2 → dapps-0.3.0}/CONTRIBUTORS +0 -0
  44. {dapps-0.2.2 → dapps-0.3.0}/LICENSE +0 -0
  45. {dapps-0.2.2 → dapps-0.3.0}/MANIFEST.in +0 -0
  46. {dapps-0.2.2 → dapps-0.3.0}/SECURITY.md +0 -0
  47. {dapps-0.2.2 → dapps-0.3.0}/docs/dapp_uml.txt +0 -0
  48. {dapps-0.2.2 → dapps-0.3.0}/docs/ground_truth_labeling.md +0 -0
  49. {dapps-0.2.2 → dapps-0.3.0}/docs/gui_uml.txt +0 -0
  50. {dapps-0.2.2 → dapps-0.3.0}/examples/__init__.py +0 -0
  51. {dapps-0.2.2 → dapps-0.3.0}/examples/simple_dapp.py +0 -0
  52. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/CMakeLists.txt +0 -0
  53. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/cmake/libiqsaver.pc.in +0 -0
  54. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/cmake/libiqsaverConfig.cmake.in +0 -0
  55. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/include/iqsaver/annotation.hpp +0 -0
  56. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/include/iqsaver/iq_saver_writer.hpp +0 -0
  57. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/include/iqsaver/iqsaver.hpp +0 -0
  58. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/include/iqsaver/types.hpp +0 -0
  59. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/include/iqsaver/version.hpp.in +0 -0
  60. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/src/iq_saver_writer.cpp +0 -0
  61. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/swig/iqsaver.i +0 -0
  62. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/swig/iqsaver_swig.cpp +0 -0
  63. {dapps-0.2.2 → dapps-0.3.0}/libiqsaver/swig/iqsaver_swig.hpp +0 -0
  64. {dapps-0.2.2 → dapps-0.3.0}/src/dapp/__init__.py +0 -0
  65. {dapps-0.2.2 → dapps-0.3.0}/src/dapp/dapp.py +0 -0
  66. {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/__init__.py +0 -0
  67. {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/e3_interface.py +0 -0
  68. {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/e3_logging.py +0 -0
  69. {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/e3_transport.py +0 -0
  70. {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/latrec.py +0 -0
  71. {dapps-0.2.2 → dapps-0.3.0}/src/e3interface/sm_helpers.py +0 -0
  72. {dapps-0.2.2 → dapps-0.3.0}/src/iq_saver/__init__.py +0 -0
  73. {dapps-0.2.2 → dapps-0.3.0}/src/iq_saver/iq_saver.py +0 -0
  74. {dapps-0.2.2 → dapps-0.3.0}/src/simple/__init__.py +0 -0
  75. {dapps-0.2.2 → dapps-0.3.0}/src/simple/defs/__init__.py +0 -0
  76. {dapps-0.2.2 → dapps-0.3.0}/src/simple/defs/e3sm_simple.proto +0 -0
  77. {dapps-0.2.2 → dapps-0.3.0}/src/simple/defs/e3sm_simple_pb2.py +0 -0
  78. {dapps-0.2.2 → dapps-0.3.0}/src/simple/simple_dapp.py +0 -0
  79. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/__init__.py +0 -0
  80. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/adaptive_noise_floor.py +0 -0
  81. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/__init__.py +0 -0
  82. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_oai_l1_kpm.asn +0 -0
  83. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_oai_l1_kpm.proto +0 -0
  84. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_oai_l1_kpm_pb2.py +0 -0
  85. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum.asn +0 -0
  86. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum.json +0 -0
  87. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum.proto +0 -0
  88. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/defs/e3sm_spectrum_pb2.py +0 -0
  89. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/e3_l2_sensing_reader.py +0 -0
  90. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/e3_ran_buffers_reader.py +0 -0
  91. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/embargo_manager.py +0 -0
  92. {dapps-0.2.2 → dapps-0.3.0}/src/spectrum/ru_profiles.py +0 -0
  93. {dapps-0.2.2 → dapps-0.3.0}/src/visualization/__init__.py +0 -0
  94. {dapps-0.2.2 → dapps-0.3.0}/src/visualization/energy.py +0 -0
  95. {dapps-0.2.2 → dapps-0.3.0}/src/visualization/grafana.py +0 -0
  96. {dapps-0.2.2 → dapps-0.3.0}/src/visualization/influx.py +0 -0
  97. {dapps-0.2.2 → dapps-0.3.0}/src/visualization/iq.py +0 -0
  98. {dapps-0.2.2 → dapps-0.3.0}/src/visualization/subcarrier/insi_white.png +0 -0
  99. {dapps-0.2.2 → dapps-0.3.0}/start_dapp.sh +0 -0
  100. {dapps-0.2.2 → dapps-0.3.0}/tests/test_e3_concurrency.py +0 -0
  101. {dapps-0.2.2 → dapps-0.3.0}/tests/test_indication_dispatch.py +0 -0
  102. {dapps-0.2.2 → dapps-0.3.0}/tests/test_iq_saver.py +0 -0
  103. {dapps-0.2.2 → dapps-0.3.0}/tests/test_latrec.py +0 -0
  104. {dapps-0.2.2 → dapps-0.3.0}/tests/test_latrec_stamps.py +0 -0
  105. {dapps-0.2.2 → dapps-0.3.0}/tests/test_outbound_send_result.py +0 -0
  106. {dapps-0.2.2 → dapps-0.3.0}/tests/test_protobuf_sm.py +0 -0
  107. {dapps-0.2.2 → dapps-0.3.0}/tests/test_ru_profiles.py +0 -0
  108. {dapps-0.2.2 → dapps-0.3.0}/tests/test_sensing_policy.py +0 -0
  109. {dapps-0.2.2 → dapps-0.3.0}/tests/test_spectrum_sm_timestamps_mask.py +0 -0
  110. {dapps-0.2.2 → dapps-0.3.0}/tests/test_spectrum_telemetry.py +0 -0
  111. {dapps-0.2.2 → dapps-0.3.0}/tools/README.md +0 -0
  112. {dapps-0.2.2 → dapps-0.3.0}/tools/asntojson.py +0 -0
  113. {dapps-0.2.2 → dapps-0.3.0}/tools/generate_sm.sh +0 -0
  114. {dapps-0.2.2 → dapps-0.3.0}/tools/read_recording.py +0 -0
  115. {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.2.2
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 Python bindings (`libe3py`)
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. You must therefore install libe3 with its Python bindings **into the
61
- same interpreter/venv** that runs the dApp, *before* installing this package:
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 ~/libe3
65
- ./build_libe3 --install --enable-swig \
66
- --cmake-opt "-DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=ON"
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: build the bindings with
77
- `--cmake-opt "-DLIBE3_ENABLE_LATREC=ON"` and the dApp stamps its E3SM decode,
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 Python bindings (`libe3py`)
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. You must therefore install libe3 with its Python bindings **into the
14
- same interpreter/venv** that runs the dApp, *before* installing this package:
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 ~/libe3
18
- ./build_libe3 --install --enable-swig \
19
- --cmake-opt "-DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=ON"
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: build the bindings with
30
- `--cmake-opt "-DLIBE3_ENABLE_LATREC=ON"` and the dApp stamps its E3SM decode,
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
- print(f'Threshold is {noise_floor_threshold}')
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.use_adaptive_noise_floor:
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
- # NOTE: E3AP transport is provided by libe3's Python bindings (`libe3py`),
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:
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`` is provided by installing libe3 with the SWIG bindings enabled::
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 --install --enable-swig \
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
- " cd ~/libe3 && ./build_libe3 --install --enable-swig "
68
- '--cmake-opt "-DLIBE3_ENABLE_ASN1=ON -DLIBE3_ENABLE_JSON=ON"'
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. Build and install the libe3 Python bindings "
73
- "into this environment:\n" + _BUILD_CMD
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'. Rebuild and reinstall the bindings:\n" + _BUILD_CMD
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.