spectre-core 0.0.25__py3-none-any.whl → 0.0.26__py3-none-any.whl
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.
- spectre_core/__init__.py +1 -1
- spectre_core/capture_configs/_capture_config.py +2 -0
- spectre_core/plotting/_panel_stack.py +11 -4
- spectre_core/receivers/__init__.py +11 -8
- spectre_core/receivers/_factory.py +16 -11
- spectre_core/receivers/_receiver.py +246 -0
- spectre_core/receivers/_register.py +3 -3
- spectre_core/receivers/{_spec_names.py → _specs.py} +42 -7
- spectre_core/receivers/plugins/_b200mini.py +219 -34
- spectre_core/receivers/plugins/{gr/_usrp.py → _b200mini_gr.py} +38 -61
- spectre_core/receivers/plugins/_custom.py +20 -0
- spectre_core/receivers/plugins/{gr/_base.py → _gr.py} +1 -1
- spectre_core/receivers/plugins/_receiver_names.py +5 -3
- spectre_core/receivers/plugins/_rsp1a.py +38 -43
- spectre_core/receivers/plugins/_rsp1a_gr.py +112 -0
- spectre_core/receivers/plugins/_rspduo.py +47 -57
- spectre_core/receivers/plugins/_rspduo_gr.py +165 -0
- spectre_core/receivers/plugins/_sdrplay_receiver.py +146 -42
- spectre_core/receivers/plugins/_signal_generator.py +225 -0
- spectre_core/receivers/plugins/_signal_generator_gr.py +77 -0
- spectre_core/spectrograms/_analytical.py +18 -18
- {spectre_core-0.0.25.dist-info → spectre_core-0.0.26.dist-info}/METADATA +1 -1
- {spectre_core-0.0.25.dist-info → spectre_core-0.0.26.dist-info}/RECORD +26 -27
- spectre_core/receivers/_base.py +0 -242
- spectre_core/receivers/plugins/_test.py +0 -225
- spectre_core/receivers/plugins/_usrp.py +0 -213
- spectre_core/receivers/plugins/gr/__init__.py +0 -3
- spectre_core/receivers/plugins/gr/_rsp1a.py +0 -127
- spectre_core/receivers/plugins/gr/_rspduo.py +0 -202
- spectre_core/receivers/plugins/gr/_test.py +0 -117
- {spectre_core-0.0.25.dist-info → spectre_core-0.0.26.dist-info}/WHEEL +0 -0
- {spectre_core-0.0.25.dist-info → spectre_core-0.0.26.dist-info}/licenses/LICENSE +0 -0
- {spectre_core-0.0.25.dist-info → spectre_core-0.0.26.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,11 @@
|
|
2
2
|
# This file is part of SPECTRE
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
4
|
|
5
|
-
from
|
5
|
+
from abc import ABC, abstractmethod
|
6
|
+
from typing import Callable, cast, Optional
|
7
|
+
from logging import getLogger
|
8
|
+
|
9
|
+
_LOGGER = getLogger(__name__)
|
6
10
|
|
7
11
|
from spectre_core.capture_configs import (
|
8
12
|
CaptureTemplate,
|
@@ -10,38 +14,136 @@ from spectre_core.capture_configs import (
|
|
10
14
|
Parameters,
|
11
15
|
Bound,
|
12
16
|
PName,
|
17
|
+
OneOf,
|
13
18
|
get_base_capture_template,
|
14
19
|
get_base_ptemplate,
|
15
|
-
OneOf,
|
16
20
|
validate_fixed_center_frequency,
|
17
21
|
validate_swept_center_frequency,
|
18
22
|
)
|
19
|
-
from .._base import BaseReceiver
|
20
|
-
from .._spec_names import SpecName
|
21
23
|
|
24
|
+
from .._receiver import Receiver, ReceiverName
|
25
|
+
from .._specs import SpecName
|
26
|
+
|
27
|
+
LOW_IF_SAMPLE_RATE_CUTOFF = 2e6
|
28
|
+
LOW_IF_PERMITTED_SAMPLE_RATES = [LOW_IF_SAMPLE_RATE_CUTOFF / (2**i) for i in range(6)]
|
29
|
+
|
30
|
+
|
31
|
+
class SDRplayReceiver(ABC, Receiver):
|
32
|
+
"""An abstract base class for SDRplay devices."""
|
33
|
+
|
34
|
+
def __init__(self, name: ReceiverName, mode: Optional[str]):
|
35
|
+
"""Initialise an instance of an `SDRplayReceiver`."""
|
36
|
+
super().__init__(name, mode)
|
37
|
+
|
38
|
+
self.add_spec(SpecName.SAMPLE_RATE_LOWER_BOUND, 62.5e3)
|
39
|
+
self.add_spec(SpecName.SAMPLE_RATE_UPPER_BOUND, 10.66e6)
|
40
|
+
self.add_spec(SpecName.FREQUENCY_LOWER_BOUND, 1e3)
|
41
|
+
self.add_spec(SpecName.FREQUENCY_UPPER_BOUND, 2e9)
|
42
|
+
self.add_spec(SpecName.IF_GAIN_UPPER_BOUND, -20)
|
43
|
+
self.add_spec(SpecName.IF_GAIN_LOWER_BOUND, -59)
|
44
|
+
self.add_spec(SpecName.API_RETUNING_LATENCY, 25 * 1e-3)
|
45
|
+
|
46
|
+
# bandwidth == 0 means 'AUTO', i.e. the largest bandwidth compatible with the sample rate
|
47
|
+
self.add_spec(
|
48
|
+
SpecName.BANDWIDTH_OPTIONS,
|
49
|
+
[0, 200e3, 300e3, 600e3, 1.536e6, 5e6, 6e6, 7e6, 8e6],
|
50
|
+
)
|
51
|
+
|
52
|
+
@abstractmethod
|
53
|
+
def get_rf_gains(self, center_frequency: float) -> list[int]:
|
54
|
+
"""Get an ordered list of RF gain values corresponding to each LNA state at the specified center frequency.
|
55
|
+
|
56
|
+
The values are taken from the gain reduction tables documented in the SDRplay API specification, and are
|
57
|
+
unique to each model. Note that negative gain values represent positive gain reduction.
|
58
|
+
"""
|
59
|
+
|
60
|
+
|
61
|
+
def _validate_rf_gain(rf_gain: int, expected_rf_gains: list[int]):
|
62
|
+
"""Validate the RF gain value against the expected values for the current LNA state.
|
63
|
+
|
64
|
+
The RF gain is determined by the LNA state and can only take specific values as documented in the
|
65
|
+
gain reduction tables of the SDRplay API specification.
|
66
|
+
|
67
|
+
For implementation details, refer to the `gr-sdrplay3` OOT module:
|
68
|
+
https://github.com/fventuri/gr-sdrplay3/blob/v3.11.0.9/lib/rsp_impl.cc#L378-L387
|
69
|
+
"""
|
70
|
+
if rf_gain not in expected_rf_gains:
|
71
|
+
raise ValueError(
|
72
|
+
f"The value of RF gain must be one of {expected_rf_gains}. "
|
73
|
+
f"Got {rf_gain}."
|
74
|
+
)
|
75
|
+
|
76
|
+
|
77
|
+
def _validate_low_if_sample_rate(sample_rate: int) -> None:
|
78
|
+
"""Validate the sample rate if the receiver is operating in low IF mode.
|
79
|
+
|
80
|
+
The minimum physical sampling rate of the SDRplay hardware is 2 MHz. Lower effective rates can be achieved
|
81
|
+
through decimation, as handled by the `gr-sdrplay3` OOT module. This function ensures that the sample rate
|
82
|
+
is not silently adjusted by the backend.
|
83
|
+
|
84
|
+
For implementation details, refer to:
|
85
|
+
https://github.com/fventuri/gr-sdrplay3/blob/v3.11.0.9/lib/rsp_impl.cc#L140-L179
|
86
|
+
"""
|
87
|
+
if sample_rate <= LOW_IF_SAMPLE_RATE_CUTOFF:
|
88
|
+
if sample_rate not in LOW_IF_PERMITTED_SAMPLE_RATES:
|
89
|
+
raise ValueError(
|
90
|
+
f"If the requested sample rate is less than or equal to {LOW_IF_SAMPLE_RATE_CUTOFF}, "
|
91
|
+
f"the receiver will be operating in low IF mode. "
|
92
|
+
f"So, the sample rate must be exactly one of {LOW_IF_PERMITTED_SAMPLE_RATES}. "
|
93
|
+
f"Got sample rate {sample_rate} Hz"
|
94
|
+
)
|
22
95
|
|
23
|
-
|
24
|
-
|
96
|
+
|
97
|
+
def make_pvalidator_fixed_center_frequency(
|
98
|
+
receiver: SDRplayReceiver,
|
25
99
|
) -> Callable[[Parameters], None]:
|
26
100
|
def pvalidator(parameters: Parameters) -> None:
|
27
101
|
validate_fixed_center_frequency(parameters)
|
28
102
|
|
103
|
+
# Validate the sample rate, in the case the receiver will be operating in low if mode.
|
104
|
+
sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
|
105
|
+
_validate_low_if_sample_rate(sample_rate)
|
106
|
+
|
107
|
+
# Validate the rf gain value, which is a function of center frequency.
|
108
|
+
rf_gain = cast(int, parameters.get_parameter_value(PName.RF_GAIN))
|
109
|
+
center_frequency = cast(
|
110
|
+
float, parameters.get_parameter_value(PName.CENTER_FREQUENCY)
|
111
|
+
)
|
112
|
+
_validate_rf_gain(rf_gain, receiver.get_rf_gains(center_frequency))
|
113
|
+
|
29
114
|
return pvalidator
|
30
115
|
|
31
116
|
|
32
|
-
def
|
33
|
-
|
117
|
+
def make_pvalidator_swept_center_frequency(
|
118
|
+
receiver: SDRplayReceiver,
|
34
119
|
) -> Callable[[Parameters], None]:
|
35
120
|
def pvalidator(parameters: Parameters) -> None:
|
36
121
|
validate_swept_center_frequency(
|
37
|
-
parameters,
|
122
|
+
parameters, receiver.get_spec(SpecName.API_RETUNING_LATENCY)
|
38
123
|
)
|
39
124
|
|
125
|
+
# Validate the sample rate, in the case the receiver will be operating in low if mode.
|
126
|
+
sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
|
127
|
+
_validate_low_if_sample_rate(sample_rate)
|
128
|
+
|
129
|
+
# Validate the rf gain value, which is a function of center frequency.
|
130
|
+
rf_gain = cast(int, parameters.get_parameter_value(PName.RF_GAIN))
|
131
|
+
min_frequency = cast(float, parameters.get_parameter_value(PName.MIN_FREQUENCY))
|
132
|
+
max_frequency = cast(float, parameters.get_parameter_value(PName.MAX_FREQUENCY))
|
133
|
+
_validate_rf_gain(rf_gain, receiver.get_rf_gains(min_frequency))
|
134
|
+
_validate_rf_gain(rf_gain, receiver.get_rf_gains(max_frequency))
|
135
|
+
|
136
|
+
# Check that we are not cross a threshold where the LNA state has to change
|
137
|
+
if receiver.get_rf_gains(min_frequency) != receiver.get_rf_gains(max_frequency):
|
138
|
+
_LOGGER.warning(
|
139
|
+
"Crossing a threshold where the LNA state has to change. Performance may be reduced."
|
140
|
+
)
|
141
|
+
|
40
142
|
return pvalidator
|
41
143
|
|
42
144
|
|
43
|
-
def
|
44
|
-
|
145
|
+
def make_capture_template_fixed_center_frequency(
|
146
|
+
receiver: SDRplayReceiver,
|
45
147
|
) -> CaptureTemplate:
|
46
148
|
|
47
149
|
capture_template = get_base_capture_template(CaptureMode.FIXED_CENTER_FREQUENCY)
|
@@ -52,12 +154,12 @@ def get_capture_template_fixed_center_frequency(
|
|
52
154
|
capture_template.set_defaults(
|
53
155
|
(PName.BATCH_SIZE, 3.0),
|
54
156
|
(PName.CENTER_FREQUENCY, 95800000),
|
55
|
-
(PName.SAMPLE_RATE,
|
56
|
-
(PName.BANDWIDTH,
|
157
|
+
(PName.SAMPLE_RATE, 500000),
|
158
|
+
(PName.BANDWIDTH, 300000),
|
57
159
|
(PName.WINDOW_HOP, 512),
|
58
160
|
(PName.WINDOW_SIZE, 1024),
|
59
161
|
(PName.WINDOW_TYPE, "blackman"),
|
60
|
-
(PName.RF_GAIN,
|
162
|
+
(PName.RF_GAIN, 0),
|
61
163
|
(PName.IF_GAIN, -30),
|
62
164
|
)
|
63
165
|
|
@@ -65,8 +167,8 @@ def get_capture_template_fixed_center_frequency(
|
|
65
167
|
PName.CENTER_FREQUENCY,
|
66
168
|
[
|
67
169
|
Bound(
|
68
|
-
lower_bound=
|
69
|
-
upper_bound=
|
170
|
+
lower_bound=receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
|
171
|
+
upper_bound=receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
|
70
172
|
)
|
71
173
|
],
|
72
174
|
)
|
@@ -74,27 +176,28 @@ def get_capture_template_fixed_center_frequency(
|
|
74
176
|
PName.SAMPLE_RATE,
|
75
177
|
[
|
76
178
|
Bound(
|
77
|
-
lower_bound=
|
78
|
-
upper_bound=
|
179
|
+
lower_bound=receiver.get_spec(SpecName.SAMPLE_RATE_LOWER_BOUND),
|
180
|
+
upper_bound=receiver.get_spec(SpecName.SAMPLE_RATE_UPPER_BOUND),
|
79
181
|
)
|
80
182
|
],
|
81
183
|
)
|
82
184
|
capture_template.add_pconstraint(
|
83
|
-
PName.BANDWIDTH, [OneOf(
|
185
|
+
PName.BANDWIDTH, [OneOf(receiver.get_spec(SpecName.BANDWIDTH_OPTIONS))]
|
84
186
|
)
|
85
187
|
capture_template.add_pconstraint(
|
86
188
|
PName.IF_GAIN,
|
87
|
-
[
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
189
|
+
[
|
190
|
+
Bound(
|
191
|
+
lower_bound=receiver.get_spec(SpecName.IF_GAIN_LOWER_BOUND),
|
192
|
+
upper_bound=receiver.get_spec(SpecName.IF_GAIN_UPPER_BOUND),
|
193
|
+
)
|
194
|
+
],
|
92
195
|
)
|
93
196
|
return capture_template
|
94
197
|
|
95
198
|
|
96
|
-
def
|
97
|
-
|
199
|
+
def make_capture_template_swept_center_frequency(
|
200
|
+
receiver: Receiver,
|
98
201
|
) -> CaptureTemplate:
|
99
202
|
|
100
203
|
capture_template = get_base_capture_template(CaptureMode.SWEPT_CENTER_FREQUENCY)
|
@@ -103,17 +206,17 @@ def get_capture_template_swept_center_frequency(
|
|
103
206
|
capture_template.add_ptemplate(get_base_ptemplate(PName.RF_GAIN))
|
104
207
|
|
105
208
|
capture_template.set_defaults(
|
106
|
-
(PName.BATCH_SIZE,
|
209
|
+
(PName.BATCH_SIZE, 3.0),
|
107
210
|
(PName.MIN_FREQUENCY, 95000000),
|
108
211
|
(PName.MAX_FREQUENCY, 100000000),
|
109
212
|
(PName.SAMPLES_PER_STEP, 80000),
|
110
|
-
(PName.FREQUENCY_STEP,
|
111
|
-
(PName.SAMPLE_RATE,
|
213
|
+
(PName.FREQUENCY_STEP, 2000000),
|
214
|
+
(PName.SAMPLE_RATE, 2e6),
|
112
215
|
(PName.BANDWIDTH, 1536000),
|
113
216
|
(PName.WINDOW_HOP, 512),
|
114
217
|
(PName.WINDOW_SIZE, 1024),
|
115
218
|
(PName.WINDOW_TYPE, "blackman"),
|
116
|
-
(PName.RF_GAIN,
|
219
|
+
(PName.RF_GAIN, 0),
|
117
220
|
(PName.IF_GAIN, -30),
|
118
221
|
)
|
119
222
|
|
@@ -121,8 +224,8 @@ def get_capture_template_swept_center_frequency(
|
|
121
224
|
PName.MIN_FREQUENCY,
|
122
225
|
[
|
123
226
|
Bound(
|
124
|
-
lower_bound=
|
125
|
-
upper_bound=
|
227
|
+
lower_bound=receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
|
228
|
+
upper_bound=receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
|
126
229
|
)
|
127
230
|
],
|
128
231
|
)
|
@@ -130,8 +233,8 @@ def get_capture_template_swept_center_frequency(
|
|
130
233
|
PName.MAX_FREQUENCY,
|
131
234
|
[
|
132
235
|
Bound(
|
133
|
-
lower_bound=
|
134
|
-
upper_bound=
|
236
|
+
lower_bound=receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
|
237
|
+
upper_bound=receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
|
135
238
|
)
|
136
239
|
],
|
137
240
|
)
|
@@ -139,20 +242,21 @@ def get_capture_template_swept_center_frequency(
|
|
139
242
|
PName.SAMPLE_RATE,
|
140
243
|
[
|
141
244
|
Bound(
|
142
|
-
lower_bound=
|
143
|
-
upper_bound=
|
245
|
+
lower_bound=receiver.get_spec(SpecName.SAMPLE_RATE_LOWER_BOUND),
|
246
|
+
upper_bound=receiver.get_spec(SpecName.SAMPLE_RATE_UPPER_BOUND),
|
144
247
|
)
|
145
248
|
],
|
146
249
|
)
|
147
250
|
capture_template.add_pconstraint(
|
148
|
-
PName.BANDWIDTH, [OneOf(
|
251
|
+
PName.BANDWIDTH, [OneOf(receiver.get_spec(SpecName.BANDWIDTH_OPTIONS))]
|
149
252
|
)
|
150
253
|
capture_template.add_pconstraint(
|
151
254
|
PName.IF_GAIN,
|
152
|
-
[
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
255
|
+
[
|
256
|
+
Bound(
|
257
|
+
lower_bound=receiver.get_spec(SpecName.IF_GAIN_LOWER_BOUND),
|
258
|
+
upper_bound=receiver.get_spec(SpecName.IF_GAIN_UPPER_BOUND),
|
259
|
+
)
|
260
|
+
],
|
157
261
|
)
|
158
262
|
return capture_template
|
@@ -0,0 +1,225 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from typing import Callable, Optional, cast
|
7
|
+
from functools import partial
|
8
|
+
|
9
|
+
from spectre_core.capture_configs import (
|
10
|
+
CaptureTemplate,
|
11
|
+
CaptureMode,
|
12
|
+
Parameters,
|
13
|
+
Bound,
|
14
|
+
PName,
|
15
|
+
get_base_capture_template,
|
16
|
+
make_base_capture_template,
|
17
|
+
get_base_ptemplate,
|
18
|
+
validate_window,
|
19
|
+
)
|
20
|
+
from ._signal_generator_gr import cosine_wave, constant_staircase
|
21
|
+
from ._gr import capture
|
22
|
+
from ._receiver_names import ReceiverName
|
23
|
+
from .._receiver import Receiver
|
24
|
+
from .._specs import SpecName
|
25
|
+
from .._register import register_receiver
|
26
|
+
|
27
|
+
|
28
|
+
def _make_capture_template_cos_wave(receiver: Receiver) -> CaptureTemplate:
|
29
|
+
capture_template = get_base_capture_template(CaptureMode.FIXED_CENTER_FREQUENCY)
|
30
|
+
capture_template.add_ptemplate(get_base_ptemplate(PName.AMPLITUDE))
|
31
|
+
capture_template.add_ptemplate(get_base_ptemplate(PName.FREQUENCY))
|
32
|
+
|
33
|
+
capture_template.set_defaults(
|
34
|
+
(PName.BATCH_SIZE, 3.0),
|
35
|
+
(PName.CENTER_FREQUENCY, 16000),
|
36
|
+
(PName.AMPLITUDE, 2.0),
|
37
|
+
(PName.FREQUENCY, 32000),
|
38
|
+
(PName.SAMPLE_RATE, 128000),
|
39
|
+
(PName.WINDOW_HOP, 512),
|
40
|
+
(PName.WINDOW_SIZE, 512),
|
41
|
+
(PName.WINDOW_TYPE, "boxcar"),
|
42
|
+
)
|
43
|
+
|
44
|
+
capture_template.enforce_defaults(
|
45
|
+
PName.TIME_RESOLUTION,
|
46
|
+
PName.TIME_RANGE,
|
47
|
+
PName.FREQUENCY_RESOLUTION,
|
48
|
+
PName.WINDOW_TYPE,
|
49
|
+
)
|
50
|
+
|
51
|
+
capture_template.add_pconstraint(
|
52
|
+
PName.SAMPLE_RATE,
|
53
|
+
[
|
54
|
+
Bound(
|
55
|
+
lower_bound=receiver.get_spec(SpecName.SAMPLE_RATE_LOWER_BOUND),
|
56
|
+
upper_bound=receiver.get_spec(SpecName.SAMPLE_RATE_UPPER_BOUND),
|
57
|
+
)
|
58
|
+
],
|
59
|
+
)
|
60
|
+
capture_template.add_pconstraint(
|
61
|
+
PName.FREQUENCY,
|
62
|
+
[
|
63
|
+
Bound(
|
64
|
+
lower_bound=receiver.get_spec(SpecName.FREQUENCY_LOWER_BOUND),
|
65
|
+
upper_bound=receiver.get_spec(SpecName.FREQUENCY_UPPER_BOUND),
|
66
|
+
)
|
67
|
+
],
|
68
|
+
)
|
69
|
+
return capture_template
|
70
|
+
|
71
|
+
|
72
|
+
def _make_capture_template_constant_staircase(receiver: Receiver) -> CaptureTemplate:
|
73
|
+
capture_template = make_base_capture_template(
|
74
|
+
PName.TIME_RESOLUTION,
|
75
|
+
PName.FREQUENCY_RESOLUTION,
|
76
|
+
PName.TIME_RANGE,
|
77
|
+
PName.SAMPLE_RATE,
|
78
|
+
PName.BATCH_SIZE,
|
79
|
+
PName.WINDOW_TYPE,
|
80
|
+
PName.WINDOW_HOP,
|
81
|
+
PName.WINDOW_SIZE,
|
82
|
+
PName.EVENT_HANDLER_KEY,
|
83
|
+
PName.BATCH_KEY,
|
84
|
+
PName.WATCH_EXTENSION,
|
85
|
+
PName.MIN_SAMPLES_PER_STEP,
|
86
|
+
PName.MAX_SAMPLES_PER_STEP,
|
87
|
+
PName.FREQUENCY_STEP,
|
88
|
+
PName.STEP_INCREMENT,
|
89
|
+
PName.OBS_ALT,
|
90
|
+
PName.OBS_LAT,
|
91
|
+
PName.OBS_LON,
|
92
|
+
PName.OBJECT,
|
93
|
+
PName.ORIGIN,
|
94
|
+
PName.TELESCOPE,
|
95
|
+
PName.INSTRUMENT,
|
96
|
+
)
|
97
|
+
|
98
|
+
capture_template.set_defaults(
|
99
|
+
(PName.BATCH_SIZE, 3.0),
|
100
|
+
(PName.FREQUENCY_STEP, 128000),
|
101
|
+
(PName.MAX_SAMPLES_PER_STEP, 5000),
|
102
|
+
(PName.MIN_SAMPLES_PER_STEP, 4000),
|
103
|
+
(PName.SAMPLE_RATE, 128000),
|
104
|
+
(PName.STEP_INCREMENT, 200),
|
105
|
+
(PName.WINDOW_HOP, 512),
|
106
|
+
(PName.WINDOW_SIZE, 512),
|
107
|
+
(PName.WINDOW_TYPE, "boxcar"),
|
108
|
+
(PName.EVENT_HANDLER_KEY, "swept_center_frequency"),
|
109
|
+
(PName.BATCH_KEY, "iq_stream"),
|
110
|
+
(PName.WATCH_EXTENSION, "bin"),
|
111
|
+
)
|
112
|
+
|
113
|
+
capture_template.enforce_defaults(
|
114
|
+
PName.TIME_RESOLUTION,
|
115
|
+
PName.TIME_RANGE,
|
116
|
+
PName.FREQUENCY_RESOLUTION,
|
117
|
+
PName.WINDOW_TYPE,
|
118
|
+
PName.EVENT_HANDLER_KEY,
|
119
|
+
PName.BATCH_KEY,
|
120
|
+
PName.WATCH_EXTENSION,
|
121
|
+
)
|
122
|
+
|
123
|
+
return capture_template
|
124
|
+
|
125
|
+
|
126
|
+
def _make_pvalidator_cosine_wave(receiver: Receiver) -> Callable[[Parameters], None]:
|
127
|
+
def pvalidator(parameters: Parameters) -> None:
|
128
|
+
validate_window(parameters)
|
129
|
+
|
130
|
+
sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
|
131
|
+
window_size = cast(int, parameters.get_parameter_value(PName.WINDOW_SIZE))
|
132
|
+
frequency = cast(float, parameters.get_parameter_value(PName.FREQUENCY))
|
133
|
+
|
134
|
+
# check that the sample rate is an integer multiple of the underlying signal frequency
|
135
|
+
if sample_rate % frequency != 0:
|
136
|
+
raise ValueError(
|
137
|
+
"The sampling rate must be some integer multiple of frequency"
|
138
|
+
)
|
139
|
+
|
140
|
+
a = sample_rate / frequency
|
141
|
+
if a < 2:
|
142
|
+
raise ValueError(
|
143
|
+
(
|
144
|
+
f"The ratio of sampling rate over frequency must be greater than two. "
|
145
|
+
f"Got {a}"
|
146
|
+
)
|
147
|
+
)
|
148
|
+
|
149
|
+
# analytical requirement
|
150
|
+
# if p is the number of sampled cycles, we can find that p = window_size / a
|
151
|
+
# the number of sampled cycles must be a positive natural number.
|
152
|
+
p = window_size / a
|
153
|
+
if window_size % a != 0:
|
154
|
+
raise ValueError(
|
155
|
+
(
|
156
|
+
f"The number of sampled cycles must be a positive natural number. "
|
157
|
+
f"Computed that p={p}"
|
158
|
+
)
|
159
|
+
)
|
160
|
+
|
161
|
+
return pvalidator
|
162
|
+
|
163
|
+
|
164
|
+
def _make_pvalidator_constant_staircase(
|
165
|
+
receiver: Receiver,
|
166
|
+
) -> Callable[[Parameters], None]:
|
167
|
+
def pvalidator(parameters: Parameters) -> None:
|
168
|
+
validate_window(parameters)
|
169
|
+
|
170
|
+
freq_step = cast(float, parameters.get_parameter_value(PName.FREQUENCY_STEP))
|
171
|
+
sample_rate = cast(int, parameters.get_parameter_value(PName.SAMPLE_RATE))
|
172
|
+
min_samples_per_step = cast(
|
173
|
+
int, parameters.get_parameter_value(PName.MIN_SAMPLES_PER_STEP)
|
174
|
+
)
|
175
|
+
max_samples_per_step = cast(
|
176
|
+
int, parameters.get_parameter_value(PName.MAX_SAMPLES_PER_STEP)
|
177
|
+
)
|
178
|
+
|
179
|
+
if freq_step != sample_rate:
|
180
|
+
raise ValueError(f"The frequency step must be equal to the sampling rate")
|
181
|
+
|
182
|
+
if min_samples_per_step > max_samples_per_step:
|
183
|
+
raise ValueError(
|
184
|
+
(
|
185
|
+
f"Minimum samples per step cannot be greater than the maximum samples per step. "
|
186
|
+
f"Got {min_samples_per_step}, which is greater than {max_samples_per_step}"
|
187
|
+
)
|
188
|
+
)
|
189
|
+
|
190
|
+
return pvalidator
|
191
|
+
|
192
|
+
|
193
|
+
@dataclass(frozen=True)
|
194
|
+
class _Mode:
|
195
|
+
"""An operating mode for the `SignalGenerator` receiver."""
|
196
|
+
|
197
|
+
COSINE_WAVE = "cosine_wave"
|
198
|
+
CONSTANT_STAIRCASE = "constant_staircase"
|
199
|
+
|
200
|
+
|
201
|
+
@register_receiver(ReceiverName.SIGNAL_GENERATOR)
|
202
|
+
class SignalGenerator(Receiver):
|
203
|
+
"""An entirely software-defined receiver, which generates synthetic signals."""
|
204
|
+
|
205
|
+
def __init__(self, name: ReceiverName, mode: Optional[str] = None) -> None:
|
206
|
+
super().__init__(name, mode)
|
207
|
+
|
208
|
+
self.add_spec(SpecName.SAMPLE_RATE_LOWER_BOUND, 64000)
|
209
|
+
self.add_spec(SpecName.SAMPLE_RATE_UPPER_BOUND, 640000)
|
210
|
+
self.add_spec(SpecName.FREQUENCY_LOWER_BOUND, 16000)
|
211
|
+
self.add_spec(SpecName.FREQUENCY_UPPER_BOUND, 160000)
|
212
|
+
|
213
|
+
self.add_mode(
|
214
|
+
_Mode.COSINE_WAVE,
|
215
|
+
partial(capture, top_block_cls=cosine_wave),
|
216
|
+
_make_capture_template_cos_wave(self),
|
217
|
+
_make_pvalidator_cosine_wave(self),
|
218
|
+
)
|
219
|
+
|
220
|
+
self.add_mode(
|
221
|
+
_Mode.CONSTANT_STAIRCASE,
|
222
|
+
partial(capture, top_block_cls=constant_staircase),
|
223
|
+
_make_capture_template_constant_staircase(self),
|
224
|
+
_make_pvalidator_constant_staircase(self),
|
225
|
+
)
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024-2025 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from gnuradio import gr
|
6
|
+
from gnuradio import blocks
|
7
|
+
from gnuradio import analog
|
8
|
+
from gnuradio import spectre
|
9
|
+
|
10
|
+
from spectre_core.capture_configs import Parameters, PName
|
11
|
+
from spectre_core.config import get_batches_dir_path
|
12
|
+
from ._gr import spectre_top_block
|
13
|
+
|
14
|
+
|
15
|
+
class cosine_wave(spectre_top_block):
|
16
|
+
def flowgraph(self, tag: str, parameters: Parameters) -> None:
|
17
|
+
|
18
|
+
# Unpack the capture config parameters
|
19
|
+
samp_rate = parameters.get_parameter_value(PName.SAMPLE_RATE)
|
20
|
+
batch_size = parameters.get_parameter_value(PName.BATCH_SIZE)
|
21
|
+
frequency = parameters.get_parameter_value(PName.FREQUENCY)
|
22
|
+
amplitude = parameters.get_parameter_value(PName.AMPLITUDE)
|
23
|
+
|
24
|
+
# Blocks
|
25
|
+
self.spectre_batched_file_sink = spectre.batched_file_sink(
|
26
|
+
get_batches_dir_path(), tag, batch_size, samp_rate
|
27
|
+
)
|
28
|
+
self.blocks_throttle_0 = blocks.throttle(gr.sizeof_float * 1, samp_rate, True)
|
29
|
+
self.blocks_throttle_1 = blocks.throttle(gr.sizeof_float * 1, samp_rate, True)
|
30
|
+
self.blocks_null_source = blocks.null_source(gr.sizeof_float * 1)
|
31
|
+
self.blocks_float_to_complex = blocks.float_to_complex(1)
|
32
|
+
self.analog_sig_source = analog.sig_source_f(
|
33
|
+
samp_rate, analog.GR_COS_WAVE, frequency, amplitude, 0, 0
|
34
|
+
)
|
35
|
+
|
36
|
+
# Connections
|
37
|
+
self.connect((self.analog_sig_source, 0), (self.blocks_throttle_0, 0))
|
38
|
+
self.connect((self.blocks_null_source, 0), (self.blocks_throttle_1, 0))
|
39
|
+
self.connect((self.blocks_throttle_0, 0), (self.blocks_float_to_complex, 0))
|
40
|
+
self.connect((self.blocks_throttle_1, 0), (self.blocks_float_to_complex, 1))
|
41
|
+
self.connect(
|
42
|
+
(self.blocks_float_to_complex, 0), (self.spectre_batched_file_sink, 0)
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
class constant_staircase(spectre_top_block):
|
47
|
+
def flowgraph(self, tag: str, parameters: Parameters) -> None:
|
48
|
+
# Unpack the capture config parameters
|
49
|
+
step_increment = parameters.get_parameter_value(PName.STEP_INCREMENT)
|
50
|
+
samp_rate = parameters.get_parameter_value(PName.SAMPLE_RATE)
|
51
|
+
min_samples_per_step = parameters.get_parameter_value(
|
52
|
+
PName.MIN_SAMPLES_PER_STEP
|
53
|
+
)
|
54
|
+
max_samples_per_step = parameters.get_parameter_value(
|
55
|
+
PName.MAX_SAMPLES_PER_STEP
|
56
|
+
)
|
57
|
+
frequency_step = parameters.get_parameter_value(PName.FREQUENCY_STEP)
|
58
|
+
batch_size = parameters.get_parameter_value(PName.BATCH_SIZE)
|
59
|
+
|
60
|
+
# Blocks
|
61
|
+
self.spectre_constant_staircase = spectre.tagged_staircase(
|
62
|
+
min_samples_per_step,
|
63
|
+
max_samples_per_step,
|
64
|
+
frequency_step,
|
65
|
+
step_increment,
|
66
|
+
samp_rate,
|
67
|
+
)
|
68
|
+
self.spectre_batched_file_sink = spectre.batched_file_sink(
|
69
|
+
get_batches_dir_path(), tag, batch_size, samp_rate, True, "rx_freq", 0
|
70
|
+
) # zero means the center frequency is unset
|
71
|
+
self.blocks_throttle = blocks.throttle(
|
72
|
+
gr.sizeof_gr_complex * 1, samp_rate, True
|
73
|
+
)
|
74
|
+
|
75
|
+
# Connections
|
76
|
+
self.connect((self.spectre_constant_staircase, 0), (self.blocks_throttle, 0))
|
77
|
+
self.connect((self.blocks_throttle, 0), (self.spectre_batched_file_sink, 0))
|