Ubertooth Zero solderpad
ubertooth-zero / host / specan_ui / ubertooth-specan-ui
#!/usr/bin/env python
#
# Copyright 2011 Jared Boone
#
# This file is part of Project Ubertooth.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
import sys
import threading
import numpy
import usb.core
from PySide import QtCore, QtGui
from PySide.QtCore import Qt, QPointF, QLineF
from specan import Ubertooth
class SpecanThread(threading.Thread):
def __init__(self, device, low_frequency, high_frequency, new_frame_callback):
threading.Thread.__init__(self)
self.daemon = True
self._device = device
self._low_frequency = low_frequency
self._high_frequency = high_frequency
self._new_frame_callback = new_frame_callback
self._stop = False
self._stopped = False
def run(self):
frame_source = self._device.specan(self._low_frequency, self._high_frequency)
for frequency_axis, rssi_values in frame_source:
self._new_frame_callback(numpy.copy(frequency_axis), numpy.copy(rssi_values))
if self._stop:
break
def stop(self):
self._stop = True
self.join(3.0)
self._stopped = True
class RenderArea(QtGui.QWidget):
def __init__(self, device, parent=None):
QtGui.QWidget.__init__(self, parent)
self._graph = None
self._reticle = None
self._device = device
self._frame = None
self._persisted_frames = None
self._persisted_frames_depth = 350
self._path_max = None
self._low_frequency = 2.400e9
self._high_frequency = 2.483e9
self._frequency_step = 1e6
self._high_dbm = 0.0
self._low_dbm = -100.0
self._thread = SpecanThread(self._device,
self._low_frequency,
self._high_frequency,
self._new_frame)
self._thread.start()
def stop_thread(self):
self._thread.stop()
def _new_graph(self):
self._graph = QtGui.QPixmap(self.width(), self.height())
self._graph.fill(Qt.black)
def _new_reticle(self):
self._reticle = QtGui.QPixmap(self.width(), self.height())
self._reticle.fill(Qt.transparent)
def _new_persisted_frames(self, frequency_bins):
self._persisted_frames = numpy.empty((self._persisted_frames_depth, frequency_bins))
self._persisted_frames.fill(-128 + -54)
self._persisted_frames_next_index = 0
def minimumSizeHint(self):
x_points = round((self._high_frequency - self._low_frequency) / self._frequency_step)
y_points = round(self._high_dbm - self._low_dbm)
return QtCore.QSize(x_points * 4, y_points * 1)
def _new_frame(self, frequency_axis, rssi_values):
self._frame = (frequency_axis, rssi_values)
if self._persisted_frames is None:
self._new_persisted_frames(len(frequency_axis))
self._persisted_frames[self._persisted_frames_next_index] = rssi_values
self._persisted_frames_next_index = (self._persisted_frames_next_index + 1) % self._persisted_frames.shape[0]
self.update()
def _draw_graph(self):
if self._graph is None:
self._new_graph()
elif self._graph.size() != self.size():
self._new_graph()
painter = QtGui.QPainter(self._graph)
try:
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.fillRect(0, 0, self._graph.width(), self._graph.height(), QtGui.QColor(0, 0, 0, 10))
if self._frame:
frequency_axis, rssi_values = self._frame
path_now = QtGui.QPainterPath()
path_max = QtGui.QPainterPath()
bins = range(len(frequency_axis))
x_axis = self._hz_to_x(frequency_axis)
y_now = self._dbm_to_y(rssi_values)
y_max = self._dbm_to_y(numpy.amax(self._persisted_frames, axis=0))
# TODO: Wrapped Numpy types with float() to support old (<1.0) PySide API in Ubuntu 10.10
path_now.moveTo(float(x_axis[0]), float(y_now[0]))
for i in bins:
path_now.lineTo(float(x_axis[i]), float(y_now[i]))
# TODO: Wrapped Numpy types with float() to support old (<1.0) PySide API in Ubuntu 10.10
path_max.moveTo(float(x_axis[0]), float(y_max[0]))
for i in bins:
path_max.lineTo(float(x_axis[i]), float(y_max[i]))
painter.setPen(Qt.white)
painter.drawPath(path_now)
self._path_max = path_max
finally:
painter.end()
def _draw_reticle(self):
if self._reticle is None or (self._reticle.size() != self.size()):
self._new_reticle()
dbm_lines = [QLineF(self._hz_to_x(self._low_frequency), self._dbm_to_y(dbm),
self._hz_to_x(self._high_frequency), self._dbm_to_y(dbm))
for dbm in numpy.arange(self._low_dbm, self._high_dbm, 20.0)]
dbm_labels = [(dbm, QPointF(self._hz_to_x(self._low_frequency) + 2, self._dbm_to_y(dbm) - 2))
for dbm in numpy.arange(self._low_dbm, self._high_dbm, 20.0)]
frequency_lines = [QLineF(self._hz_to_x(frequency), self._dbm_to_y(self._high_dbm),
self._hz_to_x(frequency), self._dbm_to_y(self._low_dbm))
for frequency in numpy.arange(self._low_frequency, self._high_frequency, self._frequency_step * 10.0)]
frequency_labels = [(frequency, QPointF(self._hz_to_x(frequency) + 2, self._dbm_to_y(self._high_dbm) + 10))
for frequency in numpy.arange(self._low_frequency, self._high_frequency, self._frequency_step * 10.0)]
painter = QtGui.QPainter(self._reticle)
try:
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(Qt.blue)
# TODO: Removed to support old (<1.0) PySide API in Ubuntu 10.10
#painter.drawLines(dbm_lines)
for dbm_line in dbm_lines: painter.drawLine(dbm_line)
# TODO: Removed to support old (<1.0) PySide API in Ubuntu 10.10
#painter.drawLines(frequency_lines)
for frequency_line in frequency_lines: painter.drawLine(frequency_line)
painter.setPen(Qt.white)
for dbm, point in dbm_labels:
painter.drawText(point, '%+.0f' % dbm)
for frequency, point in frequency_labels:
painter.drawText(point, '%.0f' % (frequency / 1e6))
finally:
painter.end()
def paintEvent(self, event):
self._draw_graph()
self._draw_reticle()
painter = QtGui.QPainter(self)
try:
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtGui.QPen())
painter.setBrush(QtGui.QBrush())
if self._graph:
painter.drawPixmap(0, 0, self._graph)
if self._path_max:
painter.setPen(Qt.green)
painter.drawPath(self._path_max)
painter.setOpacity(0.5)
if self._reticle:
painter.drawPixmap(0, 0, self._reticle)
finally:
painter.end()
def _hz_to_x(self, frequency_hz):
delta = frequency_hz - self._low_frequency
range = self._high_frequency - self._low_frequency
normalized = delta / range
return normalized * self.width()
def _dbm_to_y(self, dbm):
delta = self._high_dbm - dbm
range = self._high_dbm - self._low_dbm
normalized = delta / range
return normalized * self.height()
class Window(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self._device = self._open_device()
self.render_area = RenderArea(self._device)
main_layout = QtGui.QGridLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(self.render_area, 0, 0)
self.setLayout(main_layout)
self.setWindowTitle("Ubertooth Spectrum Analyzer")
def sizeHint(self):
return QtCore.QSize(480, 160)
def _open_device(self):
device = usb.core.find(idVendor=0xFFFF, idProduct=0x0004)
if device is None:
raise Exception('Device not found')
return Ubertooth(device)
def closeEvent(self, event):
self.render_area.stop_thread()
self._device.close()
event.accept()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())