Advertisement
fahim420

rtlscanner.py

Oct 3rd, 2015
77
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 57.32 KB | None | 0 0
  1. #! /usr/bin/env python
  2.  
  3. #
  4. # rtlsdr_scan
  5. #
  6. # http://eartoearoak.com/software/rtlsdr-scanner
  7. #
  8. # Copyright 2012, 2013 Al Brown
  9. #
  10. # A frequency scanning GUI for the OsmoSDR rtl-sdr library at
  11. # http://sdr.osmocom.org/trac/wiki/rtl-sdr
  12. #
  13. #
  14. # This program is free software: you can redistribute it and/or modify
  15. # it under the terms of the GNU General Public License as published by
  16. # the Free Software Foundation, or (at your option)
  17. # any later version.
  18. #
  19. # This program is distributed in the hope that it will be useful,
  20. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  21. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  22. # GNU General Public License for more details.
  23. #
  24. # You should have received a copy of the GNU General Public License
  25. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  26. #
  27.  
  28. try:
  29. input = raw_input
  30. except:
  31. pass
  32.  
  33. try:
  34. import matplotlib
  35. matplotlib.interactive(True)
  36. matplotlib.use('WXAgg')
  37. from matplotlib.backends.backend_wxagg import \
  38. FigureCanvasWxAgg as FigureCanvas, \
  39. NavigationToolbar2WxAgg
  40. from matplotlib.backends.backend_wx import _load_bitmap
  41. import argparse
  42. import cPickle
  43. import itertools
  44. import math
  45. import numpy
  46. import os.path
  47. import rtlsdr
  48. import threading
  49. import wx
  50. import wx.lib.masked as masked
  51. import wx.lib.mixins.listctrl as listmix
  52. import wx.grid as grid
  53. except ImportError as error:
  54. print('Import error: {0}'.format(error))
  55. input('\nError importing libraries\nPress [Return] to exit')
  56. exit(1)
  57.  
  58.  
  59. F_MIN = 0
  60. F_MAX = 9999
  61. GAIN = 0
  62. SAMPLE_RATE = 2e6
  63. BANDWIDTH = 500e3
  64. NFFT = [128,
  65. 512,
  66. 1024,
  67. 2048,
  68. 4096,
  69. 8192,
  70. 16384,
  71. 32768]
  72.  
  73. DWELL = ["10 ms", 0.01,
  74. "25 ms", 0.025,
  75. "50 ms", 0.05,
  76. "100 ms", 0.1,
  77. "200 ms", 0.2,
  78. "500 ms", 0.5,
  79. "1 s", 1,
  80. "2 s", 2,
  81. "5 s", 5]
  82.  
  83. THREAD_STATUS_STARTING = 0
  84. THREAD_STATUS_SCAN = 1
  85. THREAD_STATUS_DATA = 2
  86. THREAD_STATUS_FINISHED = 3
  87. THREAD_STATUS_STOPPED = 4
  88. THREAD_STATUS_ERROR = 5
  89. THREAD_STATUS_PROCESSED = 6
  90.  
  91. WARN_SCAN = 0
  92. WARN_OPEN = 1
  93. WARN_EXIT = 2
  94.  
  95. CAL_START = 0
  96. CAL_DONE = 1
  97. CAL_OK = 2
  98. CAL_CANCEL = 3
  99.  
  100. FILE_RFS = "RTLSDR frequency scan (*.rfs)|*.rfs"
  101. FILE_CSV = "CSV table (*.csv)|*.csv"
  102. FILE_HEADER = "RTLSDR Scanner"
  103. FILE_VERSION = 1
  104.  
  105. EVT_THREAD_STATUS = wx.NewId()
  106.  
  107.  
  108. class Device():
  109. def __init__(self):
  110. self.index = None
  111. self.name = None
  112. self.gain = 0
  113. self.calibration = None
  114. self.lo = None
  115. self.offset = 250e3
  116.  
  117. class Settings():
  118. def __init__(self):
  119. self.cfg = None
  120. self.start = None
  121. self.stop = None
  122. self.dwell = 0.0
  123. self.nfft = 0
  124. self.calFreq = None
  125. self.yAuto = True
  126. self.yMax = 1
  127. self.yMin = 0
  128. self.devices = []
  129. self.index = None
  130.  
  131. self.load()
  132.  
  133. def load(self):
  134. self.cfg = wx.Config('rtlsdr-scanner')
  135. self.start = self.cfg.ReadInt('start', 87)
  136. self.stop = self.cfg.ReadInt('stop', 108)
  137. self.dwell = self.cfg.ReadFloat('dwell', 0.1)
  138. self.nfft = int(self.cfg.Read('nfft', '1024'))
  139. self.calFreq = self.cfg.ReadFloat('calFreq', 1575.42)
  140. self.index = self.cfg.ReadInt('index', 0)
  141. self.cfg.SetPath("/Devices")
  142. group = self.cfg.GetFirstGroup()
  143. while group[0]:
  144. self.cfg.SetPath("/Devices/" + group[1])
  145. device = Device()
  146. device.name = group[1]
  147. device.gain = self.cfg.ReadFloat('gain', 0)
  148. device.calibration = self.cfg.ReadFloat('calibration', 0)
  149. device.lo = self.cfg.ReadFloat('lo', 0)
  150. device.offset = self.cfg.ReadFloat('offset', 250e3)
  151. self.devices.append(device)
  152. self.cfg.SetPath("/Devices")
  153. group = self.cfg.GetNextGroup(group[2])
  154.  
  155.  
  156. def save(self):
  157. self.cfg.SetPath("/")
  158. self.cfg.WriteInt('start', self.start)
  159. self.cfg.WriteInt('stop', self.stop)
  160. self.cfg.WriteFloat('dwell', self.dwell)
  161. self.cfg.Write('nfft', str(self.nfft))
  162. self.cfg.WriteFloat('calFreq', self.calFreq)
  163. self.cfg.WriteInt('index', self.index)
  164. if self.devices:
  165. for device in self.devices:
  166. self.cfg.SetPath("/Devices/" + format_device_name(device.name))
  167. self.cfg.WriteFloat('gain', device.gain)
  168. self.cfg.WriteFloat('lo', device.lo)
  169. self.cfg.WriteFloat('calibration', device.calibration)
  170. self.cfg.WriteFloat('offset', device.offset)
  171.  
  172. class Status():
  173. def __init__(self, status, freq, data):
  174. self.status = status
  175. self.freq = freq
  176. self.data = data
  177.  
  178. def get_status(self):
  179. return self.status
  180.  
  181. def get_freq(self):
  182. return self.freq
  183.  
  184. def get_data(self):
  185. return self.data
  186.  
  187.  
  188. class EventThreadStatus(wx.PyEvent):
  189. def __init__(self, status, freq, data):
  190. wx.PyEvent.__init__(self)
  191. self.SetEventType(EVT_THREAD_STATUS)
  192. self.data = Status(status, freq, data)
  193.  
  194.  
  195. class ThreadScan(threading.Thread):
  196. def __init__(self, notify, settings, devices, samples, isCal):
  197. threading.Thread.__init__(self)
  198. self.notify = notify
  199. self.index = settings.index
  200. self.fstart = settings.start * 1e6
  201. self.fstop = settings.stop * 1e6
  202. self.samples = samples
  203. self.isCal = isCal
  204. self.gain = devices[self.index].gain
  205. self.lo = devices[self.index].lo * 1e6
  206. self.offset = devices[self.index].offset
  207. self.cancel = False
  208. wx.PostEvent(self.notify, EventThreadStatus(THREAD_STATUS_STARTING,
  209. None, None))
  210. self.start()
  211.  
  212. def run(self):
  213. sdr = self.rtl_setup()
  214. if sdr is None:
  215. return
  216. freq = self.fstart - self.offset
  217.  
  218. while freq <= self.fstop + self.offset:
  219. if self.cancel:
  220. wx.PostEvent(self.notify,
  221. EventThreadStatus(THREAD_STATUS_STOPPED,
  222. None, None))
  223. sdr.close()
  224. return
  225. try:
  226. progress = ((freq - self.fstart + self.offset) /
  227. (self.fstop - self.fstart + BANDWIDTH)) * 100
  228. wx.PostEvent(self.notify, EventThreadStatus(THREAD_STATUS_SCAN,
  229. None, progress))
  230. scan = self.scan(sdr, freq)
  231. wx.PostEvent(self.notify, EventThreadStatus(THREAD_STATUS_DATA,
  232. freq, scan))
  233. except (IOError, WindowsError):
  234. if sdr is not None:
  235. sdr.close()
  236. sdr = self.rtl_setup()
  237. except (TypeError, AttributeError) as error:
  238. if self.notify:
  239. wx.PostEvent(self.notify,
  240. EventThreadStatus(THREAD_STATUS_ERROR,
  241. None, error.message))
  242. return
  243.  
  244. freq += BANDWIDTH / 2
  245.  
  246. sdr.close()
  247. wx.PostEvent(self.notify, EventThreadStatus(THREAD_STATUS_FINISHED,
  248. None, self.isCal))
  249.  
  250. def abort(self):
  251. self.cancel = True
  252.  
  253. def rtl_setup(self):
  254. sdr = None
  255. try:
  256. sdr = rtlsdr.RtlSdr(self.index)
  257. sdr.set_sample_rate(SAMPLE_RATE)
  258. sdr.set_gain(self.gain)
  259. except IOError as error:
  260. wx.PostEvent(self.notify, EventThreadStatus(THREAD_STATUS_ERROR,
  261. None, error.message))
  262.  
  263. return sdr
  264.  
  265. def scan(self, sdr, freq):
  266. sdr.set_center_freq(freq + self.lo)
  267. capture = sdr.read_samples(self.samples)
  268.  
  269. return capture
  270.  
  271.  
  272. class ThreadProcess(threading.Thread):
  273. def __init__(self, notify, freq, data, settings, devices, nfft):
  274. threading.Thread.__init__(self)
  275. self.notify = notify
  276. self.freq = freq
  277. self.data = data
  278. self.cal = devices[settings.index].calibration
  279. self.nfft = nfft
  280. self.window = matplotlib.numpy.hamming(nfft)
  281.  
  282. self.start()
  283.  
  284. def run(self):
  285. scan = {}
  286. powers, freqs = matplotlib.mlab.psd(self.data,
  287. NFFT=self.nfft,
  288. Fs=SAMPLE_RATE / 1e6,
  289. window=self.window)
  290. for freq, pwr in itertools.izip(freqs, powers):
  291. xr = freq + (self.freq / 1e6)
  292. xr = xr + (xr * self.cal / 1e6)
  293. xr = int((xr * 5e4) + 0.5) / 5e4
  294. scan[xr] = pwr
  295. wx.PostEvent(self.notify, EventThreadStatus(THREAD_STATUS_PROCESSED,
  296. self.freq, scan))
  297.  
  298. class DropTarget(wx.FileDropTarget):
  299. def __init__(self, window):
  300. wx.FileDropTarget.__init__(self)
  301. self.window = window
  302.  
  303. def OnDropFiles(self, _xPos, _yPos, filenames):
  304. filename = filenames[0]
  305. if os.path.splitext(filename)[1].lower() == ".rfs":
  306. self.window.dirname, self.window.filename = os.path.split(filename)
  307. self.window.open()
  308.  
  309.  
  310. class DeviceList(wx.ListCtrl, listmix.TextEditMixin):
  311. def __init__(self, parent, ID=wx.ID_ANY, pos=wx.DefaultPosition,
  312. size=wx.DefaultSize, style=0):
  313. wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
  314. listmix.TextEditMixin.__init__(self)
  315.  
  316.  
  317. class CellRenderer(grid.PyGridCellRenderer):
  318. def __init__(self):
  319. grid.PyGridCellRenderer.__init__(self)
  320.  
  321. def Draw(self, grid, attr, dc, rect, row, col, _isSelected):
  322. dc.SetBrush(wx.Brush(attr.GetBackgroundColour()))
  323. dc.DrawRectangleRect(rect)
  324. if grid.GetCellValue(row, col) == "1":
  325. dc.SetBrush(wx.Brush(attr.GetTextColour()))
  326. dc.DrawCircle(rect.x + (rect.width / 2), rect.y + (rect.height / 2),
  327. rect.height / 4)
  328.  
  329.  
  330. class NavigationToolbar(NavigationToolbar2WxAgg):
  331. def __init__(self, canvas, main):
  332. self.main = main
  333.  
  334. navId = wx.NewId()
  335. NavigationToolbar2WxAgg.__init__(self, canvas)
  336. self.AddSimpleTool(navId, _load_bitmap('subplots.png'),
  337. 'Range', 'Set plot range')
  338. wx.EVT_TOOL(self, navId, self.on_range)
  339.  
  340. def on_range(self, _event):
  341.  
  342. dlg = DialogRange(self, self.main)
  343. dlg.ShowModal()
  344. dlg.Destroy()
  345. self.canvas.draw()
  346. self.main.draw_plot()
  347.  
  348.  
  349. class NavigationToolbarCompare(NavigationToolbar2WxAgg):
  350. def __init__(self, canvas):
  351. NavigationToolbar2WxAgg.__init__(self, canvas)
  352.  
  353.  
  354. class PanelGraph(wx.Panel):
  355. def __init__(self, parent, main):
  356. self.main = main
  357.  
  358. wx.Panel.__init__(self, parent)
  359.  
  360. self.figure = matplotlib.figure.Figure(facecolor='white')
  361. self.axes = self.figure.add_subplot(111)
  362. self.canvas = FigureCanvas(self, -1, self.figure)
  363. self.canvas.mpl_connect('motion_notify_event', self.on_motion)
  364. self.toolbar = NavigationToolbar(self.canvas, self.main)
  365. self.toolbar.Realize()
  366. self.toolbar.DeleteToolByPos(1)
  367. self.toolbar.DeleteToolByPos(1)
  368. self.toolbar.DeleteToolByPos(4)
  369.  
  370. vbox = wx.BoxSizer(wx.VERTICAL)
  371. vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
  372. vbox.Add(self.toolbar, 0, wx.EXPAND)
  373.  
  374. self.SetSizer(vbox)
  375. vbox.Fit(self)
  376.  
  377. def on_motion(self, event):
  378. if self.main.thread:
  379. return
  380. xpos = event.xdata
  381. ypos = event.ydata
  382. text = ""
  383. if xpos is not None:
  384. spectrum = self.main.spectrum
  385. if len(spectrum) > 0:
  386. xpos = min(spectrum.keys(), key=lambda freq: abs(freq - xpos))
  387. ypos = spectrum[xpos]
  388. text = "f = {0:.3f}MHz, p = {1:.2f}dB".format(xpos, ypos)
  389.  
  390. self.main.status.SetStatusText(text, 1)
  391.  
  392. def get_canvas(self):
  393. return self.canvas
  394.  
  395. def get_axes(self):
  396. return self.axes
  397.  
  398. def get_toolbar(self):
  399. return self.toolbar
  400.  
  401.  
  402. class PanelGraphCompare(wx.Panel):
  403. def __init__(self, parent):
  404.  
  405. self.spectrum1 = None
  406. self.spectrum2 = None
  407.  
  408. wx.Panel.__init__(self, parent)
  409.  
  410. figure = matplotlib.figure.Figure(facecolor='white')
  411.  
  412. self.axesScan = figure.add_subplot(111)
  413. self.axesDiff = self.axesScan.twinx()
  414. self.plotScan1, = self.axesScan.plot([], [], 'b-',
  415. linewidth=0.4)
  416. self.plotScan2, = self.axesScan.plot([], [], 'g-',
  417. linewidth=0.4)
  418. self.plotDiff, = self.axesDiff.plot([], [], 'r-', linewidth=0.4)
  419. self.axesScan.set_ylim(auto=True)
  420. self.axesDiff.set_ylim(auto=True)
  421.  
  422. self.axesScan.set_title("Level Comparison")
  423. self.axesScan.set_xlabel("Frequency (MHz)")
  424. self.axesScan.set_ylabel('Level (dB)')
  425. self.axesDiff.set_ylabel('Difference (db)')
  426.  
  427. self.canvas = FigureCanvas(self, -1, figure)
  428.  
  429. self.check1 = wx.CheckBox(self, wx.ID_ANY, "Scan 1")
  430. self.check2 = wx.CheckBox(self, wx.ID_ANY, "Scan 2")
  431. self.checkDiff = wx.CheckBox(self, wx.ID_ANY, "Difference")
  432. self.checkGrid = wx.CheckBox(self, wx.ID_ANY, "Grid")
  433. self.check1.SetValue(True)
  434. self.check2.SetValue(True)
  435. self.checkDiff.SetValue(True)
  436. self.checkGrid.SetValue(False)
  437. self.Bind(wx.EVT_CHECKBOX, self.on_check1, self.check1)
  438. self.Bind(wx.EVT_CHECKBOX, self.on_check2, self.check2)
  439. self.Bind(wx.EVT_CHECKBOX, self.on_check_diff, self.checkDiff)
  440. self.Bind(wx.EVT_CHECKBOX, self.on_check_grid, self.checkGrid)
  441.  
  442. grid = wx.GridBagSizer(5, 5)
  443. grid.Add(self.check1, pos=(0, 0), flag=wx.ALIGN_CENTER)
  444. grid.Add(self.check2, pos=(0, 1), flag=wx.ALIGN_CENTER)
  445. grid.Add((20, 1), pos=(0, 2))
  446. grid.Add(self.checkDiff, pos=(0, 3), flag=wx.ALIGN_CENTER)
  447. grid.Add((20, 1), pos=(0, 4))
  448. grid.Add(self.checkGrid, pos=(0, 5), flag=wx.ALIGN_CENTER)
  449.  
  450. toolbar = NavigationToolbarCompare(self.canvas)
  451. toolbar.Realize()
  452.  
  453. vbox = wx.BoxSizer(wx.VERTICAL)
  454. vbox.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
  455. vbox.Add(grid, 0, wx.ALIGN_CENTRE | wx.ALL, border=5)
  456. vbox.Add(toolbar, 0, wx.EXPAND)
  457.  
  458. self.SetSizer(vbox)
  459. vbox.Fit(self)
  460.  
  461. def on_check1(self, _event):
  462. self.plotScan1.set_visible(self.check1.GetValue())
  463. self.canvas.draw()
  464.  
  465. def on_check2(self, _event):
  466. self.plotScan2.set_visible(self.check2.GetValue())
  467. self.canvas.draw()
  468.  
  469. def on_check_diff(self, _event):
  470. self.plotDiff.set_visible(self.checkDiff.GetValue())
  471. self.canvas.draw()
  472.  
  473. def on_check_grid(self, _event):
  474. self.axesDiff.grid(self.checkGrid.GetValue())
  475. self.canvas.draw()
  476.  
  477. def plot_diff(self):
  478. diff = {}
  479.  
  480. if self.spectrum1 is not None and self.spectrum2 is not None :
  481. set1 = set(self.spectrum1)
  482. set2 = set(self.spectrum2)
  483. intersect = set1.intersection(set2)
  484. for freq in intersect:
  485. diff[freq] = self.spectrum1[freq] - self.spectrum2[freq]
  486. freqs, powers = split_spectrum(diff)
  487. self.plotDiff.set_xdata(freqs)
  488. self.plotDiff.set_ydata(powers)
  489. elif self.spectrum1 is None:
  490. freqs, powers = split_spectrum(self.spectrum2)
  491. self.plotDiff.set_xdata(freqs)
  492. self.plotDiff.set_ydata([0] * len(freqs))
  493. else:
  494. freqs, powers = split_spectrum(self.spectrum1)
  495. self.plotDiff.set_xdata(freqs)
  496. self.plotDiff.set_ydata([0] * len(freqs))
  497.  
  498. self.axesDiff.relim()
  499. self.axesDiff.autoscale_view()
  500.  
  501. def set_spectrum1(self, spectrum):
  502. self.spectrum1 = spectrum
  503. freqs, powers = split_spectrum(spectrum)
  504. self.plotScan1.set_xdata(freqs)
  505. self.plotScan1.set_ydata(powers)
  506. self.plot_diff()
  507. self.axesScan.relim()
  508. self.axesScan.autoscale_view()
  509. self.canvas.draw()
  510.  
  511. def set_spectrum2(self, spectrum):
  512. self.spectrum2 = spectrum
  513. freqs, powers = split_spectrum(spectrum)
  514. self.plotScan2.set_xdata(freqs)
  515. self.plotScan2.set_ydata(powers)
  516. self.plot_diff()
  517. self.axesScan.relim()
  518. self.axesScan.autoscale_view()
  519. self.canvas.draw()
  520.  
  521.  
  522. class DialogCompare(wx.Dialog):
  523. def __init__(self, parent, dirname, filename):
  524.  
  525. self.dirname = dirname
  526. self.filename = filename
  527.  
  528. wx.Dialog.__init__(self, parent=parent, title="Compare plots",
  529. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.MAXIMIZE_BOX)
  530.  
  531. self.graph = PanelGraphCompare(self)
  532.  
  533. self.buttonPlot1 = wx.Button(self, wx.ID_ANY, 'Load plot #1')
  534. self.buttonPlot2 = wx.Button(self, wx.ID_ANY, 'Load plot #2')
  535. self.Bind(wx.EVT_BUTTON, self.on_load_plot, self.buttonPlot1)
  536. self.Bind(wx.EVT_BUTTON, self.on_load_plot, self.buttonPlot2)
  537. self.textPlot1 = wx.StaticText(self, label="<None>")
  538. self.textPlot2 = wx.StaticText(self, label="<None>")
  539.  
  540. buttonClose = wx.Button(self, wx.ID_CLOSE, 'Close')
  541. self.Bind(wx.EVT_BUTTON, self.on_close, buttonClose)
  542.  
  543. grid = wx.GridBagSizer(5, 5)
  544. grid.AddGrowableCol(2, 0)
  545. grid.Add(self.buttonPlot1, pos=(0, 0), flag=wx.ALIGN_CENTER)
  546. grid.Add(self.textPlot1, pos=(0, 1), span=(1, 2))
  547. grid.Add(self.buttonPlot2, pos=(1, 0), flag=wx.ALIGN_CENTER)
  548. grid.Add(self.textPlot2, pos=(1, 1), span=(1, 2))
  549. grid.Add(buttonClose, pos=(2, 3), flag=wx.ALIGN_RIGHT)
  550.  
  551. sizer = wx.BoxSizer(wx.VERTICAL)
  552. sizer.Add(self.graph, 1, wx.EXPAND)
  553. sizer.Add(grid, 0, wx.EXPAND | wx.ALL, border=5)
  554. self.SetSizerAndFit(sizer)
  555.  
  556. def on_load_plot(self, event):
  557. dlg = wx.FileDialog(self, "Open a scan", self.dirname, self.filename,
  558. FILE_RFS, wx.OPEN)
  559. if dlg.ShowModal() == wx.ID_OK:
  560. _start, _stop, spectrum = open_plot(dlg.GetDirectory(),
  561. dlg.GetFilename())
  562. if(event.EventObject == self.buttonPlot1):
  563. self.textPlot1.SetLabel(dlg.GetFilename())
  564. self.graph.set_spectrum1(spectrum)
  565. else:
  566. self.textPlot2.SetLabel(dlg.GetFilename())
  567. self.graph.set_spectrum2(spectrum)
  568. dlg.Destroy()
  569.  
  570. def on_close(self, _event):
  571. self.EndModal(wx.ID_CLOSE)
  572. return
  573.  
  574.  
  575. class DialogAutoCal(wx.Dialog):
  576. def __init__(self, parent, freq, callback):
  577. self.callback = callback
  578. self.cal = 0
  579.  
  580. wx.Dialog.__init__(self, parent=parent, title="Auto Calibration",
  581. style=wx.CAPTION)
  582. self.Bind(wx.EVT_CLOSE, self.on_close)
  583.  
  584. title = wx.StaticText(self, label="Calibrate to a known stable signal")
  585. font = title.GetFont()
  586. font.SetPointSize(font.GetPointSize() + 2)
  587. title.SetFont(font)
  588. text = wx.StaticText(self, label="Frequency (MHz)")
  589. self.textFreq = masked.NumCtrl(self, value=freq, fractionWidth=3,
  590. min=F_MIN, max=F_MAX)
  591.  
  592. self.buttonCal = wx.Button(self, label="Calibrate")
  593. if len(parent.devices) == 0:
  594. self.buttonCal.Disable()
  595. self.buttonCal.Bind(wx.EVT_BUTTON, self.on_cal)
  596. self.textResult = wx.StaticText(self)
  597.  
  598. self.buttonOk = wx.Button(self, wx.ID_OK, 'OK')
  599. self.buttonOk.Disable()
  600. self.buttonCancel = wx.Button(self, wx.ID_CANCEL, 'Cancel')
  601.  
  602. self.buttonOk.Bind(wx.EVT_BUTTON, self.on_close)
  603. self.buttonCancel.Bind(wx.EVT_BUTTON, self.on_close)
  604.  
  605. buttons = wx.StdDialogButtonSizer()
  606. buttons.AddButton(self.buttonOk)
  607. buttons.AddButton(self.buttonCancel)
  608. buttons.Realize()
  609.  
  610. sizer = wx.GridBagSizer(10, 10)
  611. sizer.Add(title, pos=(0, 0), span=(1, 2),
  612. flag=wx.ALIGN_CENTER | wx.ALL, border=10)
  613. sizer.Add(text, pos=(1, 0), flag=wx.ALL | wx.EXPAND, border=10)
  614. sizer.Add(self.textFreq, pos=(1, 1), flag=wx.ALL | wx.EXPAND,
  615. border=5)
  616. sizer.Add(self.buttonCal, pos=(2, 0), span=(1, 2),
  617. flag=wx.ALIGN_CENTER | wx.ALL | wx.EXPAND, border=10)
  618. sizer.Add(self.textResult, pos=(3, 0), span=(1, 2),
  619. flag=wx.ALL | wx.EXPAND, border=10)
  620. sizer.Add(buttons, pos=(4, 0), span=(1, 2),
  621. flag=wx.ALL | wx.EXPAND, border=10)
  622.  
  623. self.SetSizerAndFit(sizer)
  624.  
  625. def on_cal(self, _event):
  626. self.buttonCal.Disable()
  627. self.buttonOk.Disable()
  628. self.buttonCancel.Disable()
  629. self.textFreq.Disable()
  630. self.textResult.SetLabel("Calibrating...")
  631. self.callback(CAL_START)
  632.  
  633. def on_close(self, event):
  634. status = [CAL_CANCEL, CAL_OK][event.GetId() == wx.ID_OK]
  635. self.callback(status)
  636. self.EndModal(event.GetId())
  637. return
  638.  
  639. def enable_controls(self):
  640. self.buttonCal.Enable(True)
  641. self.buttonOk.Enable(True)
  642. self.buttonCancel.Enable(True)
  643. self.textFreq.Enable()
  644.  
  645. def set_cal(self, cal):
  646. self.cal = cal
  647. self.enable_controls()
  648. self.textResult.SetLabel("Correction (ppm): {0:.3f}".format(cal))
  649.  
  650. def get_cal(self):
  651. return self.cal
  652.  
  653. def reset_cal(self):
  654. self.set_cal(self.cal)
  655.  
  656. def get_freq(self):
  657. return self.textFreq.GetValue()
  658.  
  659. class DialogOffset(wx.Dialog):
  660. def __init__(self, parent, index, offset):
  661. self.index = index
  662. self.offset = offset
  663. self.band1 = None
  664. self.band2 = None
  665.  
  666. wx.Dialog.__init__(self, parent=parent, title="Scan Offset")
  667.  
  668. figure = matplotlib.figure.Figure(facecolor='white')
  669. self.axes = figure.add_subplot(111)
  670. self.canvas = FigureCanvas(self, -1, figure)
  671.  
  672. textHelp = wx.StaticText(self,
  673. label="Remove the aerial and press refresh, adjust the offset so the shaded areas overlay the flattest parts of the plot.")
  674.  
  675. textFreq = wx.StaticText(self, label="Test frequency (MHz)")
  676. self.spinFreq = wx.SpinCtrl(self)
  677. self.spinFreq.SetRange(F_MIN, F_MAX)
  678. self.spinFreq.SetValue(200)
  679.  
  680. textGain = wx.StaticText(self, label="Test gain (dB)")
  681. self.spinGain = wx.SpinCtrl(self)
  682. self.spinGain.SetRange(-100, 200)
  683. self.spinGain.SetValue(200)
  684.  
  685. refresh = wx.Button(self, wx.ID_ANY, 'Refresh')
  686. self.Bind(wx.EVT_BUTTON, self.on_refresh, refresh)
  687.  
  688. textOffset = wx.StaticText(self, label="Offset (kHz)")
  689. self.spinOffset = wx.SpinCtrl(self)
  690. self.spinOffset.SetRange(0, (SAMPLE_RATE - BANDWIDTH) / 1e3)
  691. self.spinOffset.SetValue(offset)
  692. self.Bind(wx.EVT_SPINCTRL, self.on_spin, self.spinOffset)
  693.  
  694. sizerButtons = wx.StdDialogButtonSizer()
  695. buttonOk = wx.Button(self, wx.ID_OK)
  696. buttonCancel = wx.Button(self, wx.ID_CANCEL)
  697. sizerButtons.AddButton(buttonOk)
  698. sizerButtons.AddButton(buttonCancel)
  699. sizerButtons.Realize()
  700. self.Bind(wx.EVT_BUTTON, self.on_ok, buttonOk)
  701.  
  702. boxSizer1 = wx.BoxSizer(wx.HORIZONTAL)
  703. boxSizer1.Add(textFreq, border=5)
  704. boxSizer1.Add(self.spinFreq, border=5)
  705. boxSizer1.Add(textGain, border=5)
  706. boxSizer1.Add(self.spinGain, border=5)
  707.  
  708. boxSizer2 = wx.BoxSizer(wx.HORIZONTAL)
  709. boxSizer2.Add(textOffset, border=5)
  710. boxSizer2.Add(self.spinOffset, border=5)
  711.  
  712. gridSizer = wx.GridBagSizer(5, 5)
  713. gridSizer.Add(self.canvas, pos=(0, 0), span=(1, 2),
  714. flag=wx.ALIGN_CENTER | wx.ALL)
  715. gridSizer.Add(textHelp, pos=(1, 0), span=(1, 2),
  716. flag=wx.ALIGN_CENTER | wx.ALL)
  717. gridSizer.Add(boxSizer1, pos=(2, 0), span=(1, 2),
  718. flag=wx.ALIGN_CENTER | wx.ALL)
  719. gridSizer.Add(refresh, pos=(3, 0), span=(1, 2),
  720. flag=wx.ALIGN_CENTER | wx.ALL)
  721. gridSizer.Add(boxSizer2, pos=(4, 0), span=(1, 2),
  722. flag=wx.ALIGN_CENTER | wx.ALL)
  723. gridSizer.Add(sizerButtons, pos=(5, 1), span=(1, 1),
  724. flag=wx.ALIGN_RIGHT | wx.ALL)
  725.  
  726. self.SetSizerAndFit(gridSizer)
  727. self.draw_limits()
  728.  
  729. def on_ok(self, _event):
  730.  
  731. self.EndModal(wx.ID_OK)
  732.  
  733. def on_refresh(self, _event):
  734. plot = []
  735.  
  736. dlg = wx.BusyInfo('Please wait...')
  737.  
  738. try:
  739. sdr = rtlsdr.RtlSdr(int(self.index))
  740. sdr.set_sample_rate(SAMPLE_RATE)
  741. sdr.set_center_freq(self.spinFreq.GetValue() * 1e6)
  742. sdr.set_gain(self.spinGain.GetValue())
  743. capture = sdr.read_samples(2 ** 18)
  744. except IOError as error:
  745. dlg.Destroy()
  746. dlg = wx.MessageDialog(self,
  747. 'Capture failed:\n{0}'.format(error.message),
  748. 'Error',
  749. wx.OK | wx.ICON_ERROR)
  750. dlg.ShowModal()
  751. dlg.Destroy()
  752. return
  753.  
  754. powers, freqs = matplotlib.mlab.psd(capture,
  755. NFFT=1024,
  756. Fs=SAMPLE_RATE / 1e6,
  757. window=matplotlib.numpy.hamming(1024))
  758.  
  759. for x, y in itertools.izip(freqs, powers):
  760. x = x * SAMPLE_RATE / 2e6
  761. plot.append((x, y))
  762. plot.sort()
  763. x, y = numpy.transpose(plot)
  764.  
  765. self.axes.clear()
  766. self.band1 = None
  767. self.band2 = None
  768. self.axes.set_xlabel("Frequency (MHz)")
  769. self.axes.set_ylabel('Level (dB)')
  770. self.axes.set_yscale('log')
  771. self.axes.plot(x, y, linewidth=0.4)
  772. self.draw_limits()
  773.  
  774. dlg.Destroy()
  775.  
  776. def on_spin(self, _event):
  777. self.offset = self.spinOffset.GetValue()
  778. self.draw_limits()
  779.  
  780. def draw_limits(self):
  781. limit1 = self.offset / 1e3
  782. limit2 = limit1 + BANDWIDTH / 1e6
  783. if(self.band1 is not None):
  784. self.band1.remove()
  785. if(self.band2 is not None):
  786. self.band2.remove()
  787. self.band1 = self.axes.axvspan(limit1, limit2, color='g', alpha=0.25)
  788. self.band2 = self.axes.axvspan(-limit1, -limit2, color='g', alpha=0.25)
  789. self.canvas.draw()
  790.  
  791. def get_offset(self):
  792. return self.offset
  793.  
  794.  
  795. class DialogPrefs(wx.Dialog):
  796. def __init__(self, parent, devices, settings):
  797. self.settings = settings
  798. self.index = 0
  799.  
  800. wx.Dialog.__init__(self, parent=parent, title="Preferences")
  801.  
  802. title = wx.StaticText(self, label="Select a device")
  803. font = title.GetFont()
  804. font.SetPointSize(font.GetPointSize() + 2)
  805. title.SetFont(font)
  806.  
  807. self.devices = devices
  808. self.gridDev = grid.Grid(self)
  809. self.gridDev.CreateGrid(len(self.devices), 7)
  810. self.gridDev.SetRowLabelSize(0)
  811. self.gridDev.SetColLabelValue(0, "Select")
  812. self.gridDev.SetColLabelValue(1, "Device")
  813. self.gridDev.SetColLabelValue(2, "Index")
  814. self.gridDev.SetColLabelValue(3, "Gain\n(dB)")
  815. self.gridDev.SetColLabelValue(4, "Calibration\n(ppm)")
  816. self.gridDev.SetColLabelValue(5, "LO\n(MHz)")
  817. self.gridDev.SetColLabelValue(6, "Band Offset\n(kHz)")
  818. self.gridDev.SetColFormatFloat(3, -1, 1)
  819. self.gridDev.SetColFormatFloat(4, -1, 3)
  820. self.gridDev.SetColFormatFloat(5, -1, 3)
  821. self.gridDev.SetColFormatFloat(6, -1, 0)
  822.  
  823. attributes = grid.GridCellAttr()
  824. attributes.SetBackgroundColour(self.gridDev.GetLabelBackgroundColour())
  825. self.gridDev.SetColAttr(1, attributes)
  826. self.gridDev.SetColAttr(2, attributes)
  827.  
  828. i = 0
  829. for device in self.devices:
  830. self.gridDev.SetReadOnly(i, 0, True)
  831. self.gridDev.SetReadOnly(i, 1, True)
  832. self.gridDev.SetReadOnly(i, 2, True)
  833. self.gridDev.SetCellRenderer(i, 0, CellRenderer())
  834. self.gridDev.SetCellEditor(i, 3, grid.GridCellFloatEditor(-1, 1))
  835. self.gridDev.SetCellEditor(i, 4, grid.GridCellFloatEditor(-1, 3))
  836. self.gridDev.SetCellEditor(i, 5, grid.GridCellFloatEditor(-1, 3))
  837. self.gridDev.SetCellValue(i, 1, device.name)
  838. self.gridDev.SetCellValue(i, 2, str(i))
  839. self.gridDev.SetCellValue(i, 3, str(device.gain))
  840. self.gridDev.SetCellValue(i, 4, str(device.calibration))
  841. self.gridDev.SetCellValue(i, 5, str(device.lo))
  842. self.gridDev.SetCellValue(i, 6, str(device.offset / 1e3))
  843. i += 1
  844.  
  845. if settings.index > len(self.devices):
  846. settings.index = len(self.devices)
  847. self.select_row(settings.index)
  848.  
  849. self.gridDev.AutoSize()
  850.  
  851. self.Bind(grid.EVT_GRID_CELL_LEFT_CLICK, self.on_click)
  852.  
  853. sizerButtons = wx.StdDialogButtonSizer()
  854. buttonOk = wx.Button(self, wx.ID_OK)
  855. buttonCancel = wx.Button(self, wx.ID_CANCEL)
  856. sizerButtons.AddButton(buttonOk)
  857. sizerButtons.AddButton(buttonCancel)
  858. sizerButtons.Realize()
  859. self.Bind(wx.EVT_BUTTON, self.on_ok, buttonOk)
  860.  
  861. vbox = wx.BoxSizer(wx.VERTICAL)
  862. vbox.Add(title, 0, wx.ALL | wx.EXPAND, 10)
  863. vbox.Add(self.gridDev, 0, wx.ALL | wx.EXPAND, 10)
  864. vbox.Add(sizerButtons, 0, wx.ALL | wx.EXPAND, 10)
  865.  
  866. self.SetSizerAndFit(vbox)
  867.  
  868. def on_click(self, event):
  869. col = event.GetCol()
  870. index = event.GetRow()
  871. if(col == 0):
  872. self.index = event.GetRow()
  873. self.select_row(index)
  874. elif(col == 6):
  875. dlg = DialogOffset(self, index,
  876. float(self.gridDev.GetCellValue(index, 6)))
  877. if dlg.ShowModal() == wx.ID_OK:
  878. self.gridDev.SetCellValue(index, 6, str(dlg.get_offset()))
  879. dlg.Destroy()
  880. event.Skip()
  881.  
  882. def on_ok(self, _event):
  883. for i in range(0, self.gridDev.GetNumberRows()):
  884. self.devices[i].gain = float(self.gridDev.GetCellValue(i, 3))
  885. self.devices[i].calibration = float(self.gridDev.GetCellValue(i, 4))
  886. self.devices[i].lo = float(self.gridDev.GetCellValue(i, 5))
  887. self.devices[i].offset = float(self.gridDev.GetCellValue(i, 6)) * 1e3
  888.  
  889. self.EndModal(wx.ID_OK)
  890.  
  891. def select_row(self, index):
  892. for i in range(0, self.gridDev.GetNumberRows()):
  893. tick = "0"
  894. if i == index:
  895. tick = "1"
  896. self.gridDev.SetCellValue(i, 0, tick)
  897.  
  898. def get_index(self):
  899. return self.index
  900.  
  901. def get_devices(self):
  902. return self.devices
  903.  
  904.  
  905. class DialogSaveWarn(wx.Dialog):
  906. def __init__(self, parent, warnType):
  907. self.code = -1
  908.  
  909. wx.Dialog.__init__(self, parent=parent, title="Warning",
  910. style=wx.ICON_EXCLAMATION)
  911.  
  912. prompt = ["scanning again", "opening a file", "exiting"][warnType]
  913. text = wx.StaticText(self, label="Save plot before {0}?".format(prompt))
  914. icon = wx.StaticBitmap(self, wx.ID_ANY,
  915. wx.ArtProvider.GetBitmap(wx.ART_INFORMATION,
  916. wx.ART_MESSAGE_BOX))
  917.  
  918. tbox = wx.BoxSizer(wx.HORIZONTAL)
  919. tbox.Add(text)
  920.  
  921. hbox = wx.BoxSizer(wx.HORIZONTAL)
  922. hbox.Add(icon, 0, wx.ALL, 5)
  923. hbox.Add(tbox, 0, wx.ALL, 5)
  924.  
  925. buttonYes = wx.Button(self, wx.ID_YES, 'Yes')
  926. buttonNo = wx.Button(self, wx.ID_NO, 'No')
  927. buttonCancel = wx.Button(self, wx.ID_CANCEL, 'Cancel')
  928.  
  929. buttonYes.Bind(wx.EVT_BUTTON, self.on_close)
  930. buttonNo.Bind(wx.EVT_BUTTON, self.on_close)
  931.  
  932. buttons = wx.StdDialogButtonSizer()
  933. buttons.AddButton(buttonYes)
  934. buttons.AddButton(buttonNo)
  935. buttons.AddButton(buttonCancel)
  936. buttons.Realize()
  937.  
  938. vbox = wx.BoxSizer(wx.VERTICAL)
  939. vbox.Add(hbox, 1, wx.ALL | wx.EXPAND, 10)
  940. vbox.Add(buttons, 1, wx.ALL | wx.EXPAND, 10)
  941.  
  942. self.SetSizerAndFit(vbox)
  943.  
  944. def on_close(self, event):
  945. self.EndModal(event.GetId())
  946. return
  947.  
  948. def get_code(self):
  949. return self.code
  950.  
  951. class DialogRange(wx.Dialog):
  952. def __init__(self, parent, main):
  953. self.main = main
  954.  
  955. wx.Dialog.__init__(self, parent=parent, title="Plot Range")
  956.  
  957. self.checkAuto = wx.CheckBox(self, wx.ID_ANY, "Auto Range")
  958. self.checkAuto.SetValue(self.main.settings.yAuto)
  959. self.Bind(wx.EVT_CHECKBOX, self.on_auto, self.checkAuto)
  960.  
  961. textMax = wx.StaticText(self, label="Maximum (dB)")
  962. self.yMax = masked.NumCtrl(self, value=int(self.main.settings.yMax),
  963. fractionWidth=0, min=-100, max=20)
  964. textMin = wx.StaticText(self, label="Minimum (dB)")
  965. self.yMin = masked.NumCtrl(self, value=int(self.main.settings.yMin),
  966. fractionWidth=0, min=-100, max=20)
  967. self.set_enabled(not self.main.settings.yAuto)
  968.  
  969. sizerButtons = wx.StdDialogButtonSizer()
  970. buttonOk = wx.Button(self, wx.ID_OK)
  971. buttonCancel = wx.Button(self, wx.ID_CANCEL)
  972. sizerButtons.AddButton(buttonOk)
  973. sizerButtons.AddButton(buttonCancel)
  974. sizerButtons.Realize()
  975. self.Bind(wx.EVT_BUTTON, self.on_ok, buttonOk)
  976.  
  977.  
  978. sizer = wx.GridBagSizer(10, 10)
  979. sizer.Add(self.checkAuto, pos=(0, 0), span=(1, 1),
  980. flag=wx.ALIGN_CENTER | wx.ALL, border=10)
  981. sizer.Add(textMax, pos=(1, 0), span=(1, 1),
  982. flag=wx.ALIGN_CENTER | wx.ALL, border=10)
  983. sizer.Add(self.yMax, pos=(1, 1), span=(1, 1),
  984. flag=wx.ALIGN_CENTER | wx.ALL, border=10)
  985. sizer.Add(textMin, pos=(2, 0), span=(1, 1),
  986. flag=wx.ALIGN_CENTER | wx.ALL, border=10)
  987. sizer.Add(self.yMin, pos=(2, 1), span=(1, 1),
  988. flag=wx.ALIGN_CENTER | wx.ALL, border=10)
  989. sizer.Add(sizerButtons, pos=(3, 0), span=(1, 2),
  990. flag=wx.ALIGN_CENTER | wx.ALL, border=10)
  991.  
  992. self.SetSizerAndFit(sizer)
  993.  
  994. def on_auto(self, _event):
  995. self.set_enabled(not self.checkAuto.GetValue())
  996.  
  997. def on_ok(self, _event):
  998. self.main.settings.yAuto = self.checkAuto.GetValue()
  999. self.main.settings.yMin = self.yMin.GetValue()
  1000. self.main.settings.yMax = self.yMax.GetValue()
  1001. self.EndModal(wx.ID_OK)
  1002.  
  1003. def set_enabled(self, isEnabled):
  1004. self.yMax.Enable(isEnabled)
  1005. self.yMin.Enable(isEnabled)
  1006.  
  1007.  
  1008. class FrameMain(wx.Frame):
  1009. def __init__(self, title):
  1010.  
  1011. self.update = False
  1012. self.grid = False
  1013.  
  1014. self.thread = None
  1015.  
  1016. self.dlgCal = None
  1017.  
  1018. self.menuOpen = None
  1019. self.menuSave = None
  1020. self.menuExport = None
  1021. self.menuStart = None
  1022. self.menuStop = None
  1023. self.menuPref = None
  1024. self.menuCompare = None
  1025. self.menuCal = None
  1026.  
  1027. self.panel = None
  1028. self.graph = None
  1029. self.canvas = None
  1030. self.buttonStart = None
  1031. self.buttonStop = None
  1032. self.choiceDwell = None
  1033. self.choiceNfft = None
  1034. self.spinCtrlStart = None
  1035. self.spinCtrlStop = None
  1036. self.checkUpdate = None
  1037. self.checkGrid = None
  1038.  
  1039. self.filename = ""
  1040. self.dirname = "."
  1041.  
  1042. self.spectrum = {}
  1043. self.isSaved = True
  1044.  
  1045. self.settings = Settings()
  1046. self.devices = self.get_devices()
  1047. self.oldCal = 0
  1048.  
  1049. displaySize = wx.DisplaySize()
  1050. wx.Frame.__init__(self, None, title=title, size=(displaySize[0] / 1.5,
  1051. displaySize[1] / 2))
  1052.  
  1053. self.Bind(wx.EVT_SIZE, self.on_size)
  1054. self.Bind(wx.EVT_CLOSE, self.on_exit)
  1055.  
  1056. self.status = self.CreateStatusBar()
  1057. self.status.SetFieldsCount(2)
  1058. self.statusProgress = wx.Gauge(self.status, -1,
  1059. style=wx.GA_HORIZONTAL | wx.GA_SMOOTH)
  1060. self.statusProgress.Hide()
  1061.  
  1062. self.create_widgets()
  1063. self.create_menu()
  1064. self.set_controls(True)
  1065. self.menuSave.Enable(False)
  1066. self.menuExport.Enable(False)
  1067. self.Show()
  1068.  
  1069. size = self.panel.GetSize()
  1070. size[1] += displaySize[1] / 4
  1071. self.SetMinSize(size)
  1072.  
  1073. thread_event_handler(self, EVT_THREAD_STATUS, self.on_thread_status)
  1074.  
  1075. self.SetDropTarget(DropTarget(self))
  1076.  
  1077. def create_widgets(self):
  1078. panel = wx.Panel(self)
  1079.  
  1080. self.panel = wx.Panel(panel)
  1081. self.graph = PanelGraph(panel, self)
  1082.  
  1083. self.buttonStart = wx.Button(self.panel, wx.ID_ANY, 'Start')
  1084. self.buttonStop = wx.Button(self.panel, wx.ID_ANY, 'Stop')
  1085. self.buttonStart.SetToolTip(wx.ToolTip('Start scan'))
  1086. self.buttonStop.SetToolTip(wx.ToolTip('Stop scan'))
  1087. self.Bind(wx.EVT_BUTTON, self.on_start, self.buttonStart)
  1088. self.Bind(wx.EVT_BUTTON, self.on_stop, self.buttonStop)
  1089.  
  1090. textRange = wx.StaticText(self.panel, label="Range (MHz)",
  1091. style=wx.ALIGN_CENTER)
  1092. textStart = wx.StaticText(self.panel, label="Start")
  1093. textStop = wx.StaticText(self.panel, label="Stop")
  1094.  
  1095. self.spinCtrlStart = wx.SpinCtrl(self.panel)
  1096. self.spinCtrlStop = wx.SpinCtrl(self.panel)
  1097. self.spinCtrlStart.SetToolTip(wx.ToolTip('Start frequency'))
  1098. self.spinCtrlStop.SetToolTip(wx.ToolTip('Stop frequency'))
  1099. self.spinCtrlStart.SetRange(F_MIN, F_MAX - 1)
  1100. self.spinCtrlStop.SetRange(F_MIN + 1, F_MAX)
  1101. self.set_range()
  1102. self.Bind(wx.EVT_SPINCTRL, self.on_spin, self.spinCtrlStart)
  1103. self.Bind(wx.EVT_SPINCTRL, self.on_spin, self.spinCtrlStop)
  1104.  
  1105. textDwell = wx.StaticText(self.panel, label="Dwell")
  1106. self.choiceDwell = wx.Choice(self.panel, choices=DWELL[::2])
  1107. self.choiceDwell.SetToolTip(wx.ToolTip('Scan time per step'))
  1108. self.choiceDwell.SetSelection(DWELL[1::2].index(self.settings.dwell))
  1109.  
  1110. textNfft = wx.StaticText(self.panel, label="FFT size")
  1111. self.choiceNfft = wx.Choice(self.panel, choices=map(str, NFFT))
  1112. self.choiceNfft.SetToolTip(wx.ToolTip('Higher values for greater precision'))
  1113. self.choiceNfft.SetSelection(NFFT.index(self.settings.nfft))
  1114.  
  1115. self.checkUpdate = wx.CheckBox(self.panel, wx.ID_ANY,
  1116. "Continuous update")
  1117. self.checkUpdate.SetToolTip(wx.ToolTip('Very slow, not recommended'))
  1118. self.checkUpdate.SetValue(self.update)
  1119. self.Bind(wx.EVT_CHECKBOX, self.on_check_update, self.checkUpdate)
  1120.  
  1121. self.checkGrid = wx.CheckBox(self.panel, wx.ID_ANY, "Grid")
  1122. self.checkGrid.SetToolTip(wx.ToolTip('Draw grid'))
  1123. self.checkGrid.SetValue(self.grid)
  1124. self.Bind(wx.EVT_CHECKBOX, self.on_check_grid, self.checkGrid)
  1125.  
  1126. grid = wx.GridBagSizer(5, 5)
  1127.  
  1128. grid.Add(self.buttonStart, pos=(0, 0), span=(2, 1),
  1129. flag=wx.ALIGN_CENTER)
  1130. grid.Add(self.buttonStop, pos=(0, 1), span=(2, 1),
  1131. flag=wx.ALIGN_CENTER)
  1132.  
  1133. grid.Add((20, 1), pos=(0, 2))
  1134.  
  1135. grid.Add(textRange, pos=(0, 3), span=(1, 4), flag=wx.ALIGN_CENTER)
  1136. grid.Add(textStart, pos=(1, 3), flag=wx.ALIGN_CENTER)
  1137. grid.Add(self.spinCtrlStart, pos=(1, 4))
  1138. grid.Add(textStop, pos=(1, 5), flag=wx.ALIGN_CENTER)
  1139. grid.Add(self.spinCtrlStop, pos=(1, 6))
  1140.  
  1141. grid.Add((20, 1), pos=(0, 7))
  1142.  
  1143. grid.Add(textDwell, pos=(0, 8), flag=wx.ALIGN_CENTER)
  1144. grid.Add(self.choiceDwell, pos=(1, 8), flag=wx.ALIGN_CENTER)
  1145.  
  1146. grid.Add(textNfft, pos=(0, 9), flag=wx.ALIGN_CENTER)
  1147. grid.Add(self.choiceNfft, pos=(1, 9), flag=wx.ALIGN_CENTER)
  1148.  
  1149. grid.Add((20, 1), pos=(0, 10))
  1150.  
  1151. grid.Add(self.checkUpdate, pos=(0, 11))
  1152. grid.Add(self.checkGrid, pos=(1, 11))
  1153.  
  1154. self.panel.SetSizer(grid)
  1155.  
  1156. sizer = wx.BoxSizer(wx.VERTICAL)
  1157. sizer.Add(self.graph, 1, wx.EXPAND)
  1158. sizer.Add(self.panel, 0, wx.ALIGN_CENTER)
  1159. panel.SetSizer(sizer)
  1160.  
  1161. def create_menu(self):
  1162. menuFile = wx.Menu()
  1163. self.menuOpen = menuFile.Append(wx.ID_OPEN, "&Open...", "Open plot")
  1164. self.menuSave = menuFile.Append(wx.ID_SAVE, "&Save As...",
  1165. "Save plot")
  1166. self.menuExport = menuFile.Append(wx.ID_ANY, "&Export...",
  1167. "Export plot")
  1168. menuExit = menuFile.Append(wx.ID_EXIT, "E&xit", "Exit the program")
  1169.  
  1170. menuScan = wx.Menu()
  1171. self.menuStart = menuScan.Append(wx.ID_ANY, "&Start", "Start scan")
  1172. self.menuStop = menuScan.Append(wx.ID_ANY, "S&top", "Stop scan")
  1173.  
  1174. menuView = wx.Menu()
  1175. self.menuPref = menuView.Append(wx.ID_ANY, "&Preferences...",
  1176. "Preferences")
  1177.  
  1178. menuTools = wx.Menu()
  1179. self.menuCompare = menuTools.Append(wx.ID_ANY, "&Compare...",
  1180. "Compare plots")
  1181. self.menuCal = menuTools.Append(wx.ID_ANY, "&Auto Calibration...",
  1182. "Automatically calibrate to a known frequency")
  1183.  
  1184. menuHelp = wx.Menu()
  1185. menuAbout = menuHelp.Append(wx.ID_ABOUT, "&About...",
  1186. "Information about this program")
  1187.  
  1188. menuBar = wx.MenuBar()
  1189. menuBar.Append(menuFile, "&File")
  1190. menuBar.Append(menuScan, "&Scan")
  1191. menuBar.Append(menuView, "&View")
  1192. menuBar.Append(menuTools, "&Tools")
  1193. menuBar.Append(menuHelp, "&Help")
  1194. self.SetMenuBar(menuBar)
  1195.  
  1196. self.Bind(wx.EVT_MENU, self.on_open, self.menuOpen)
  1197. self.Bind(wx.EVT_MENU, self.on_save, self.menuSave)
  1198. self.Bind(wx.EVT_MENU, self.on_export, self.menuExport)
  1199. self.Bind(wx.EVT_MENU, self.on_exit, menuExit)
  1200. self.Bind(wx.EVT_MENU, self.on_start, self.menuStart)
  1201. self.Bind(wx.EVT_MENU, self.on_stop, self.menuStop)
  1202. self.Bind(wx.EVT_MENU, self.on_pref, self.menuPref)
  1203. self.Bind(wx.EVT_MENU, self.on_compare, self.menuCompare)
  1204. self.Bind(wx.EVT_MENU, self.on_cal, self.menuCal)
  1205. self.Bind(wx.EVT_MENU, self.on_about, menuAbout)
  1206.  
  1207. def on_open(self, _event):
  1208. if self.save_warn(WARN_OPEN):
  1209. return
  1210. dlg = wx.FileDialog(self, "Open a scan", self.dirname, self.filename,
  1211. FILE_RFS, wx.OPEN)
  1212. if dlg.ShowModal() == wx.ID_OK:
  1213. self.open(dlg.GetDirectory(), dlg.GetFilename())
  1214. dlg.Destroy()
  1215.  
  1216. def on_save(self, _event):
  1217. dlg = wx.FileDialog(self, "Save a scan", self.dirname,
  1218. self.filename + ".rfs", FILE_RFS,
  1219. wx.SAVE | wx.OVERWRITE_PROMPT)
  1220. if dlg.ShowModal() == wx.ID_OK:
  1221. self.status.SetStatusText("Saving", 0)
  1222. self.filename = dlg.GetFilename()
  1223. self.dirname = dlg.GetDirectory()
  1224. handle = open(os.path.join(self.dirname, self.filename), 'wb')
  1225. cPickle.dump(FILE_HEADER, handle)
  1226. cPickle.dump(FILE_VERSION, handle)
  1227. cPickle.dump(self.settings.start, handle)
  1228. cPickle.dump(self.settings.stop, handle)
  1229. cPickle.dump(self.spectrum, handle)
  1230. handle.close()
  1231. self.isSaved = True
  1232. self.status.SetStatusText("Finished", 0)
  1233. dlg.Destroy()
  1234.  
  1235. def on_export(self, _event):
  1236. dlg = wx.FileDialog(self, "Export a scan", self.dirname,
  1237. self.filename + ".csv", FILE_CSV, wx.SAVE)
  1238. if dlg.ShowModal() == wx.ID_OK:
  1239. self.status.SetStatusText("Exporting", 0)
  1240. self.filename = dlg.GetFilename()
  1241. self.dirname = dlg.GetDirectory()
  1242. handle = open(os.path.join(self.dirname, self.filename), 'wb')
  1243. handle.write("Frequency (MHz),Level (dB)\n")
  1244. for freq, pwr in self.spectrum.iteritems():
  1245. handle.write("{0},{1}\n".format(freq, pwr))
  1246. handle.close()
  1247. self.status.SetStatusText("Finished", 0)
  1248. dlg.Destroy()
  1249.  
  1250. def on_exit(self, _event):
  1251. self.Unbind(wx.EVT_CLOSE)
  1252. if self.save_warn(WARN_EXIT):
  1253. self.Bind(wx.EVT_CLOSE, self.on_exit)
  1254. return
  1255. self.get_range()
  1256. self.settings.dwell = DWELL[1::2][self.choiceDwell.GetSelection()]
  1257. self.settings.nfft = NFFT[self.choiceNfft.GetSelection()]
  1258. self.settings.devices = self.devices
  1259. self.settings.save()
  1260. self.Close(True)
  1261.  
  1262. def on_pref(self, _event):
  1263. self.devices = self.refresh_devices()
  1264. dlg = DialogPrefs(self, self.devices, self.settings)
  1265. if dlg.ShowModal() == wx.ID_OK:
  1266. self.devices = dlg.get_devices()
  1267. self.settings.index = dlg.get_index()
  1268. dlg.Destroy()
  1269.  
  1270. def on_compare(self, _event):
  1271. dlg = DialogCompare(self, self.dirname, self.filename)
  1272. dlg.ShowModal()
  1273. dlg.Destroy()
  1274.  
  1275. def on_cal(self, _event):
  1276. self.dlgCal = DialogAutoCal(self, self.settings.calFreq, self.auto_cal)
  1277. self.dlgCal.ShowModal()
  1278.  
  1279. def on_about(self, _event):
  1280. dlg = wx.MessageDialog(self, "RTLSDR Scanner",
  1281. "A tool for scanning frequency ranges with an RTL-SDR compatible USB dongle",
  1282. wx.OK)
  1283. dlg.ShowModal()
  1284. dlg.Destroy()
  1285.  
  1286. def on_spin(self, event):
  1287. control = event.GetEventObject()
  1288. if control == self.spinCtrlStart:
  1289. self.spinCtrlStop.SetRange(self.spinCtrlStart.GetValue() + 1,
  1290. F_MAX)
  1291.  
  1292. def on_start(self, _event):
  1293. self.scan_start(False)
  1294.  
  1295. def on_stop(self, _event):
  1296. if self.thread:
  1297. self.status.SetStatusText("Stopping", 0)
  1298. self.thread.abort()
  1299.  
  1300. def on_check_update(self, _event):
  1301. self.update = self.checkUpdate.GetValue()
  1302.  
  1303. def on_check_grid(self, _event):
  1304. self.grid = self.checkGrid.GetValue()
  1305. self.draw_plot()
  1306.  
  1307. def on_thread_status(self, event):
  1308. status = event.data.get_status()
  1309. freq = event.data.get_freq()
  1310. data = event.data.get_data()
  1311. if status == THREAD_STATUS_STARTING:
  1312. self.status.SetStatusText("Starting", 0)
  1313. elif status == THREAD_STATUS_SCAN:
  1314. self.status.SetStatusText("Scanning", 0)
  1315. self.statusProgress.Show()
  1316. self.statusProgress.SetValue(data)
  1317. elif status == THREAD_STATUS_STOPPED:
  1318. self.statusProgress.Hide()
  1319. self.status.SetStatusText("Stopped", 0)
  1320. self.thread = None
  1321. self.set_controls(True)
  1322. self.draw_plot()
  1323. elif status == THREAD_STATUS_FINISHED:
  1324. self.statusProgress.Hide()
  1325. self.status.SetStatusText("Finished", 0)
  1326. self.thread = None
  1327. self.set_controls(True)
  1328. self.draw_plot()
  1329. if data:
  1330. self.auto_cal(CAL_DONE)
  1331. elif status == THREAD_STATUS_ERROR:
  1332. self.statusProgress.Hide()
  1333. self.status.SetStatusText("Dongle error: {0}".format(data), 0)
  1334. self.thread = None
  1335. self.set_controls(True)
  1336. if self.dlgCal is not None:
  1337. self.dlgCal.Destroy()
  1338. self.dlgCal = None
  1339. elif status == THREAD_STATUS_DATA:
  1340. self.isSaved = False
  1341. fftChoice = self.choiceNfft.GetSelection()
  1342. nfft = NFFT[fftChoice]
  1343. ThreadProcess(self, freq, data, self.settings, self.devices, nfft)
  1344. elif status == THREAD_STATUS_PROCESSED:
  1345. self.update_scan(freq, data)
  1346. if self.update:
  1347. self.draw_plot()
  1348.  
  1349. def on_size(self, event):
  1350. rect = self.status.GetFieldRect(1)
  1351. self.statusProgress.SetPosition((rect.x + 10, rect.y + 2))
  1352. self.statusProgress.SetSize((rect.width - 20, rect.height - 4))
  1353. event.Skip()
  1354.  
  1355. def open(self, dirname, filename):
  1356. self.filename = filename
  1357. self.dirname = dirname
  1358. self.status.SetStatusText("Opening: {0}".format(filename), 0)
  1359.  
  1360. start, stop, spectrum = open_plot(dirname, filename)
  1361.  
  1362. if len(spectrum) > 0:
  1363. self.settings.start = start
  1364. self.settings.stop = stop
  1365. self.spectrum = spectrum
  1366. self.isSaved = True
  1367. self.set_range()
  1368. self.set_controls(True)
  1369. self.draw_plot()
  1370. self.status.SetStatusText("Finished", 0)
  1371. else:
  1372. self.status.SetStatusText("Open failed", 0)
  1373.  
  1374. def auto_cal(self, status):
  1375. freq = self.dlgCal.get_freq()
  1376. if self.dlgCal is not None:
  1377. if status == CAL_START:
  1378. self.spinCtrlStart.SetValue(freq - 1)
  1379. self.spinCtrlStop.SetValue(freq + 1)
  1380. self.oldCal = self.devices[self.settings.index].calibration
  1381. self.devices[self.settings.index].calibration = 0
  1382. if not self.scan_start(True):
  1383. self.dlgCal.reset_cal()
  1384. elif status == CAL_DONE:
  1385. ppm = self.calc_ppm(freq)
  1386. self.dlgCal.set_cal(ppm)
  1387. elif status == CAL_OK:
  1388. self.devices[self.settings.index].calibration = self.dlgCal.get_cal()
  1389. self.settings.calFreq = freq
  1390. self.dlgCal = None
  1391. elif status == CAL_CANCEL:
  1392. self.dlgCal = None
  1393. if len(self.devices) > 0:
  1394. self.devices[self.settings.index].calibration = self.oldCal
  1395.  
  1396. def calc_ppm(self, freq):
  1397. spectrum = self.spectrum.copy()
  1398. for x, y in spectrum.iteritems():
  1399. spectrum[x] = (((x - freq) * (x - freq)) + 1) * y
  1400.  
  1401. peak = max(spectrum, key=spectrum.get)
  1402.  
  1403. return ((freq - peak) / freq) * 1e6
  1404.  
  1405. def scan_start(self, isCal):
  1406. if self.save_warn(WARN_SCAN):
  1407. return False
  1408.  
  1409. self.devices = self.refresh_devices()
  1410. if(len(self.devices) == 0):
  1411. wx.MessageBox('No devices found',
  1412. 'Error', wx.OK | wx.ICON_ERROR)
  1413. return
  1414.  
  1415. self.get_range()
  1416. if self.settings.start >= self.settings.stop:
  1417. wx.MessageBox('Stop frequency must be greater that start',
  1418. 'Warning', wx.OK | wx.ICON_WARNING)
  1419. return
  1420.  
  1421. choice = self.choiceDwell.GetSelection()
  1422.  
  1423. if not self.thread or not self.thread.is_alive():
  1424.  
  1425. self.set_controls(False)
  1426. dwell = DWELL[1::2][choice]
  1427. samples = dwell * SAMPLE_RATE
  1428. samples = next_2_to_pow(int(samples))
  1429. self.spectrum = {}
  1430. self.status.SetStatusText("", 1)
  1431. self.thread = ThreadScan(self, self.settings, self.devices,
  1432. samples, isCal)
  1433. self.filename = "Scan {0:.1f}-{1:.1f}MHz".format(self.settings.start,
  1434. self.settings.stop)
  1435.  
  1436. return True
  1437.  
  1438. def update_scan(self, freqCentre, scan):
  1439. offset = self.settings.devices[self.settings.index].offset
  1440. upperStart = freqCentre + offset
  1441. upperEnd = freqCentre + offset + BANDWIDTH / 2
  1442. lowerStart = freqCentre - offset - BANDWIDTH / 2
  1443. lowerEnd = freqCentre - offset
  1444.  
  1445. for freq in scan:
  1446. if self.settings.start < freq < self.settings.stop:
  1447. power = 10 * math.log10(scan[freq])
  1448. if upperStart < freq * 1e6 < upperEnd:
  1449. self.spectrum[freq] = power
  1450. if lowerStart < freq * 1e6 < lowerEnd:
  1451. if freq in self.spectrum:
  1452. self.spectrum[freq] = (self.spectrum[freq] + power) / 2
  1453. else:
  1454. self.spectrum[freq] = power
  1455.  
  1456. def set_controls(self, state):
  1457. self.spinCtrlStart.Enable(state)
  1458. self.spinCtrlStop.Enable(state)
  1459. self.choiceDwell.Enable(state)
  1460. self.choiceNfft.Enable(state)
  1461. self.buttonStart.Enable(state)
  1462. self.buttonStop.Enable(not state)
  1463. self.menuOpen.Enable(state)
  1464. self.menuSave.Enable(state)
  1465. self.menuExport.Enable(state)
  1466. self.menuStart.Enable(state)
  1467. self.menuStop.Enable(not state)
  1468. self.menuPref.Enable(state)
  1469. self.menuCal.Enable(state)
  1470.  
  1471. def draw_plot(self):
  1472. axes = self.graph.get_axes()
  1473. gain = self.settings.devices[self.settings.index].gain
  1474. if len(self.spectrum) > 0:
  1475. freqs, powers = split_spectrum(self.spectrum)
  1476. axes.clear()
  1477. axes.set_title("Frequency Scan\n{0} - {1} MHz, gain = {2}".format(self.settings.start,
  1478. self.settings.stop, gain))
  1479. axes.set_xlabel("Frequency (MHz)")
  1480. axes.set_ylabel('Level (dB)')
  1481. axes.plot(freqs, powers, linewidth=0.4)
  1482. self.graph.get_toolbar().update()
  1483. axes.grid(self.grid)
  1484. if(self.settings.yAuto):
  1485. axes.set_ylim(auto=True)
  1486. self.settings.yMin, self.settings.yMax = axes.get_ylim()
  1487. else:
  1488. axes.set_ylim(self.settings.yMin, self.settings.yMax)
  1489. self.graph.get_canvas().draw()
  1490.  
  1491. def save_warn(self, warnType):
  1492. if not self.isSaved:
  1493. dlg = DialogSaveWarn(self, warnType)
  1494. code = dlg.ShowModal()
  1495. if code == wx.ID_YES:
  1496. self.on_save(None)
  1497. if self.isSaved:
  1498. return False
  1499. else:
  1500. return True
  1501. elif code == wx.ID_NO:
  1502. return False
  1503. else:
  1504. return True
  1505.  
  1506. return False
  1507.  
  1508. def set_range(self):
  1509. self.spinCtrlStart.SetValue(self.settings.start)
  1510. self.spinCtrlStop.SetValue(self.settings.stop)
  1511.  
  1512. def get_range(self):
  1513. self.settings.start = self.spinCtrlStart.GetValue()
  1514. self.settings.stop = self.spinCtrlStop.GetValue()
  1515.  
  1516. def refresh_devices(self):
  1517. self.settings.devices = self.devices
  1518. self.settings.save()
  1519. return self.get_devices()
  1520.  
  1521. def get_devices(self):
  1522. devices = []
  1523. count = rtlsdr.librtlsdr.rtlsdr_get_device_count()
  1524.  
  1525. for dev in range(0, count):
  1526. device = Device()
  1527. device.index = dev
  1528. device.name = format_device_name(rtlsdr.librtlsdr.rtlsdr_get_device_name(dev))
  1529. device.calibration = 0.0
  1530. device.lo = 0.0
  1531. for conf in self.settings.devices:
  1532. # TODO: better matching than just name?
  1533. if device.name == conf.name:
  1534. device.gain = conf.gain
  1535. device.calibration = conf.calibration
  1536. device.lo = conf.lo
  1537. device.offset = conf.offset
  1538. break
  1539.  
  1540. devices.append(device)
  1541.  
  1542. return devices
  1543.  
  1544. def format_device_name(name):
  1545. remove = ["/", "\\"]
  1546. for char in remove:
  1547. name = name.replace(char, " ")
  1548.  
  1549. return name
  1550.  
  1551. def thread_event_handler(win, event, function):
  1552. win.Connect(-1, -1, event, function)
  1553.  
  1554.  
  1555. def next_2_to_pow(val):
  1556. val -= 1
  1557. val |= val >> 1
  1558. val |= val >> 2
  1559. val |= val >> 4
  1560. val |= val >> 8
  1561. val |= val >> 16
  1562. return val + 1
  1563.  
  1564.  
  1565. def arguments():
  1566. parser = argparse.ArgumentParser()
  1567. parser.add_argument("file", help="plot filename", nargs='?')
  1568. args = parser.parse_args()
  1569.  
  1570. filename = None
  1571. directory = None
  1572. if args.file != None:
  1573. directory, filename = os.path.split(args.file)
  1574.  
  1575. return directory, filename
  1576.  
  1577. def open_plot(dirname, filename):
  1578. try:
  1579. handle = open(os.path.join(dirname, filename), 'rb')
  1580. header = cPickle.load(handle)
  1581. if header != FILE_HEADER:
  1582. wx.MessageBox('Invalid or corrupted file', 'Warning',
  1583. wx.OK | wx.ICON_WARNING)
  1584. return
  1585. _version = cPickle.load(handle)
  1586. start = cPickle.load(handle)
  1587. stop = cPickle.load(handle)
  1588. spectrum = cPickle.load(handle)
  1589. except:
  1590. wx.MessageBox('File could not be opened', 'Warning',
  1591. wx.OK | wx.ICON_WARNING)
  1592.  
  1593. return start, stop, spectrum
  1594.  
  1595. def split_spectrum(spectrum):
  1596. freqs = spectrum.keys()
  1597. freqs.sort()
  1598. powers = map(spectrum.get, freqs)
  1599.  
  1600. return freqs, powers
  1601.  
  1602. if __name__ == '__main__':
  1603. app = wx.App(False)
  1604. frame = FrameMain("RTLSDR Scanner")
  1605. directory, filename = arguments()
  1606.  
  1607. if filename != None:
  1608. frame.open(directory, filename)
  1609. app.MainLoop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement