dapps 0.1.1__tar.gz → 0.2.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 (97) hide show
  1. {dapps-0.1.1 → dapps-0.2.0}/.github/ISSUE_TEMPLATE/new_sm.yml +1 -1
  2. {dapps-0.1.1 → dapps-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +1 -1
  3. {dapps-0.1.1 → dapps-0.2.0}/CONTRIBUTING.md +1 -1
  4. {dapps-0.1.1 → dapps-0.2.0}/PKG-INFO +5 -3
  5. dapps-0.2.0/VERSION +1 -0
  6. {dapps-0.1.1 → dapps-0.2.0}/examples/spectrum_dapp.py +112 -30
  7. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/include/iqsaver/iqsaver.hpp +1 -1
  8. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/include/iqsaver/types.hpp +1 -1
  9. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/src/iq_saver_writer.cpp +26 -2
  10. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/swig/iqsaver_swig.hpp +1 -1
  11. {dapps-0.1.1 → dapps-0.2.0}/pyproject.toml +3 -2
  12. {dapps-0.1.1 → dapps-0.2.0}/src/dapp/dapp.py +5 -2
  13. {dapps-0.1.1 → dapps-0.2.0}/src/e3interface/defs/e3.json +63 -141
  14. {dapps-0.1.1 → dapps-0.2.0}/src/e3interface/e3_connector.py +39 -4
  15. {dapps-0.1.1 → dapps-0.2.0}/src/e3interface/e3_encoder.py +125 -28
  16. {dapps-0.1.1 → dapps-0.2.0}/src/e3interface/e3_interface.py +241 -144
  17. {dapps-0.1.1 → dapps-0.2.0}/src/iq_saver/iq_saver.py +75 -11
  18. dapps-0.2.0/src/simple/defs/e3sm_simple.asn +111 -0
  19. {dapps-0.1.1 → dapps-0.2.0}/src/simple/simple_dapp.py +52 -6
  20. dapps-0.2.0/src/spectrum/README.md +56 -0
  21. {dapps-0.1.1 → dapps-0.2.0}/src/spectrum/adaptive_noise_floor.py +22 -3
  22. dapps-0.2.0/src/spectrum/defs/e3sm_oai_l1_kpm.asn +97 -0
  23. dapps-0.2.0/src/spectrum/defs/e3sm_spectrum.asn +156 -0
  24. dapps-0.2.0/src/spectrum/defs/e3sm_spectrum.json +250 -0
  25. dapps-0.2.0/src/spectrum/e3_l2_sensing_reader.py +155 -0
  26. dapps-0.2.0/src/spectrum/e3_ran_buffers_reader.py +338 -0
  27. dapps-0.2.0/src/spectrum/spectrum_dapp.py +2055 -0
  28. {dapps-0.1.1 → dapps-0.2.0}/src/spectrum/threshold_detector.py +32 -2
  29. dapps-0.2.0/src/visualization/subcarrier/insi_white.png +0 -0
  30. dapps-0.2.0/src/visualization/subcarrier/start_subcarrier_visualizer.sh +51 -0
  31. dapps-0.2.0/src/visualization/subcarrier/subcarrier_visualizer.py +2504 -0
  32. dapps-0.2.0/src/visualization/subcarrier_pub.py +192 -0
  33. dapps-0.2.0/start_dapp.sh +2 -0
  34. dapps-0.2.0/tests/test_e3_concurrency.py +68 -0
  35. dapps-0.2.0/tests/test_encoder_roundtrip.py +186 -0
  36. dapps-0.2.0/tests/test_indication_dispatch.py +182 -0
  37. {dapps-0.1.1 → dapps-0.2.0}/tests/test_iq_saver.py +215 -12
  38. dapps-0.2.0/tests/test_sensing_policy.py +253 -0
  39. dapps-0.2.0/tests/test_spectrum_telemetry.py +87 -0
  40. dapps-0.1.1/VERSION +0 -1
  41. dapps-0.1.1/src/simple/defs/e3sm_simple.asn +0 -54
  42. dapps-0.1.1/src/spectrum/defs/e3sm_spectrum.asn +0 -76
  43. dapps-0.1.1/src/spectrum/defs/e3sm_spectrum.json +0 -137
  44. dapps-0.1.1/src/spectrum/spectrum_dapp.py +0 -743
  45. dapps-0.1.1/src/visualization/dashboard.py +0 -185
  46. dapps-0.1.1/src/visualization/templates/index.html +0 -508
  47. dapps-0.1.1/tests/test_dashboard.py +0 -43
  48. {dapps-0.1.1 → dapps-0.2.0}/.github/CODEOWNERS +0 -0
  49. {dapps-0.1.1 → dapps-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  50. {dapps-0.1.1 → dapps-0.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  51. {dapps-0.1.1 → dapps-0.2.0}/.github/ISSUE_TEMPLATE/documentation.yml +0 -0
  52. {dapps-0.1.1 → dapps-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  53. {dapps-0.1.1 → dapps-0.2.0}/.github/ISSUE_TEMPLATE/question.yml +0 -0
  54. {dapps-0.1.1 → dapps-0.2.0}/.github/workflows/mirror.yml +0 -0
  55. {dapps-0.1.1 → dapps-0.2.0}/.github/workflows/tag-and-release.yml +0 -0
  56. {dapps-0.1.1 → dapps-0.2.0}/.github/workflows/test.yml +0 -0
  57. {dapps-0.1.1 → dapps-0.2.0}/.gitignore +0 -0
  58. {dapps-0.1.1 → dapps-0.2.0}/CMakeLists.txt +0 -0
  59. {dapps-0.1.1 → dapps-0.2.0}/CONTRIBUTORS +0 -0
  60. {dapps-0.1.1 → dapps-0.2.0}/LICENSE +0 -0
  61. {dapps-0.1.1 → dapps-0.2.0}/MANIFEST.in +0 -0
  62. {dapps-0.1.1 → dapps-0.2.0}/README.md +0 -0
  63. {dapps-0.1.1 → dapps-0.2.0}/SECURITY.md +0 -0
  64. {dapps-0.1.1 → dapps-0.2.0}/docs/dapp_uml.txt +0 -0
  65. {dapps-0.1.1 → dapps-0.2.0}/docs/ground_truth_labeling.md +0 -0
  66. {dapps-0.1.1 → dapps-0.2.0}/docs/gui_uml.txt +0 -0
  67. {dapps-0.1.1 → dapps-0.2.0}/examples/nvidia_dapp.py +0 -0
  68. {dapps-0.1.1 → dapps-0.2.0}/examples/simple_dapp.py +0 -0
  69. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/CMakeLists.txt +0 -0
  70. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/cmake/libiqsaver.pc.in +0 -0
  71. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/cmake/libiqsaverConfig.cmake.in +0 -0
  72. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/include/iqsaver/annotation.hpp +0 -0
  73. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/include/iqsaver/iq_saver_writer.hpp +0 -0
  74. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/include/iqsaver/version.hpp.in +0 -0
  75. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/swig/iqsaver.i +0 -0
  76. {dapps-0.1.1 → dapps-0.2.0}/libiqsaver/swig/iqsaver_swig.cpp +0 -0
  77. {dapps-0.1.1 → dapps-0.2.0}/src/dapp/__init__.py +0 -0
  78. {dapps-0.1.1 → dapps-0.2.0}/src/e3interface/__init__.py +0 -0
  79. {dapps-0.1.1 → dapps-0.2.0}/src/e3interface/defs/e3.asn +0 -0
  80. {dapps-0.1.1 → dapps-0.2.0}/src/e3interface/e3_logging.py +0 -0
  81. {dapps-0.1.1 → dapps-0.2.0}/src/iq_saver/__init__.py +0 -0
  82. {dapps-0.1.1 → dapps-0.2.0}/src/nvidia/__init__.py +0 -0
  83. {dapps-0.1.1 → dapps-0.2.0}/src/nvidia/defs/e3sm_nvidia.asn +0 -0
  84. {dapps-0.1.1 → dapps-0.2.0}/src/nvidia/defs/e3sm_nvidia.json +0 -0
  85. {dapps-0.1.1 → dapps-0.2.0}/src/nvidia/nvidia_dapp.py +0 -0
  86. {dapps-0.1.1 → dapps-0.2.0}/src/simple/__init__.py +0 -0
  87. {dapps-0.1.1 → dapps-0.2.0}/src/spectrum/__init__.py +0 -0
  88. {dapps-0.1.1 → dapps-0.2.0}/src/spectrum/embargo_manager.py +0 -0
  89. {dapps-0.1.1 → dapps-0.2.0}/src/visualization/__init__.py +0 -0
  90. {dapps-0.1.1 → dapps-0.2.0}/src/visualization/energy.py +0 -0
  91. {dapps-0.1.1 → dapps-0.2.0}/src/visualization/grafana.py +0 -0
  92. {dapps-0.1.1 → dapps-0.2.0}/src/visualization/influx.py +0 -0
  93. {dapps-0.1.1 → dapps-0.2.0}/src/visualization/iq.py +0 -0
  94. {dapps-0.1.1 → dapps-0.2.0}/tools/README.md +0 -0
  95. {dapps-0.1.1 → dapps-0.2.0}/tools/asntojson.py +0 -0
  96. {dapps-0.1.1 → dapps-0.2.0}/tools/generate_sm.sh +0 -0
  97. {dapps-0.1.1 → dapps-0.2.0}/tools/read_recording.py +0 -0
@@ -77,7 +77,7 @@ body:
77
77
  id: paired_issue
78
78
  attributes:
79
79
  label: Paired libe3 issue / PR (if any)
80
- placeholder: "wineslab/dApp-libe3#123"
80
+ placeholder: "wineslab/libe3#123"
81
81
  validations:
82
82
  required: false
83
83
 
@@ -43,7 +43,7 @@ These mirror what CI (`.github/workflows/test.yml`) enforces. **All boxes must b
43
43
 
44
44
  ## Twin-repo coordination
45
45
 
46
- dapps is paired with [`libe3`](https://github.com/wineslab/dApp-libe3) and [`dApp-openairinterface5g`](https://github.com/wineslab/dApp-openairinterface5g). Breaking changes to the twin repos are not accepted.
46
+ dapps is paired with [`libe3`](https://github.com/wineslab/libe3) and [`dApp-openairinterface5g`](https://github.com/wineslab/dApp-openairinterface5g). Breaking changes to the twin repos are not accepted.
47
47
 
48
48
  - [ ] This PR does not break the E3 wire protocol, OR a paired PR exists in the affected twin repo (link below).
49
49
 
@@ -58,7 +58,7 @@ The following are **mandatory** for every contribution. PRs that do not meet the
58
58
 
59
59
  ### Service Models and twin-repo coordination
60
60
 
61
- This repository is part of a framework that includes [`libe3`](https://github.com/wineslab/dApp-libe3) (the gNB-side E3 agent) and [`dApp-openairinterface5g`](https://github.com/wineslab/dApp-openairinterface5g) (the OAI fork). PRs that change the E3 wire protocol or add a new Service Model must come with a paired PR in the affected twin repo, linked from the PR description.
61
+ This repository is part of a framework that includes [`libe3`](https://github.com/wineslab/libe3) (the gNB-side E3 agent) and [`dApp-openairinterface5g`](https://github.com/wineslab/dApp-openairinterface5g) (the OAI fork). PRs that change the E3 wire protocol or add a new Service Model must come with a paired PR in the affected twin repo, linked from the PR description.
62
62
 
63
63
  ## Code of Conduct
64
64
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dapps
3
- Version: 0.1.1
3
+ Version: 0.2.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>
@@ -28,14 +28,16 @@ Provides-Extra: cnn
28
28
  Requires-Dist: libiq; extra == "cnn"
29
29
  Provides-Extra: gui
30
30
  Requires-Dist: flask; extra == "gui"
31
- Requires-Dist: flask-socketio; extra == "gui"
31
+ Requires-Dist: flask-sock; extra == "gui"
32
+ Requires-Dist: flask-cors; extra == "gui"
32
33
  Requires-Dist: matplotlib; extra == "gui"
33
34
  Provides-Extra: network
34
35
  Requires-Dist: pysctp; extra == "network"
35
36
  Provides-Extra: all
36
37
  Requires-Dist: pysctp; extra == "all"
37
38
  Requires-Dist: flask; extra == "all"
38
- Requires-Dist: flask-socketio; extra == "all"
39
+ Requires-Dist: flask-sock; extra == "all"
40
+ Requires-Dist: flask-cors; extra == "all"
39
41
  Requires-Dist: matplotlib; extra == "all"
40
42
  Requires-Dist: influxdb_client; extra == "all"
41
43
  Requires-Dist: libiq; extra == "all"
dapps-0.2.0/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -9,7 +9,11 @@ import time
9
9
  import logging
10
10
 
11
11
  from e3interface.e3_connector import E3LinkLayer, E3TransportLayer
12
- from spectrum.spectrum_dapp import SpectrumSharingDApp, compute_fft_size
12
+ from spectrum.spectrum_dapp import (
13
+ SpectrumSharingDApp,
14
+ compute_fft_size,
15
+ make_periodic_toggle_callback,
16
+ )
13
17
  from spectrum.threshold_detector import StaticThresholdDetector, AdaptiveThresholdDetector
14
18
 
15
19
  LOG_DIR = '/tmp/'
@@ -23,17 +27,15 @@ def stop_program(time_to_wait, dapp: SpectrumSharingDApp):
23
27
 
24
28
  def main(args):
25
29
  if args.model:
26
- try:
27
- from libiq.classifier.cnn import Classifier
28
- except ModuleNotFoundError:
29
- print(
30
- "Optional dependencies to run this example are not installed.\n"
31
- "Fix this by running:\n\n"
32
- " pip install libiq # OR\n"
33
- " pip install dapps[cnn] (minimal) # OR\n"
34
- " pip install dapps[all] (preferred)\n"
35
- )
36
- exit(-1)
30
+ # CNN signal classification is not wired into SpectrumSharingDApp; a
31
+ # classifier= kwarg would fall through to **kwargs and be dropped, so
32
+ # --model (and --time-window/--moving-avg-window/--extraction-window)
33
+ # would silently do nothing. Fail loudly instead.
34
+ raise SystemExit(
35
+ "--model (CNN signal classification) is not supported by "
36
+ "SpectrumSharingDApp; remove --model and the associated "
37
+ "--time-window/--moving-avg-window/--extraction-window flags."
38
+ )
37
39
 
38
40
  # This value really depends on the RF conditions and the RU used and
39
41
  # should be carefully calibrated.
@@ -81,16 +83,6 @@ def main(args):
81
83
  f" | window: {args.average_over_frames} frames"
82
84
  )
83
85
 
84
- classifier = None
85
- if args.model:
86
- classifier = Classifier(
87
- time_window=args.time_window,
88
- input_vector=fft_size,
89
- moving_avg_window=args.moving_avg_window,
90
- extraction_window=args.extraction_window,
91
- model_path=args.model,
92
- )
93
-
94
86
  dapp = SpectrumSharingDApp(
95
87
  detector=detector,
96
88
  save_iqs=args.save_iqs,
@@ -100,14 +92,21 @@ def main(args):
100
92
  energyGui=args.energy_gui,
101
93
  iqPlotterGui=args.iq_plotter_gui,
102
94
  dashboard=args.demo_gui,
103
- classifier=classifier,
95
+ viz_web_port=args.viz_web_port,
96
+ viz_zmq_port=args.viz_zmq_port,
97
+ external_viz=args.external_viz,
104
98
  center_freq=args.center_freq,
105
99
  num_prbs=args.num_prbs,
106
100
  e_sampling=args.e,
107
101
  num_subcarrier_spacing=args.num_subcarrier_spacing,
108
102
  sampling_threshold=args.sampling_threshold,
103
+ max_samples_per_file=args.max_samples_per_file,
104
+ fp16_beta=args.fp16_beta,
105
+ sensing_only=args.sensing_only,
106
+ strict_sensing=args.strict_sensing,
107
+ min_sensing_symbols=args.min_sensing_symbols,
108
+ encoding_method=args.encoding_method,
109
109
  ground_truth=args.ground_truth,
110
- show_controls=args.show_controls,
111
110
  dapp_name="SpectrumSharing",
112
111
  dapp_version="1.0.0",
113
112
  vendor="WinesLab",
@@ -137,7 +136,29 @@ def main(args):
137
136
  )
138
137
  time.sleep(1)
139
138
 
140
- dapp.send_subscription_request()
139
+ if not dapp.send_subscription_request():
140
+ # send_subscription_request returns True only if the gNB accepted both
141
+ # the L1-KPM (RF=2) and Spectrum (RF=1) subscriptions; anything else
142
+ # means the dApp would run deaf, so abort with a non-zero exit.
143
+ dapp.stop()
144
+ raise SystemExit("[FATAL] gNB did not accept the dApp subscriptions; aborting")
145
+
146
+ # Optional sensing-policy toggle. When --toggle-period > 0, installs
147
+ # a periodic on/off toggle that flips the gNB's masked UL TDA
148
+ # selector via spectrum_sm ctrl_id=2. Operator must have configured
149
+ # additional_ul_tdas in the gNB (e.g. "0:7") for the scheduler to
150
+ # have a short TDA to switch to. See examples/README or the gNB
151
+ # boot log ([additional] tag) to verify.
152
+ if args.toggle_period > 0:
153
+ print(f"[INFO] Installing sensing-policy toggle: "
154
+ f"period={args.toggle_period}s "
155
+ f"n_slots={args.toggle_n_slots} "
156
+ f"mask_when_on=0x{args.toggle_mask:04x}")
157
+ dapp.set_sensing_policy_logic(make_periodic_toggle_callback(
158
+ period_s=args.toggle_period,
159
+ n_slots=args.toggle_n_slots,
160
+ mask_when_on=args.toggle_mask,
161
+ ))
141
162
 
142
163
  if args.timed:
143
164
  timer = threading.Thread(target=stop_program, args=(args.timed, dapp), daemon=False)
@@ -167,7 +188,8 @@ if __name__ == "__main__":
167
188
  parser.add_argument('--save-iqs', action='store_true', default=False,
168
189
  help="Save I/Q samples to SigMF files")
169
190
  parser.add_argument('--control', action='store_true', default=False,
170
- help="Send PRB blacklist control messages to the gNB")
191
+ help="Send PRB-block control messages to the gNB when "
192
+ "PRBs are detected above the noise threshold")
171
193
  parser.add_argument('--noise-floor-threshold', type=int, default=None,
172
194
  help="Detection threshold in dB (static) or dB above noise floor (adaptive)")
173
195
  parser.add_argument('--use-adaptive-noise-floor', action='store_true', default=False,
@@ -184,7 +206,13 @@ if __name__ == "__main__":
184
206
  parser.add_argument('--iq-plotter-gui', action='store_true', default=False,
185
207
  help="Enable IQ time-domain plotter")
186
208
  parser.add_argument('--demo-gui', action='store_true', default=False,
187
- help="Enable dashboard visualization")
209
+ help="Publish per-slot subcarrier power to the visualizer")
210
+ parser.add_argument('--viz-web-port', type=int, default=5001,
211
+ help="Visualizer web UI port (default 5001)")
212
+ parser.add_argument('--viz-zmq-port', type=int, default=5559,
213
+ help="ZMQ port the dApp publishes on / the visualizer reads (default 5559)")
214
+ parser.add_argument('--external-viz', action='store_true', default=False,
215
+ help="Don't spawn the visualizer; publish to an already-running one")
188
216
  parser.add_argument('--num-prbs', type=int, default=106,
189
217
  help="Number of PRBs")
190
218
  parser.add_argument('--num-subcarrier-spacing', type=int, default=30,
@@ -205,14 +233,68 @@ if __name__ == "__main__":
205
233
  help="Samples to retain after CNN energy peak detection")
206
234
  parser.add_argument('--sampling-threshold', type=int, default=5,
207
235
  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=46_080_000,
237
+ help="Rotate the SigMF capture file once a segment reaches "
238
+ "this many true IQ samples (default 46080000). Each "
239
+ "indication is kept whole, so a segment may exceed the "
240
+ "threshold by up to one indication's worth of samples. "
241
+ "Segment wall-clock duration depends on the capture rate. "
242
+ "Only used with --save-iqs.")
208
243
  parser.add_argument('--ground-truth', type=str, default='', metavar='LABEL',
209
244
  help="Initial ground truth label written into IQ annotations "
210
245
  "(only used with --save-iqs). "
211
246
  "Updatable at runtime via the dashboard GUI "
212
247
  "when --demo-gui is also set.")
213
- parser.add_argument('--show-controls', action='store_true', default=False,
214
- help="Show tunable controls (sampling threshold) in the dashboard GUI. "
215
- "Only meaningful with --demo-gui.")
248
+ parser.add_argument('--fp16-beta', type=float, default=1.0 / 2048.0,
249
+ help="FP16 IQ rescale factor; MUST match the gNB "
250
+ "E3Configuration.fp16_beta (the reader rescales by "
251
+ "1/beta). Default 1/2048 matches the gNB code default; "
252
+ "the X410 sample conf overrides it to 0.0078125 (1/128).")
253
+ parser.add_argument('--encoding-method', type=str, default='asn1',
254
+ choices=['asn1', 'json'],
255
+ help="Wire encoding for Spectrum-* envelopes (default: asn1).")
256
+ parser.add_argument('--no-sensing-only', dest='sensing_only',
257
+ action='store_false', default=True,
258
+ help="Disable the sensing-window filter. By default the dApp uses the "
259
+ "sensing ranges from the Spectrum SM (RF=1) to slice the detector "
260
+ "input to sensing-PUSCH cells; passing this flag uses every cell.")
261
+ parser.add_argument('--strict-sensing', dest='strict_sensing',
262
+ action='store_true', default=False,
263
+ help="Stricter sensing filter: drop any slot whose sensing window "
264
+ "doesn't cover the full slot (i.e. any UE PUSCH was granted "
265
+ "in that slot). Eliminates UE CP/spectral bleed at the cost "
266
+ "of a sparser waterfall under heavy UE traffic. Requires "
267
+ "--sensing-only (the default).")
268
+ parser.add_argument('--min-sensing-symbols', type=int, default=1,
269
+ help="Minimum number of kept symbols required to emit the slot to "
270
+ "the dashboard. Default 1: display every slot that has any "
271
+ "sensing symbol — keeps the waterfall scrolling even under "
272
+ "heavy UE UL where the MAC scheduler leaves only one free "
273
+ "symbol per slot. Set to 2 to drop the typical \"only sym 13 "
274
+ "survived\" partial-sensing slots (evens out brightness at "
275
+ "the cost of dashboard freezes under load). Set to 14 to "
276
+ "require fully-clean slots (equivalent to "
277
+ "--strict-sensing).")
278
+
279
+ # --- Optional sensing-policy toggle ----------------------------- #
280
+ # Drives the gNB's masked UL TDA selector via spectrum_sm ctrl_id=2.
281
+ # Disabled by default (toggle-period=0); set --toggle-period to a
282
+ # positive value to enable. Requires additional_ul_tdas (e.g. "0:7")
283
+ # on the gNB so the scheduler has a short TDA to switch to.
284
+ parser.add_argument('--toggle-period', type=float, default=0.0,
285
+ metavar='SECONDS',
286
+ help="Toggle the gNB sensing policy on/off every N seconds "
287
+ "(0 = disabled, default). When > 0, alternates between "
288
+ "installing the mask (active) and clearing it (deactivate). "
289
+ "Requires additional_ul_tdas configured on the gNB.")
290
+ parser.add_argument('--toggle-n-slots', type=int, default=20,
291
+ help="numb_slots_frame of the gNB for the per-slot mask "
292
+ "(mu=1 -> 20). Must match the gNB exactly or the gNB "
293
+ "will NACK the policy with an n_slots mismatch.")
294
+ parser.add_argument('--toggle-mask', type=lambda s: int(s, 0), default=0x3F80,
295
+ metavar='HEX',
296
+ help="14-bit symbol bitmap applied uniformly to every slot "
297
+ "when the toggle is active (default 0x3F80 = syms 7..13).")
216
298
 
217
299
  args = parser.parse_args()
218
300
  print("Start dApp")
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // libiqsaver is a SigMF-compliant IQ-sample writer with sample-count and
6
6
  // time-based file rotation, deferred annotations, and a configurable SigMF
7
- // extension namespace (default: "spear:"). The library is the C++ backend
7
+ // extension namespace (default: "dapp:"). The library is the C++ backend
8
8
  // for the Python `iq_saver` package used by the SPEAR spectrum dApp; it is
9
9
  // also usable directly as a standalone C++ library installed via CMake +
10
10
  // pkg-config (libiqsaver.pc).
@@ -19,7 +19,7 @@ struct IQSaverConfig {
19
19
  std::string filename;
20
20
  std::optional<uint64_t> max_samples_per_file;
21
21
  std::optional<double> rotation_interval;
22
- std::string extension_namespace = "spear";
22
+ std::string extension_namespace = "dapp";
23
23
  // JSON-encoded object of extra global metadata fields. Keys whose names
24
24
  // do not already start with `core:` or `<extension_namespace>:` are
25
25
  // prefixed with `<extension_namespace>:` before being written.
@@ -29,6 +29,12 @@ namespace {
29
29
 
30
30
  constexpr const char* kSigMFVersion = "1.0.0";
31
31
 
32
+ // Fallback version stamped in the SigMF `core:extensions` entry for the
33
+ // configured extension namespace when the caller supplies no `schema_version`
34
+ // in extra metadata. Callers (e.g. the spectrum dApp) that co-version their
35
+ // namespace pass an explicit `schema_version`, which takes precedence.
36
+ constexpr const char* kDefaultExtensionVersion = "1.0.0";
37
+
32
38
  double now_seconds() {
33
39
  using namespace std::chrono;
34
40
  return duration_cast<duration<double>>(
@@ -189,6 +195,18 @@ struct IQSaverWriter::Impl {
189
195
  if (!config.hw_info.empty()) {
190
196
  record_global["core:hw"] = config.hw_info;
191
197
  }
198
+ // Declare the extension namespace we stamp so SigMF readers know which
199
+ // extension the `<ns>:*` fields belong to. Version is co-versioned with
200
+ // the namespace: use the caller's `schema_version` when provided, else
201
+ // fall back to kDefaultExtensionVersion. `optional=false` — the `<ns>:*`
202
+ // geometry/domain fields are required to interpret the recording.
203
+ record_global["core:extensions"] = json::array(
204
+ {json{{"name", config.extension_namespace},
205
+ {"version",
206
+ extra_metadata.value(
207
+ "schema_version",
208
+ std::string(kDefaultExtensionVersion))},
209
+ {"optional", false}}});
192
210
  for (auto it = extra_metadata.begin(); it != extra_metadata.end();
193
211
  ++it) {
194
212
  if (it.key() == "sampling_threshold") continue;
@@ -314,9 +332,15 @@ struct IQSaverWriter::Impl {
314
332
  std::optional<double> timestamp) {
315
333
  double ts = timestamp.value_or(now_seconds());
316
334
 
317
- // Rotation check (mirrors Python if/elif at iq_saver.py:244-250)
335
+ // Rotation check. max_samples_per_file is a count of true IQ *samples*,
336
+ // so it is compared against iq_sample_count — NOT frame_count, which
337
+ // counts save_samples() calls. The check runs before the write so each
338
+ // call's samples stay whole within one segment; a segment therefore
339
+ // reaches *at least* max_samples_per_file and may exceed it by up to one
340
+ // call's worth of samples. It is a rotation threshold, not a hard cap —
341
+ // splitting a single call across files would fragment an indication.
318
342
  if (config.max_samples_per_file.has_value() &&
319
- frame_count >= *config.max_samples_per_file) {
343
+ iq_sample_count >= *config.max_samples_per_file) {
320
344
  rotate_file(ts);
321
345
  } else if (config.rotation_interval.has_value() &&
322
346
  file_start_time.has_value() &&
@@ -33,7 +33,7 @@ struct IQSaverConfigSwig {
33
33
  // Negative => unset.
34
34
  long long max_samples_per_file = -1;
35
35
  double rotation_interval = -1.0;
36
- std::string extension_namespace = "spear";
36
+ std::string extension_namespace = "dapp";
37
37
  std::string extra_metadata_json = "{}";
38
38
  };
39
39
 
@@ -43,12 +43,13 @@ Source = "https://github.com/wineslab/dApp-library"
43
43
  [project.optional-dependencies]
44
44
  api = ["influxdb_client"]
45
45
  cnn = ["libiq"]
46
- gui = ["flask", "flask-socketio", "matplotlib"]
46
+ gui = ["flask", "flask-sock", "flask-cors", "matplotlib"]
47
47
  network = ["pysctp"]
48
48
  all = [
49
49
  "pysctp",
50
50
  "flask",
51
- "flask-socketio",
51
+ "flask-sock",
52
+ "flask-cors",
52
53
  "matplotlib",
53
54
  "influxdb_client",
54
55
  "libiq",
@@ -127,8 +127,11 @@ class DApp(ABC):
127
127
  else:
128
128
  dapp_logger.warning(f"Subscription response not positive: {data}")
129
129
 
130
- def send_subscription_request(self, subscriptionTime: int | None = None, periodicity: int | None = None) -> bool:
130
+ def send_subscription_request(self, subscriptionTime: int | None = None, periodicity: int | None = None) -> bool:
131
131
  self.e3_interface.add_subscription_callback(self.dapp_id, self.manage_subscription_response)
132
+
133
+ self.e3_interface.add_indication_callback(self.dapp_id, 0, self._handle_indication)
134
+ self.e3_interface.add_xapp_control_callback(self.dapp_id, 0, self._handle_xapp_control)
132
135
  dapp_logger.debug(f"Subscription callbacks: {self.e3_interface.subscription_callbacks}")
133
136
  scheduled = self.e3_interface.send_subscription_request(
134
137
  self.dapp_id,
@@ -148,7 +151,7 @@ class DApp(ABC):
148
151
  pass
149
152
 
150
153
  @abstractmethod
151
- def _handle_indication(self, dapp_identifier: int, data: bytes):
154
+ def _handle_indication(self, dapp_identifier: int, ran_function_id: int, data: bytes):
152
155
  # This in the future might become a class
153
156
  pass
154
157
 
@@ -344,150 +344,72 @@
344
344
  },
345
345
  "E3-PDU": {
346
346
  "type": "object",
347
+ "required": ["type", "id"],
347
348
  "properties": {
348
- "id": {
349
- "$ref": "#/$defs/E3-MessageID"
350
- },
351
- "msg": {
352
- "oneOf": [
353
- {
354
- "type": "object",
355
- "properties": {
356
- "setupRequest": {
357
- "$ref": "#/$defs/E3-SetupRequest"
358
- }
359
- },
360
- "required": [
361
- "setupRequest"
362
- ],
363
- "additionalProperties": false
364
- },
365
- {
366
- "type": "object",
367
- "properties": {
368
- "setupResponse": {
369
- "$ref": "#/$defs/E3-SetupResponse"
370
- }
371
- },
372
- "required": [
373
- "setupResponse"
374
- ],
375
- "additionalProperties": false
376
- },
377
- {
378
- "type": "object",
379
- "properties": {
380
- "subscriptionRequest": {
381
- "$ref": "#/$defs/E3-SubscriptionRequest"
382
- }
383
- },
384
- "required": [
385
- "subscriptionRequest"
386
- ],
387
- "additionalProperties": false
388
- },
389
- {
390
- "type": "object",
391
- "properties": {
392
- "subscriptionDelete": {
393
- "$ref": "#/$defs/E3-SubscriptionDelete"
394
- }
395
- },
396
- "required": [
397
- "subscriptionDelete"
398
- ],
399
- "additionalProperties": false
400
- },
401
- {
402
- "type": "object",
403
- "properties": {
404
- "subscriptionResponse": {
405
- "$ref": "#/$defs/E3-SubscriptionResponse"
406
- }
407
- },
408
- "required": [
409
- "subscriptionResponse"
410
- ],
411
- "additionalProperties": false
412
- },
413
- {
414
- "type": "object",
415
- "properties": {
416
- "indicationMessage": {
417
- "$ref": "#/$defs/E3-IndicationMessage"
418
- }
419
- },
420
- "required": [
421
- "indicationMessage"
422
- ],
423
- "additionalProperties": false
424
- },
425
- {
426
- "type": "object",
427
- "properties": {
428
- "dAppControlAction": {
429
- "$ref": "#/$defs/E3-DAppControlAction"
430
- }
431
- },
432
- "required": [
433
- "dAppControlAction"
434
- ],
435
- "additionalProperties": false
436
- },
437
- {
438
- "type": "object",
439
- "properties": {
440
- "dAppReport": {
441
- "$ref": "#/$defs/E3-DAppReport"
442
- }
443
- },
444
- "required": [
445
- "dAppReport"
446
- ],
447
- "additionalProperties": false
448
- },
449
- {
450
- "type": "object",
451
- "properties": {
452
- "xAppControlAction": {
453
- "$ref": "#/$defs/E3-XAppControlAction"
454
- }
455
- },
456
- "required": [
457
- "xAppControlAction"
458
- ],
459
- "additionalProperties": false
460
- },
461
- {
462
- "type": "object",
463
- "properties": {
464
- "releaseMessage": {
465
- "$ref": "#/$defs/E3-ReleaseMessage"
466
- }
467
- },
468
- "required": [
469
- "releaseMessage"
470
- ],
471
- "additionalProperties": false
472
- },
473
- {
474
- "type": "object",
475
- "properties": {
476
- "messageAck": {
477
- "$ref": "#/$defs/E3-MessageAck"
478
- }
479
- },
480
- "required": [
481
- "messageAck"
482
- ],
483
- "additionalProperties": false
484
- }
349
+ "type": {
350
+ "type": "string",
351
+ "enum": [
352
+ "setupRequest",
353
+ "setupResponse",
354
+ "subscriptionRequest",
355
+ "subscriptionDelete",
356
+ "subscriptionResponse",
357
+ "indicationMessage",
358
+ "dAppControlAction",
359
+ "dAppReport",
360
+ "xAppControlAction",
361
+ "releaseMessage",
362
+ "messageAck"
485
363
  ]
486
- }
364
+ },
365
+ "id": { "$ref": "#/$defs/E3-MessageID" },
366
+ "timestamp": { "type": "integer", "minimum": 0 }
487
367
  },
488
- "required": [
489
- "id",
490
- "msg"
368
+ "oneOf": [
369
+ {
370
+ "properties": { "type": { "const": "setupRequest" } },
371
+ "allOf": [ { "$ref": "#/$defs/E3-SetupRequest" } ]
372
+ },
373
+ {
374
+ "properties": { "type": { "const": "setupResponse" } },
375
+ "allOf": [ { "$ref": "#/$defs/E3-SetupResponse" } ]
376
+ },
377
+ {
378
+ "properties": { "type": { "const": "subscriptionRequest" } },
379
+ "allOf": [ { "$ref": "#/$defs/E3-SubscriptionRequest" } ]
380
+ },
381
+ {
382
+ "properties": { "type": { "const": "subscriptionDelete" } },
383
+ "allOf": [ { "$ref": "#/$defs/E3-SubscriptionDelete" } ]
384
+ },
385
+ {
386
+ "properties": { "type": { "const": "subscriptionResponse" } },
387
+ "allOf": [ { "$ref": "#/$defs/E3-SubscriptionResponse" } ]
388
+ },
389
+ {
390
+ "properties": { "type": { "const": "indicationMessage" } },
391
+ "allOf": [ { "$ref": "#/$defs/E3-IndicationMessage" } ]
392
+ },
393
+ {
394
+ "properties": { "type": { "const": "dAppControlAction" } },
395
+ "allOf": [ { "$ref": "#/$defs/E3-DAppControlAction" } ]
396
+ },
397
+ {
398
+ "properties": { "type": { "const": "dAppReport" } },
399
+ "allOf": [ { "$ref": "#/$defs/E3-DAppReport" } ]
400
+ },
401
+ {
402
+ "properties": { "type": { "const": "xAppControlAction" } },
403
+ "allOf": [ { "$ref": "#/$defs/E3-XAppControlAction" } ]
404
+ },
405
+ {
406
+ "properties": { "type": { "const": "releaseMessage" } },
407
+ "allOf": [ { "$ref": "#/$defs/E3-ReleaseMessage" } ]
408
+ },
409
+ {
410
+ "properties": { "type": { "const": "messageAck" } },
411
+ "allOf": [ { "$ref": "#/$defs/E3-MessageAck" } ]
412
+ }
491
413
  ]
492
414
  }
493
415
  }