Skip to content

Commit 58b823f

Browse files
committed
added new mode
1 parent 0524ab7 commit 58b823f

2 files changed

Lines changed: 368 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
def stop_all() -> None:
2+
...
3+
4+
def stop_sdr(serialno: str) -> None:
5+
...
6+
7+
def pybladerf_scan(frequencies: list[int], samples_per_scan: int, queue: object, sample_rate: int = 61_000_000, baseband_filter_bandwidth: int | None = None,
8+
gain: int = 20, channel: int = 0, oversample: bool = False, antenna_enable: bool = False, serial_number: str | None = None,
9+
print_to_console: bool = True) -> None:
10+
...
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
# MIT License
2+
3+
# Copyright (c) 2024-2025 GvozdevLeonid
4+
5+
# Permission is hereby granted, free of charge, to any person obtaining a copy
6+
# of this software and associated documentation files (the "Software"), to deal
7+
# in the Software without restriction, including without limitation the rights
8+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
# copies of the Software, and to permit persons to whom the Software is
10+
# furnished to do so, subject to the following conditions:
11+
12+
# The above copyright notice and this permission notice shall be included in all
13+
# copies or substantial portions of the Software.
14+
15+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
# SOFTWARE.
22+
23+
# distutils: language = c++
24+
# cython: language_level = 3str
25+
# cython: freethreading_compatible = True
26+
from python_bladerf.pylibbladerf cimport pybladerf as c_pybladerf
27+
from libc.stdint cimport uint64_t, uint32_t, uint16_t, uint8_t
28+
from python_bladerf.pylibbladerf cimport cbladerf
29+
from python_bladerf import pybladerf
30+
from libcpp.atomic cimport atomic
31+
cimport numpy as cnp
32+
import numpy as np
33+
import signal
34+
import time
35+
import sys
36+
import os
37+
38+
cnp.import_array()
39+
40+
FREQ_MIN_MHZ = 70 # 70 MHz
41+
FREQ_MAX_MHZ = 6_000 # 6000 MHZ
42+
FREQ_MIN_HZ = int(FREQ_MIN_MHZ * 1e6) # Hz
43+
FREQ_MAX_HZ = int(FREQ_MAX_MHZ * 1e6) # Hz
44+
45+
MIN_SAMPLE_RATE = 520_834
46+
MAX_SAMPLE_RATE = 61_440_000
47+
48+
MIN_BASEBAND_FILTER_BANDWIDTHS = 200_000 # MHz
49+
MAX_BASEBAND_FILTER_BANDWIDTHS = 56_000_000 # MHz
50+
51+
cdef atomic[uint8_t] working_sdrs[16]
52+
cdef dict sdr_ids = {}
53+
54+
cdef struct ScanStep:
55+
uint64_t frequency
56+
uint64_t schedule_time
57+
58+
def sigint_callback_handler(sig, frame, sdr_id):
59+
global working_sdrs
60+
working_sdrs[sdr_id].store(0)
61+
62+
63+
def init_signals() -> int:
64+
global working_sdrs
65+
66+
sdr_id = -1
67+
for i in range(16):
68+
if working_sdrs[i].load() == 0:
69+
sdr_id = i
70+
break
71+
72+
if sdr_id >= 0:
73+
try:
74+
signal.signal(signal.SIGINT, lambda sig, frame: sigint_callback_handler(sig, frame, sdr_id))
75+
signal.signal(signal.SIGILL, lambda sig, frame: sigint_callback_handler(sig, frame, sdr_id))
76+
signal.signal(signal.SIGTERM, lambda sig, frame: sigint_callback_handler(sig, frame, sdr_id))
77+
signal.signal(signal.SIGABRT, lambda sig, frame: sigint_callback_handler(sig, frame, sdr_id))
78+
except Exception as ex:
79+
sys.stderr.write(f'Error: {ex}\n')
80+
81+
return sdr_id
82+
83+
84+
def stop_all() -> None:
85+
global working_sdrs
86+
for i in range(16):
87+
working_sdrs[i].store(0)
88+
89+
90+
def stop_sdr(serialno: str) -> None:
91+
global sdr_ids, working_sdrs
92+
if serialno in sdr_ids:
93+
working_sdrs[sdr_ids[serialno]].store(0)
94+
95+
96+
def pybladerf_scan(frequencies: list[int], samples_per_scan: int, queue: object, sample_rate: int = 61_000_000, baseband_filter_bandwidth: int | None = None,
97+
gain: int = 20, channel: int = 0, oversample: bool = False, antenna_enable: bool = False, serial_number: str | None = None,
98+
print_to_console: bool = True,
99+
) -> None:
100+
101+
global working_sdrs, sdr_ids
102+
103+
cdef uint8_t device_id = init_signals()
104+
cdef uint8_t formated_channel = pybladerf.PYBLADERF_CHANNEL_RX(channel)
105+
cdef c_pybladerf.PyBladerfDevice device
106+
cdef int i
107+
108+
if serial_number is None:
109+
device = pybladerf.pybladerf_open()
110+
else:
111+
device = pybladerf.pybladerf_open_by_serial(serial_number)
112+
113+
working_sdrs[device_id].store(1)
114+
sdr_ids[device.serialno] = device_id
115+
116+
device.pybladerf_enable_feature(pybladerf.pybladerf_feature.PYBLADERF_FEATURE_OVERSAMPLE, False)
117+
118+
if oversample:
119+
sample_rate = int(sample_rate) if MIN_SAMPLE_RATE * 2 <= int(sample_rate) <= MAX_SAMPLE_RATE * 2 else 122_000_000
120+
else:
121+
sample_rate = int(sample_rate) if MIN_SAMPLE_RATE <= int(sample_rate) <= MAX_SAMPLE_RATE else 61_000_000
122+
cdef uint64_t offset = int(sample_rate // 2)
123+
124+
real_min_freq_hz = FREQ_MIN_HZ - sample_rate // 2
125+
real_max_freq_hz = FREQ_MAX_HZ + sample_rate // 2
126+
127+
if baseband_filter_bandwidth is None:
128+
baseband_filter_bandwidth = int(sample_rate * .75)
129+
baseband_filter_bandwidth = int(baseband_filter_bandwidth) if MIN_BASEBAND_FILTER_BANDWIDTHS <= int(baseband_filter_bandwidth) <= MAX_BASEBAND_FILTER_BANDWIDTHS else int(sample_rate * .75)
130+
131+
if frequencies is None:
132+
frequencies = [int(FREQ_MIN_MHZ - sample_rate // 2e6), int(FREQ_MAX_MHZ + sample_rate // 2e6)]
133+
134+
if print_to_console:
135+
sys.stderr.write(f'call pybladerf_set_tuning_mode({pybladerf.pybladerf_tuning_mode.PYBLADERF_TUNING_MODE_FPGA})\n')
136+
device.pybladerf_set_tuning_mode(pybladerf.pybladerf_tuning_mode.PYBLADERF_TUNING_MODE_FPGA)
137+
138+
if oversample:
139+
if print_to_console:
140+
sys.stderr.write(f'call pybladerf_enable_feature({pybladerf.pybladerf_feature.PYBLADERF_FEATURE_OVERSAMPLE}, True)\n')
141+
device.pybladerf_enable_feature(pybladerf.pybladerf_feature.PYBLADERF_FEATURE_OVERSAMPLE, True)
142+
143+
if print_to_console:
144+
sys.stderr.write(f'call pybladerf_set_sample_rate({sample_rate / 1e6 :.3f} MHz)\n')
145+
device.pybladerf_set_sample_rate(formated_channel, sample_rate)
146+
147+
if not oversample:
148+
if print_to_console:
149+
sys.stderr.write(f'call pybladerf_set_bandwidth({formated_channel}, {baseband_filter_bandwidth / 1e6 :.3f} MHz)\n')
150+
device.pybladerf_set_bandwidth(formated_channel, baseband_filter_bandwidth)
151+
152+
if print_to_console:
153+
sys.stderr.write(f'call pybladerf_set_gain_mode({formated_channel}, {pybladerf.pybladerf_gain_mode.PYBLADERF_GAIN_MGC})\n')
154+
device.pybladerf_set_gain_mode(formated_channel, pybladerf.pybladerf_gain_mode.PYBLADERF_GAIN_MGC)
155+
device.pybladerf_set_gain(formated_channel, gain)
156+
157+
if antenna_enable:
158+
if print_to_console:
159+
sys.stderr.write(f'call pybladerf_set_bias_tee({formated_channel}, True)\n')
160+
device.pybladerf_set_bias_tee(formated_channel, True)
161+
162+
num_ranges = len(frequencies) // 2
163+
calculated_frequencies = []
164+
165+
for i in range(num_ranges):
166+
frequencies[2 * i] = int(frequencies[2 * i] * 1e6)
167+
frequencies[2 * i + 1] = int(frequencies[2 * i + 1] * 1e6)
168+
169+
if frequencies[2 * i] >= frequencies[2 * i + 1]:
170+
device.pybladerf_close()
171+
raise RuntimeError('max frequency must be greater than min frequency.')
172+
173+
step_count = 1 + (frequencies[2 * i + 1] - frequencies[2 * i] - 1) // sample_rate
174+
frequencies[2 * i + 1] = int(frequencies[2 * i] + step_count * sample_rate)
175+
176+
if frequencies[2 * i] < real_min_freq_hz:
177+
device.pybladerf_close()
178+
raise RuntimeError(f'min frequency must must be greater than {int(real_min_freq_hz / 1e6)} MHz.')
179+
if frequencies[2 * i + 1] > real_max_freq_hz:
180+
device.pybladerf_close()
181+
raise RuntimeError(f'max frequency may not be higher {int(real_max_freq_hz / 1e6)} MHz.')
182+
183+
frequency = frequencies[2 * i]
184+
for j in range(step_count):
185+
calculated_frequencies.append(frequency)
186+
frequency += sample_rate
187+
188+
if print_to_console:
189+
sys.stderr.write(f'Sweeping from {frequencies[2 * i] / 1e6} MHz to {frequencies[2 * i + 1] / 1e6} MHz\n')
190+
191+
if len(calculated_frequencies) > 256:
192+
device.pybladerf_close()
193+
raise RuntimeError('Reached maximum number of RX quick tune profiles. Please reduce the frequency range or increase the sample rate.')
194+
195+
quick_tunes = []
196+
for frequency in calculated_frequencies:
197+
device.pybladerf_set_frequency(formated_channel, frequency + offset)
198+
199+
quick_tune = device.pybladerf_get_quick_tune(formated_channel)
200+
quick_tunes.append((frequency, quick_tune))
201+
202+
device.pybladerf_set_rfic_rx_fir(pybladerf.pybladerf_rfic_rxfir.PYBLADERF_RFIC_RXFIR_BYPASS)
203+
device.pybladerf_sync_config(
204+
layout=pybladerf.pybladerf_channel_layout.PYBLADERF_RX_X1,
205+
data_format=pybladerf.pybladerf_format.PYBLADERF_FORMAT_SC8_Q7_META if oversample else pybladerf.pybladerf_format.PYBLADERF_FORMAT_SC16_Q11_META,
206+
num_buffers=int(os.environ.get('pybladerf_sweep_num_buffers', 4096)),
207+
buffer_size=int(os.environ.get('pybladerf_sweep_buffer_size', 8192)),
208+
num_transfers=int(os.environ.get('pybladerf_sweep_num_transfers', 64)),
209+
stream_timeout=0,
210+
)
211+
device.pybladerf_enable_module(formated_channel, True)
212+
213+
cdef uint64_t time_1ms = int(sample_rate // 1000)
214+
cdef uint64_t await_time = int(time_1ms * float(os.environ.get('pybladerf_sweep_await_time', 1.5)))
215+
cdef uint16_t tune_steps = len(calculated_frequencies)
216+
217+
cdef uint8_t free_rffe_profile = 0
218+
cdef uint8_t rffe_profiles = min(8, tune_steps)
219+
220+
cdef uint64_t schedule_timestamp = 0
221+
cdef double time_start = time.time()
222+
cdef double time_prev = time.time()
223+
cdef double timestamp = time.time()
224+
cdef double time_difference = 0
225+
cdef double sweep_rate = 0
226+
cdef double time_now = 0
227+
cdef uint64_t sweep_count = 0
228+
cdef uint32_t tune_step = 0
229+
230+
cdef uint8_t sweep_step_write_ptr = 0
231+
cdef uint8_t sweep_step_read_ptr = 0
232+
cdef ScanStep[8] scan_steps
233+
234+
cdef c_pybladerf.pybladerf_metadata meta = pybladerf.pybladerf_metadata()
235+
236+
cdef cnp.ndarray buffer = np.empty(samples_per_scan if oversample else samples_per_scan * 2, dtype=np.int8 if oversample else np.int16)
237+
cdef cnp.ndarray window = np.hanning(samples_per_scan)
238+
239+
schedule_timestamp = device.pybladerf_get_timestamp(pybladerf.pybladerf_direction.PYBLADERF_RX) + time_1ms * 150
240+
241+
for i in range(8):
242+
quick_tunes[tune_step][1].rffe_profile = free_rffe_profile
243+
device.pybladerf_schedule_retune(formated_channel, schedule_timestamp, 0, quick_tunes[tune_step][1])
244+
245+
scan_steps[sweep_step_write_ptr].frequency = quick_tunes[tune_step][0]
246+
scan_steps[sweep_step_write_ptr].schedule_time = schedule_timestamp + await_time
247+
sweep_step_write_ptr = (sweep_step_write_ptr + 1) % 8
248+
249+
free_rffe_profile = (free_rffe_profile + 1) % rffe_profiles
250+
schedule_timestamp += await_time + samples_per_scan
251+
tune_step = (tune_step + 1) % tune_steps
252+
253+
while working_sdrs[device_id].load():
254+
255+
meta.timestamp = scan_steps[sweep_step_read_ptr].schedule_time
256+
257+
try:
258+
timestamp = time.time()
259+
device.pybladerf_sync_rx(buffer, samples_per_scan, meta, 0)
260+
queue.put({
261+
'start_frequency': scan_steps[sweep_step_read_ptr].frequency,
262+
'stop_frequency': scan_steps[sweep_step_read_ptr].frequency + sample_rate,
263+
'raw_iq': (buffer - buffer.mean()) * window,
264+
'timestamp': timestamp,
265+
})
266+
267+
sweep_step_read_ptr = (sweep_step_read_ptr + 1) % 8
268+
269+
quick_tunes[tune_step][1].rffe_profile = free_rffe_profile
270+
device.pybladerf_schedule_retune(formated_channel, schedule_timestamp, 0, quick_tunes[tune_step][1])
271+
272+
scan_steps[sweep_step_write_ptr].frequency = quick_tunes[tune_step][0]
273+
scan_steps[sweep_step_write_ptr].schedule_time = schedule_timestamp + await_time
274+
sweep_step_write_ptr = (sweep_step_write_ptr + 1) % 8
275+
276+
free_rffe_profile = (free_rffe_profile + 1) % rffe_profiles
277+
schedule_timestamp += await_time + samples_per_scan
278+
tune_step = (tune_step + 1) % tune_steps
279+
280+
accepted_samples += samples_per_scan
281+
282+
except pybladerf.PYBLADERF_ERR_TIME_PAST:
283+
sys.stderr.write("Timestamp is in the past, restarting...\n")
284+
285+
tune_step = 0
286+
free_rffe_profile = 0
287+
sweep_step_read_ptr = 0
288+
sweep_step_write_ptr = 0
289+
290+
schedule_timestamp = device.pybladerf_get_timestamp(pybladerf.pybladerf_direction.PYBLADERF_RX) + time_1ms * 150
291+
292+
for i in range(8):
293+
quick_tunes[tune_step][1].rffe_profile = free_rffe_profile
294+
device.pybladerf_schedule_retune(formated_channel, schedule_timestamp, 0, quick_tunes[tune_step][1])
295+
296+
scan_steps[sweep_step_write_ptr].frequency = quick_tunes[tune_step][0]
297+
scan_steps[sweep_step_write_ptr].schedule_time = schedule_timestamp + await_time
298+
sweep_step_write_ptr = (sweep_step_write_ptr + 1) % 8
299+
300+
free_rffe_profile = (free_rffe_profile + 1) % rffe_profiles
301+
schedule_timestamp += await_time + samples_per_scan
302+
tune_step = (tune_step + 1) % tune_steps
303+
continue
304+
305+
except pybladerf.PYBLADERF_ERR as ex:
306+
sys.stderr.write("pybladerf_sync_rx() failed: %s %d", cbladerf.bladerf_strerror(ex.code), ex.code)
307+
working_sdrs[device_id].store(0)
308+
break
309+
310+
time_now = time.time()
311+
time_difference = time_now - time_prev
312+
if time_difference >= 1.0:
313+
if print_to_console:
314+
sweep_rate = sweep_count / (time_now - time_start)
315+
sys.stderr.write(f'{sweep_count} total sweeps completed, {round(sweep_rate, 2)} sweeps/second\n')
316+
317+
if accepted_samples == 0:
318+
if print_to_console:
319+
sys.stderr.write("Couldn\'t transfer any data for one second.\n")
320+
break
321+
322+
accepted_samples = 0
323+
time_prev = time_now
324+
325+
if print_to_console:
326+
if not working_sdrs[device_id].load():
327+
sys.stderr.write('\nExiting...\n')
328+
else:
329+
sys.stderr.write('\nExiting... [ pybladerf streaming stopped ]\n')
330+
331+
working_sdrs[device_id].store(0)
332+
sdr_ids.pop(device.serialno, None)
333+
334+
time_now = time.time()
335+
time_difference = time_now - time_prev
336+
if sweep_rate == 0 and time_difference > 0:
337+
sweep_rate = sweep_count / (time_now - time_start)
338+
339+
if print_to_console:
340+
sys.stderr.write(f'Total sweeps: {sweep_count} in {time_now - time_start:.5f} seconds ({sweep_rate :.2f} sweeps/second)\n')
341+
342+
if antenna_enable:
343+
try:
344+
device.pybladerf_set_bias_tee(formated_channel, False)
345+
except Exception as ex:
346+
sys.stderr.write(f'{ex}\n')
347+
348+
try:
349+
device.pybladerf_enable_module(formated_channel, False)
350+
except Exception as ex:
351+
sys.stderr.write(f'{ex}\n')
352+
353+
try:
354+
device.pybladerf_close()
355+
if print_to_console:
356+
sys.stderr.write('pybladerf_close() done\n')
357+
except Exception as ex:
358+
sys.stderr.write(f'{ex}\n')

0 commit comments

Comments
 (0)