Skip to content

Commit adf1c6b

Browse files
committed
added new mode
1 parent 39b50eb commit adf1c6b

File tree

2 files changed

+317
-0
lines changed

2 files changed

+317
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from python_hackrf import pyhackrf
2+
3+
def stop_all() -> None:
4+
...
5+
6+
def stop_sdr(serialno: str) -> None:
7+
...
8+
9+
def pyhackrf_sweep(frequencies: list[int], samples_per_scan: int, queue: object, sample_rate: int = 20_000_000, baseband_filter_bandwidth: int | None = None,
10+
lna_gain: int = 16, vga_gain: int = 20, amp_enable: bool = False, antenna_enable: bool = False, serial_number: str | None = None,
11+
print_to_console: bool = True) -> None:
12+
...
Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
# MIT License
2+
3+
# Copyright (c) 2023-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 libc.stdint cimport uint64_t, uint32_t, uint8_t
27+
from python_hackrf.pylibhackrf cimport pyhackrf as c_pyhackrf
28+
from python_hackrf import pyhackrf
29+
from libcpp.atomic cimport atomic
30+
cimport numpy as cnp
31+
import numpy as np
32+
import threading
33+
import signal
34+
import time
35+
import sys
36+
37+
cnp.import_array()
38+
FREQ_MIN_MHZ = 0 # 70 MHz
39+
FREQ_MAX_MHZ = 7_250 # 6000 MHZ
40+
FREQ_MIN_HZ = int(FREQ_MIN_MHZ * 1e6) # Hz
41+
FREQ_MAX_HZ = int(FREQ_MAX_MHZ * 1e6) # Hz
42+
AVAILABLE_SAMPLING_RATES = (2_000_000, 4_000_000, 6_000_000, 8_000_000, 10_000_000, 12_000_000, 14_000_000, 16_000_000, 18_000_000, 20_000_000)
43+
AVAILABLE_BASEBAND_FILTER_BANDWIDTHS = (1_750_000, 2_500_000, 3_500_000, 5_000_000, 5_500_000, 6_000_000, 7_000_000, 8_000_000, 9_000_000, 10_000_000, 12_000_000, 14_000_000, 15_000_000, 20_000_000, 24_000_000, 28_000_000)
44+
45+
46+
cdef atomic[uint8_t] working_sdrs[16]
47+
cdef dict sdr_ids = {}
48+
49+
def sigint_callback_handler(sig, frame, sdr_id):
50+
global working_sdrs
51+
working_sdrs[sdr_id].store(0)
52+
53+
54+
def init_signals() -> int:
55+
global working_sdrs
56+
57+
sdr_id = -1
58+
for i in range(16):
59+
if working_sdrs[i].load() == 0:
60+
sdr_id = i
61+
break
62+
63+
if sdr_id >= 0:
64+
try:
65+
signal.signal(signal.SIGINT, lambda sig, frame: sigint_callback_handler(sig, frame, sdr_id))
66+
signal.signal(signal.SIGILL, lambda sig, frame: sigint_callback_handler(sig, frame, sdr_id))
67+
signal.signal(signal.SIGTERM, lambda sig, frame: sigint_callback_handler(sig, frame, sdr_id))
68+
signal.signal(signal.SIGABRT, lambda sig, frame: sigint_callback_handler(sig, frame, sdr_id))
69+
except Exception as ex:
70+
sys.stderr.write(f'Error: {ex}\n')
71+
72+
return sdr_id
73+
74+
75+
def stop_all() -> None:
76+
global working_sdrs
77+
for i in range(16):
78+
working_sdrs[i].store(0)
79+
80+
81+
def stop_sdr(serialno: str) -> None:
82+
global sdr_ids, working_sdrs
83+
if serialno in sdr_ids:
84+
working_sdrs[sdr_ids[serialno]].store(0)
85+
86+
87+
cpdef int rx_callback(c_pyhackrf.PyHackrfDevice device, cnp.ndarray[cnp.int8_t, ndim=1] buffer, int buffer_length, int valid_length):
88+
global working_sdrs
89+
90+
cdef dict device_data = device.device_data
91+
cdef uint8_t device_id = device_data['device_id']
92+
cdef cnp.ndarray accepted_data
93+
cdef double divider = 1 / 128
94+
95+
if not working_sdrs[device_id].load():
96+
device_data['close_ready'].set()
97+
device_data['hop_ready'].set()
98+
return -1
99+
100+
device_data['accepted_bytes'] += valid_length
101+
102+
cdef uint64_t to_read = valid_length
103+
if device_data['num_samples'] > 0:
104+
if (to_read > device_data['num_samples'] * 2):
105+
to_read = device_data['num_samples'] * 2
106+
107+
accepted_data = (buffer[:to_read:2] * divider + 1j * buffer[1:to_read:2] * divider).astype(np.complex64)
108+
109+
device_data['buffer'][device_data['samples_per_scan'] - device_data['num_samples']: device_data['samples_per_scan'] - device_data['num_samples'] + (to_read // 2)] = (buffer[:to_read:2] / 128 + 1j * buffer[1:to_read:2] / 128).astype(np.complex64)
110+
device_data['num_samples'] -= (to_read // 2)
111+
112+
if device_data['num_samples'] == 0:
113+
device_data['hop_ready'].set()
114+
else:
115+
return -1
116+
117+
return 0
118+
119+
120+
def pyhackrf_scan(frequencies: list[int], samples_per_scan: int, queue: object, sample_rate: int = 20_000_000, baseband_filter_bandwidth: int | None = None,
121+
lna_gain: int = 16, vga_gain: int = 20, amp_enable: bool = False, antenna_enable: bool = False, serial_number: str | None = None,
122+
print_to_console: bool = True) -> None:
123+
124+
global working_sdrs, sdr_ids
125+
126+
cdef uint8_t device_id = init_signals()
127+
cdef c_pyhackrf.PyHackrfDevice device
128+
129+
pyhackrf.pyhackrf_init()
130+
131+
if serial_number is None:
132+
device = pyhackrf.pyhackrf_open()
133+
else:
134+
device = pyhackrf.pyhackrf_open_by_serial(serial_number)
135+
136+
working_sdrs[device_id].store(1)
137+
sdr_ids[device.serialno] = device_id
138+
139+
sample_rate = int(sample_rate) if int(sample_rate) in AVAILABLE_SAMPLING_RATES else 20_000_000
140+
141+
if baseband_filter_bandwidth is None:
142+
baseband_filter_bandwidth = int(sample_rate * .75)
143+
baseband_filter_bandwidth = int(baseband_filter_bandwidth) if int(baseband_filter_bandwidth) in AVAILABLE_BASEBAND_FILTER_BANDWIDTHS else pyhackrf.pyhackrf_compute_baseband_filter_bw(int(sample_rate * .75))
144+
145+
if print_to_console:
146+
sys.stderr.write(f'call pyhackrf_set_sample_rate({sample_rate / 1e6 :.3f} MHz)\n')
147+
device.pyhackrf_set_sample_rate(sample_rate)
148+
149+
if print_to_console:
150+
sys.stderr.write(f'call pyhackrf_set_baseband_filter_bandwidth({baseband_filter_bandwidth / 1e6 :.3f} MHz)\n')
151+
device.pyhackrf_set_baseband_filter_bandwidth(baseband_filter_bandwidth)
152+
153+
if lna_gain % 8 and print_to_console:
154+
sys.stderr.write('Warning: lna_gain must be a multiple of 8\n')
155+
156+
if vga_gain % 2 and print_to_console:
157+
sys.stderr.write('Warning: vga_gain must be a multiple of 2\n')
158+
159+
device.pyhackrf_set_lna_gain(lna_gain)
160+
device.pyhackrf_set_vga_gain(vga_gain)
161+
162+
if amp_enable:
163+
if print_to_console:
164+
sys.stderr.write('call pyhackrf_set_amp_enable(True)\n')
165+
device.pyhackrf_set_amp_enable(True)
166+
167+
if antenna_enable:
168+
if print_to_console:
169+
sys.stderr.write('call pyhackrf_set_antenna_enable(True)\n')
170+
device.pyhackrf_set_antenna_enable(True)
171+
172+
num_ranges = len(frequencies) // 2
173+
calculated_frequencies = []
174+
if pyhackrf.PY_MAX_SWEEP_RANGES < num_ranges:
175+
RuntimeError(f'specify a maximum of {pyhackrf.PY_MAX_SWEEP_RANGES} frequency ranges')
176+
177+
for i in range(num_ranges):
178+
frequencies[2 * i] = int(frequencies[2 * i] * 1e6)
179+
frequencies[2 * i + 1] = int(frequencies[2 * i + 1] * 1e6)
180+
181+
if frequencies[2 * i] >= frequencies[2 * i + 1]:
182+
raise RuntimeError('max frequency must be greater than min frequency.')
183+
184+
if frequencies[2 * i] < FREQ_MIN_HZ:
185+
raise RuntimeError(f'min frequency must must be greater than {FREQ_MIN_MHZ} MHz.')
186+
if frequencies[2 * i + 1] > FREQ_MAX_HZ:
187+
raise RuntimeError(f'max frequency may not be higher {FREQ_MAX_MHZ} MHz.')
188+
189+
step_count = 1 + (frequencies[2 * i + 1] - frequencies[2 * i] - 1) // sample_rate
190+
frequencies[2 * i + 1] = int(frequencies[2 * i] + step_count * sample_rate)
191+
192+
frequency = frequencies[2 * i]
193+
for j in range(step_count):
194+
calculated_frequencies.append(frequency)
195+
frequency += sample_rate
196+
197+
if print_to_console:
198+
sys.stderr.write(f'Sweeping from {frequencies[2 * i] / 1e6} MHz to {frequencies[2 * i + 1] / 1e6} MHz\n')
199+
200+
cdef cnp.ndarray buffer = np.empty(samples_per_scan, dtype=np.complex64)
201+
cdef cnp.ndarray window = np.hanning(samples_per_scan)
202+
cdef dict device_data = {
203+
'device_id': device_id,
204+
205+
'accepted_bytes': 0,
206+
207+
'samples_per_scan': samples_per_scan,
208+
'num_samples': samples_per_scan,
209+
'close_ready': threading.Event(),
210+
'hop_ready': threading.Event(),
211+
212+
'buffer': buffer,
213+
}
214+
215+
device.device_data = device_data
216+
device.set_rx_callback(rx_callback)
217+
218+
cdef double time_start = time.time()
219+
cdef double time_prev = time.time()
220+
cdef double timestamp = time.time()
221+
cdef double time_difference = 0
222+
cdef double sweep_rate = 0
223+
cdef double time_now = 0
224+
cdef uint64_t sweep_count = 0
225+
cdef uint32_t tune_step = 0
226+
cdef uint32_t tune_steps = len(calculated_frequencies)
227+
228+
device.pyhackrf_set_freq(calculated_frequencies[tune_step])
229+
tune_step = (tune_step + 1) % tune_steps
230+
device.pyhackrf_start_rx()
231+
232+
while device.pyhackrf_is_streaming() and working_sdrs[device_id].load():
233+
time_now = time.time()
234+
time_difference = time_now - time_prev
235+
236+
if time_difference >= 1.0:
237+
if print_to_console:
238+
sweep_rate = sweep_count / (time_now - time_start)
239+
sys.stderr.write(f'{sweep_count} total sweeps completed, {round(sweep_rate, 2)} sweeps/second\n')
240+
241+
if device_data['accepted_bytes'] == 0:
242+
if print_to_console:
243+
sys.stderr.write('Couldn\'t transfer any data for one second.\n')
244+
break
245+
246+
device_data['accepted_bytes'] = 0
247+
time_prev = time_now
248+
249+
if device_data['hop_ready'].wait():
250+
device.pyhackrf_stop_rx()
251+
252+
queue.put({
253+
'start_frequency': calculated_frequencies[tune_step],
254+
'stop_frequency': calculated_frequencies[tune_step] + sample_rate,
255+
'raw_iq': (buffer - buffer.mean()) * window,
256+
'timestamp': timestamp,
257+
})
258+
259+
device.pyhackrf_set_freq(calculated_frequencies[tune_step])
260+
tune_step = (tune_step + 1) % tune_steps
261+
if tune_step == 0:
262+
sweep_count += 1
263+
264+
device_data['num_samples'] = samples_per_scan
265+
device_data['hop_ready'].clear()
266+
timestamp = time.time()
267+
device.pyhackrf_start_rx()
268+
269+
if print_to_console:
270+
if not working_sdrs[device_id].load():
271+
sys.stderr.write('\nExiting...\n')
272+
else:
273+
sys.stderr.write('\nExiting... [ pyhackrf streaming stopped ]\n')
274+
275+
time_now = time.time()
276+
time_difference = time_now - time_prev
277+
if sweep_rate == 0 and time_difference > 0:
278+
sweep_rate = sweep_count / (time_now - time_start)
279+
280+
if print_to_console:
281+
sys.stderr.write(f'Total sweeps: {sweep_count} in {time_now - time_start:.5f} seconds ({sweep_rate :.2f} sweeps/second)\n')
282+
283+
working_sdrs[device_id].store(0)
284+
device_data['close_ready'].wait()
285+
sdr_ids.pop(device.serialno, None)
286+
287+
if antenna_enable:
288+
try:
289+
device.pyhackrf_set_antenna_enable(False)
290+
except Exception as e:
291+
sys.stderr.write(f'{e}\n')
292+
293+
try:
294+
device.pyhackrf_close()
295+
if print_to_console:
296+
sys.stderr.write('pyhackrf_close() done\n')
297+
except Exception as e:
298+
sys.stderr.write(f'{e}\n')
299+
300+
try:
301+
pyhackrf.pyhackrf_exit()
302+
if print_to_console:
303+
sys.stderr.write('pyhackrf_exit() done\n')
304+
except Exception as e:
305+
sys.stderr.write(f'{e}\n')

0 commit comments

Comments
 (0)