gr-air-modes/apps/modes_gui

350 lines
15 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright 2012 Nick Foster
#
# This file is part of gr-air-modes
#
# gr-air-modes 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 3, or (at your option)
# any later version.
#
# gr-air-modes 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 gr-air-modes; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
#
import os, sys, time, threading, datetime, math, csv
from optparse import OptionParser
from PyQt4 import QtCore,QtGui
from PyQt4.Qwt5 import Qwt
from gnuradio import gr, gru, optfir, eng_notation, blks2
from gnuradio.eng_option import eng_option
import air_modes
from air_modes.exceptions import *
from air_modes.modes_rx_ui import Ui_MainWindow
from air_modes.gui_model import *
import sqlite3
import zmq
class mainwindow(QtGui.QMainWindow):
live_data_changed_signal = QtCore.pyqtSignal(QtCore.QString, name='liveDataChanged')
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
#set defaults
#add file, RTL, UHD sources
self.ui.combo_source.addItems(["UHD", "Osmocom", "File/UDP"])
self.ui.combo_source.setCurrentIndex(0)
#populate antenna, rate combo boxes based on source
self.populate_source_options()
#should round to actual achieved gain
self.ui.line_gain.insert("30")
#default to 5dB
self.ui.line_threshold.insert("5")
self.ui.prog_rssi.setMinimum(0)
self.ui.prog_rssi.setMaximum(40)
self.ui.combo_ant.setCurrentIndex(self.ui.combo_ant.findText("RX2"))
#check KML by default, leave the rest unchecked.
self.ui.check_sbs1.setCheckState(QtCore.Qt.Unchecked)
self.ui.check_raw.setCheckState(QtCore.Qt.Unchecked)
self.ui.check_fgfs.setCheckState(QtCore.Qt.Unchecked)
self.ui.check_kml.setCheckState(QtCore.Qt.Checked)
self.ui.line_sbs1port.insert("30003")
self.ui.line_rawport.insert("9988")
self.ui.line_fgfsport.insert("5500")
self.ui.line_kmlfilename.insert("modes.kml")
#disable by default
self.ui.check_adsbonly.setCheckState(QtCore.Qt.Unchecked)
self.queue = gr.msg_queue(10)
self.running = False
self.kmlgen = None #necessary bc we stop its thread in shutdown
self.dbname = "air_modes.db"
self.num_reports = 0
self.last_report = 0
self.context = zmq.Context(1)
self.datamodel = dashboard_data_model(None)
self.ui.list_aircraft.setModel(self.datamodel)
self.ui.list_aircraft.setModelColumn(0)
self.az_model = air_modes.az_map_model(None)
self.ui.azimuth_map.setModel(self.az_model)
#set up dashboard views
self.icaodelegate = ICAOViewDelegate()
self.ui.list_aircraft.setItemDelegate(self.icaodelegate)
self.dashboard_mapper = QtGui.QDataWidgetMapper()
self.dashboard_mapper.setModel(self.datamodel)
self.dashboard_mapper.addMapping(self.ui.line_icao, 0)
#self.dashboard_mapper.addMapping(self.ui.prog_rssi, 2)
self.dashboard_mapper.addMapping(self.ui.line_latitude, 3)
self.dashboard_mapper.addMapping(self.ui.line_longitude, 4)
self.dashboard_mapper.addMapping(self.ui.line_alt, 5)
self.dashboard_mapper.addMapping(self.ui.line_speed, 6)
#self.dashboard_mapper.addMapping(self.ui.compass_heading, 7)
self.dashboard_mapper.addMapping(self.ui.line_climb, 8)
self.dashboard_mapper.addMapping(self.ui.line_ident, 9)
self.dashboard_mapper.addMapping(self.ui.line_type, 10)
self.dashboard_mapper.addMapping(self.ui.line_range, 11)
compass_palette = QtGui.QPalette()
compass_palette.setColor(QtGui.QPalette.Foreground, QtCore.Qt.white)
self.ui.compass_heading.setPalette(compass_palette)
self.ui.compass_bearing.setPalette(compass_palette)
#TODO: change the needle to an aircraft silhouette
self.ui.compass_heading.setNeedle(Qwt.QwtDialSimpleNeedle(Qwt.QwtDialSimpleNeedle.Ray, False, QtCore.Qt.black))
self.ui.compass_bearing.setNeedle(Qwt.QwtDialSimpleNeedle(Qwt.QwtDialSimpleNeedle.Ray, False, QtCore.Qt.black))
#hook up the update signal
self.ui.list_aircraft.selectionModel().currentRowChanged.connect(self.dashboard_mapper.setCurrentModelIndex)
self.ui.list_aircraft.selectionModel().currentRowChanged.connect(self.update_heading_widget)
self.ui.list_aircraft.selectionModel().currentRowChanged.connect(self.update_bearing_widget)
self.datamodel.dataChanged.connect(self.unmapped_widgets_dataChanged)
self.ui.list_aircraft.selectionModel().currentRowChanged.connect(self.update_rssi_widget)
#hook up live data text box update signal
self.live_data_changed_signal.connect(self.on_append_live_data)
############ widget update functions for non-mapped widgets ############
def update_heading_widget(self, index):
if index.model() is not None:
heading = index.model().data(index.model().index(index.row(), self.datamodel._colnames.index("heading"))).toDouble()[0]
self.ui.compass_heading.setValue(heading)
def update_bearing_widget(self, index):
if index.model() is not None:
bearing = index.model().data(index.model().index(index.row(), self.datamodel._colnames.index("bearing"))).toDouble()[0]
self.ui.compass_bearing.setValue(bearing)
def unmapped_widgets_dataChanged(self, startIndex, endIndex):
index = self.ui.list_aircraft.selectionModel().currentIndex()
if index.row() in range(startIndex.row(), endIndex.row()+1): #the current aircraft was affected
if self.datamodel._colnames.index("heading") in range(startIndex.column(), endIndex.column()+1):
self.update_heading_widget(index)
if self.datamodel._colnames.index("bearing") in range(startIndex.column(), endIndex.column()+1):
self.update_bearing_widget(index)
if self.datamodel._colnames.index("rssi") in range(startIndex.column(), endIndex.column()+1):
self.update_rssi_widget(index)
def update_rssi_widget(self, index):
if index.model() is not None:
rssi = index.model().data(index.model().index(index.row(), 2)).toDouble()[0]
self.ui.prog_rssi.setValue(rssi)
def increment_reportspersec(self, msg):
self.num_reports += 1
def update_reportspersec(self):
dt = time.time() - self.last_report
if dt >= 1.0:
self.last_report = time.time()
self.ui.line_reports.setText("%i" % self.num_reports)
self.num_reports = 0
##################### dynamic option population ########################
#goes and gets valid antenna, sample rate options from the device and grays out appropriate things
def populate_source_options(self):
sourceid = self.ui.combo_source.currentText()
self.rates = []
self.ratetext = []
self.antennas = []
if sourceid == "UHD":
try:
from gnuradio import uhd
self.src = uhd.single_usrp_source("", uhd.io_type_t.COMPLEX_FLOAT32, 1)
self.rates = [rate.start() for rate in self.src.get_samp_rates() if (rate.start() % 2.e6) == 0]
self.antennas = self.src.get_antennas()
self.src = None #deconstruct UHD source for now
self.ui.combo_ant.setEnabled(True)
self.ui.combo_rate.setEnabled(True)
self.ui.stack_source.setCurrentIndex(0)
except:
self.rates = []
self.antennas = []
self.ui.combo_ant.setEnabled(False)
self.ui.combo_rate.setEnabled(False)
self.ui.stack_source.setCurrentIndex(0)
elif sourceid == "Osmocom":
try:
import osmosdr
self.src = osmosdr.source_c("")
self.rates = [rate.start() for rate in self.src.get_sample_rates() if (rate.start() % 2.e6) == 0]
self.antennas = ["RX"]
self.src = None
self.ui.combo_ant.setEnabled(False)
self.ui.combo_rate.setEnabled(True)
self.ui.stack_source.setCurrentIndex(0)
except:
self.rates = []
self.antennas = []
self.ui.combo_ant.setEnabled(False)
self.ui.combo_rate.setEnabled(False)
self.ui.stack_source.setCurrentIndex(0)
elif sourceid == "File/UDP":
self.rates = [2e6*i for i in range(2,13)]
self.antennas = ["None"]
self.ui.combo_ant.setEnabled(False)
self.ui.combo_rate.setEnabled(True)
self.ui.stack_source.setCurrentIndex(1)
self.ui.combo_rate.clear()
self.ratetext = ["%.3f" % (rate / 1.e6) for rate in self.rates]
for rate, text in zip(self.rates, self.ratetext):
self.ui.combo_rate.addItem(text, rate)
self.ui.combo_ant.clear()
self.ui.combo_ant.addItems(self.antennas)
if 4e6 in self.rates:
self.ui.combo_rate.setCurrentIndex(self.rates.index(4e6))
################ action handlers ####################
def on_combo_source_currentIndexChanged(self, index):
self.populate_source_options()
def on_button_start_released(self):
#if we're already running, kill it!
if self.running is True:
self.on_quit()
self.num_reports = 0
self.ui.line_reports.setText("0")
self.ui.button_start.setText("Start")
self.running = False
else: #we aren't already running, let's get this party started
parser = OptionParser(option_class=eng_option)
air_modes.modes_radio.add_radio_options(parser)
(options, args) = parser.parse_args() #sets defaults nicely
if str(self.ui.combo_source.currentText()) != "File/UDP":
options.source = str(self.ui.combo_source.currentText()).lower()
else:
options.source = str(self.ui.line_inputfile.text())
options.rate = float(self.ui.combo_rate.currentText()) * 1e6
options.antenna = str(self.ui.combo_ant.currentText())
options.gain = float(self.ui.line_gain.text())
options.threshold = float(self.ui.line_threshold.text())
options.pmf = self.ui.check_pmf.checkState()
self._servers = ["inproc://modes-radio-pub"] #TODO ADD REMOTES
self._relay = air_modes.zmq_pubsub_iface(self.context, subaddr=self._servers, pubaddr=None)
if self.ui.check_raw.checkState():
options.tcp = int(self.ui.line_rawport.text())
self._radio = air_modes.modes_radio(options, self.context)
try:
my_position = [float(self.ui.line_my_lat.text()), float(self.ui.line_my_lon.text())]
except:
my_position = None
self.datamodelout = dashboard_output(my_position, self.datamodel)
self._relay.subscribe("dl_data", self.datamodelout.output)
self.lock = threading.Lock() #grab a lock to ensure sql and kml don't step on each other
#output options to populate outputs, updates
if self.ui.check_kml.checkState():
#we spawn a thread to run every 30 seconds (or whatever) to generate KML
self.kmlgen = air_modes.output_kml(self.ui.line_kmlfilename.text(), self.dbname, my_position, self.lock) #create a KML generating thread
if self.ui.check_sbs1.checkState():
sbs1port = int(self.ui.line_sbs1port.text())
sbs1out = air_modes.output_sbs1(my_position, sbs1port)
self._relay.subscribe("dl_data", sbs1.output)
if self.ui.check_fgfs.checkState():
fghost = "127.0.0.1" #TODO FIXME
fgport = self.ui.line_fgfsport.text()
fgout = air_modes.output_flightgear(my_position, fghost, int(fgport))
self._relay.subscribe("dl_data", fgout.output)
#add azimuth map output and hook it up
if my_position is not None:
self.az_map_output = air_modes.az_map_output(my_position, self.az_model)
self._relay.subscribe("dl_data", self.az_map_output.output)
self.livedata = air_modes.output_print(my_position)
#add output for live data box
self._relay.subscribe("dl_data", self.output_live_data)
#create SQL database for KML and dashboard displays
self.dbwriter = air_modes.output_sql(my_position, self.dbname, self.lock)
self._relay.subscribe("dl_data", self.dbwriter.insert) #now the db will update itself
#output to update reports/sec widget
self._relay.subscribe("dl_data", self.increment_reportspersec)
#self.updates.append(self.update_reportspersec) #TODO FIXME
#start the flowgraph
self._radio.start()
self.ui.button_start.setText("Stop")
self.running = True
def on_quit(self):
if self.running is True:
self._relay.close()
self._radio.close()
self._relay = None
self._radio = None
try:
self.kmlgen.done = True
#TODO FIXME need a way to kill kmlgen safely without delay
#self.kmlgen.join()
#self.kmlgen = None
except:
pass
#slot to catch signal emitted by output_live_data (necessary for
#thread safety since output_live_data is called by another thread)
def on_append_live_data(self, msgstr):
#limit scrollback buffer size -- is there a faster way?
if(self.ui.text_livedata.document().lineCount() > 500):
cursor = self.ui.text_livedata.textCursor()
cursor.movePosition(QtGui.QTextCursor.Start)
cursor.select(QtGui.QTextCursor.LineUnderCursor)
cursor.removeSelectedText()
self.ui.text_livedata.append(msgstr)
self.ui.text_livedata.verticalScrollBar().setSliderPosition(self.ui.text_livedata.verticalScrollBar().maximum())
def output_live_data(self, msg):
try:
msgstr = self.livedata.parse(msg)
if msgstr is not None:
self.live_data_changed_signal.emit(msgstr)
except ADSBError:
pass
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = mainwindow()
app.lastWindowClosed.connect(window.on_quit)
window.setWindowTitle("Mode S/ADS-B receiver")
window.show()
sys.exit(app.exec_())