dapps 0.2.1__tar.gz → 0.2.2__tar.gz

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