-
Notifications
You must be signed in to change notification settings - Fork 2
/
rflh.py
422 lines (352 loc) · 11.5 KB
/
rflh.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
import time
import argparse
import os
import matplotlib.pyplot as plt
import random as rnd
from math import pi
from rotor import *
# description
parser = argparse.ArgumentParser(description='Sweep the 360o around recording the signals levels of a frequency at a given bandwidth; using rotctld and a RTL-SDR (default) or a HackRF One for spectrum sensing.')
# frequency
parser.add_argument(
'frequency',
type=float,
help='The center frequency to listen to, in MHz, "145.170" for 145.170 MHz')
# -b / --bandwidth
parser.add_argument(
'-b ', '--bandwidth',
type=int,
help='The bandwidth to sample in kHz: 1-800 kHz using the RTL-SDR or 1kHz-MHz using the HackRF, 20kHz by default')
# -s / --step
parser.add_argument(
'-s', '--step',
type=int,
help='Azimuth steps in degrees, 10 degrees by default, see project README.md for details.')
# -u / --paused
parser.add_argument(
'-u', '--paused',
help='Sweep mode: paused mode means move to target azimuth & take a measurement the repeat it (slow). The default is to make one full turn and do measurements on the fly (faster but may fail, read the documentation)',
action="store_true")
# -bs / --bucketsize
parser.add_argument(
'-t', '--bucketsize',
type=int,
help='Bucket size: how many bytes to get for processing at each sample time, 1.024 Mbytes by default (use 2^n units or it will fail) 1.024 Mbytes = 1024000')
# -p / --ppm
parser.add_argument(
'-p', '--ppm',
type=int,
help='Frequency correction for you device, by default 0.0')
# -q / --quiet
parser.add_argument(
'-q', '--quiet',
help='Quiet: supress output, default: verbose',
action="store_true")
# -j / --just_data
parser.add_argument(
'-j', '--just_data',
help='Don\'t generate & save the graph, default: generate & save it',
action="store_true")
# -n / --nofile
parser.add_argument(
'-n', '--nofile',
help='Don\'t save the csv file, default: save it',
action="store_true")
# -i / --interactive
parser.add_argument(
'-i', '--interactive',
help='Pop up a Matplotlib interactive graph with the results, default is no pop up',
action="store_true")
# -d / --dummy
parser.add_argument(
'-d', '--dummy',
help='Don\'t use the rotor or the RTL-SDR or HackRF, just generate a dummy dataset and plot it; it implies --nofile; just for testing purposes',
action="store_true")
# -l / --lna
parser.add_argument(
'-l', '--lna',
type=float,
help='LNA gain value: 0-49.6 dB in 0.4 dB for the RTL-SDR or 0-40 dB in 8 dB steps for HackRF. Defaults: 28.0 for the RTL-SDR & 32 for the HackRF')
# -v / --vga
parser.add_argument(
'-v', '--vga',
type=int,
help='VGA gain value (Only HackRF One): 0-62 in 2 units steps, 30 by default')
# -a / --amp_on
parser.add_argument(
'-a', '--amp_on',
help='Amplifier on (Only HackRF One): by default it\'s disabled; WATCH OUT! some firmware revision has this option reversed (mine has it)',
action="store_true")
# -o / --hackrfone
parser.add_argument(
'-o', '--hackrfone',
help='Use a HackRF One instead the default: RTL-SDR',
action="store_true")
args = parser.parse_args()
# args setup
fq = float(args.frequency)
bw = 20
if args.bandwidth:
bw = args.bandwidth
if args.step:
if args.step < 6:
print("WARNING: You selected a azimuth step lower/equal than 6 degrees: most rotors can't handle that")
astep = args.step
else:
astep = 10
just_data = False
if args.just_data:
just_data = True
quiet = False
if args.quiet:
quiet = True
nofile = False
if args.nofile:
nofile = True
interactive = False
if args.interactive:
interactive = True
dummy = False
if args.dummy:
dummy = True
lna_gain = None
if args.lna:
lna_gain = args.lna
vga_gain = None
if args.vga:
vga_gain = args.vga
amp_on = False
if args.amp_on:
amp_on = True
hackrfone = False
if args.hackrfone:
hackrfone = True
ppm = 0
if args.ppm:
ppm = args.ppm
# bucketsize
bucketsize = 1.024e6
if args.bucketsize:
bucketsize = args.bucketsize
paused = False
if args.paused:
paused = True
elif args.step and args.step <= 5:
print("WARNING: You selected a step less than 6 degrees & fast scanning, most hardware can\'t handle that!")
# conditional load of the device
if hackrfone:
from hrf import *
if not quiet:
print("Using HackRF One as RF device")
else:
from rtl import *
if not quiet:
print("Using RTL-SDR as RF device")
# instantiating if not testing
if not dummy:
r = Rotor()
rf = RF(ppm)
device = 'rtl'
if hackrfone:
device = 'hackrf'
# array that will hold the data
labels = []
levels = []
f = fq * 1000000
duration = 0
def clean_house():
if not dummy:
rf.close()
r.go_to(0, 0)
# All is wraped to detect Ctrl+c
try:
# RF setup & rotor parking
if not dummy:
# set HackRF
rf.set_freq(f)
rf.set_bw(bw)
rf.bucket(bucketsize)
if vga_gain != None and hackrfone:
rf.set_gain_vga(vga_gain)
if not quiet:
print("VGA gain set to {}".format(vga_gain))
if lna_gain != None:
rf.set_gain_lna(lna_gain)
if not quiet:
print("LNA gain set to {}".format(lna_gain))
if amp_on and hackrfone:
rf.amp_on()
if not quiet:
print("Amp turned on!")
# rotor parking advice
if not quiet:
print("Parking the rotor, please wait...")
# rotor parking (blocking)
r.set_position(0, 0)
# rotor parking advice
if not quiet:
if not dummy:
print("Parking done, starting the sweep")
print("Sweep for {} Hz ({} MHz) with {} kHz of bandwidth & {} degrees of step.".format(
f, f/1e6, bw, astep))
# getting time for the file
dt = time.strftime("%Y%m%d_%H%M")
# if dummy data
if not dummy:
if paused:
# slow scanning
tstart = time.time()
for p in range(0, 360, astep):
labels.append(str(p))
r.set_position(p, 0)
l = rf.get_average()
levels.append(l)
if not quiet:
print("{};{}".format(p, str(l).replace(".", ",")))
tstop = time.time()
else:
# fast scanning
a = 0
at = astep
tstart = time.time()
# zero
labels.append(str(a))
l = rf.get_average()
levels.append(l)
if not quiet:
print("0(0);{}".format(str(l).replace(".", ",")))
# start turning!
r.go_to(360)
while a < (360 - astep):
(a, e) = r.get_position()
if (at - a) < (astep / 2):
# data
l = rf.get_average()
levels.append(l)
(an, e) = r.get_position()
am = (an + a) / 2
labels.append(str(at))
# debug
if not quiet:
print("{}({});{}".format(at, am, str(l).replace(".", ",")))
if (am - at) >= astep:
print("ERROR!\n\nThe selected azimuth step is to small or bucket size to big, please adjust them and try again!")
clean_house()
sys.exit()
# increment at
at += astep
tstop = time.time()
# time elapsed
duration = tstop - tstart
if not quiet:
print("Scan took {}:{}".format(int(duration/60), int(duration % 60)))
# Create the name for the file if not told otherwise
if not nofile:
dfolder = os.path.join(os.getcwd(), 'data')
# the data folder exists?
if not os.path.exists(dfolder):
os.mkdir(dfolder)
else:
if not os.path.isdir(dfolder):
os.unlink(dfolder)
os.path.mkdir(dfolder)
# 20211219_2221_rtl_436.5MHz_200kHz_10o.csv
savefile = "{}_{}_{}MHz_{}kHz_{}o".format(dt, device, fq, bw, astep)
with open(os.path.join(dfolder, savefile + '.csv'), 'w') as f:
# write header
f.writelines("Degrees;dBFs\n")
i = 0
for v in labels:
f.writelines(str(v) + ";" + str(levels[i]).replace(".", ",") + "\n")
i = i + 1
if not quiet:
print("CSVFile: data/{}".format(savefile + ".csv"))
else:
# fake data
rnd.seed()
astep = 5
units = 360/astep
labels = [x for x in range(0, 360, astep)]
start = rnd.randint(-1100, -600)
stop = rnd.randint(start, 50)
levels = []
for l in labels:
levels.append(rnd.randrange(start, stop)/10.0)
if not quiet and not dummy:
print("Parking the rotor in the background")
clean_house()
# statistics
margin = 0.5
amin = min(levels)
amax = max(levels)
tmargin = abs(amax - amin)
if tmargin < 1:
margin = 0.5
else:
margin = tmargin * 0.1
lmin = amin - margin
lmax = amax + margin
if not quiet:
print("Dynamc range: {} dB, 10%: {}".format(tmargin, margin))
print("Min: {}, Max {}".format(lmin, lmax))
if not just_data:
speed = 'fast'
if paused:
speed = 'paused'
title = u"{}: ({}: {}) {:.3f} MHz, BW: {:.1f} kHz,\n{}o steps, {:.1f} dB of DNR".format(
device.upper(),
speed,
"{}:{} min".format(
int(duration / 60),
int(duration % 60)
),
fq,
bw,
astep,
tmargin
)
# Turn interactive plotting off
plt.ioff()
# create
fig = plt.figure()
ax = fig.add_subplot(label='title')
fig.subplots_adjust(top=0.85)
# Set titles for the figure and the subplot respectively
fig.suptitle(title, fontsize=12, fontweight='normal')
# repeat the last value to close the plot
levels += levels[:1]
# how many ticks/labels on the plot
N = len(labels)
# What will be the angle of each axis in the plot? (we divide the plot / number of variable)
angles = [n / float(N) * 2 * pi for n in range(N)]
# repeat the first one to close the graph
angles += angles[:1]
# Initialise the spider plot
ax = plt.subplot(111, polar=True, label='graph')
# Draw one axe per variable + add labels
plt.xticks(angles, labels, color='grey', size=8)
# set labels limits
plt.ylim(lmin, lmax)
# Plot data
ax.plot(angles, levels, linewidth=1, linestyle='solid')
# rotate
ax.set_theta_zero_location('N')
# make it clockwise
ax.set_theta_direction(-1)
# Fill area
ax.fill(angles, levels, 'b', alpha=0.1)
# save only if not dummy
if not dummy and not just_data:
plt.savefig(
os.path.join(dfolder, savefile + '.png'),
bbox_inches='tight'
)
if not quiet:
print("ImgFile: data/{}".format(savefile + ".png"))
# Show the graph if instructed to
if interactive:
plt.show()
except KeyboardInterrupt:
print("\n\nCatching Ctrl+C: cleaning the house before leaving...")
clean_house()
sys.exit()