diff --git a/apps/modes_gui b/apps/modes_gui index 450edf6..085f19d 100755 --- a/apps/modes_gui +++ b/apps/modes_gui @@ -1,13 +1,33 @@ -#!/usr/bin/python +#!/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 PyQt4 import QtCore,QtGui,QtSql +from PyQt4 import QtCore,QtGui from PyQt4.Qwt5 import Qwt from gnuradio import gr, gru, optfir, eng_notation, blks2 import gnuradio.gr.gr_threading as _threading import air_modes from air_modes.modes_exceptions import * from air_modes.modes_rx_ui import Ui_MainWindow +from air_modes.modes_gui_model import * import sqlite3 class mainwindow(QtGui.QMainWindow): @@ -66,8 +86,6 @@ class mainwindow(QtGui.QMainWindow): self.ui.list_aircraft.setModelColumn(0) #set up dashboard views - #TODO: figure out why you can't update the RSSI or compass widgets with this mapper - #pretty sure it's a Qt bug in the SQL code -- returns as string instead of double self.icaodelegate = ICAOViewDelegate() self.ui.list_aircraft.setItemDelegate(self.icaodelegate) self.dashboard_mapper = QtGui.QDataWidgetMapper() @@ -99,6 +117,7 @@ class mainwindow(QtGui.QMainWindow): self.datamodel.dataChanged.connect(self.compass_widgets_dataChanged) self.ui.list_aircraft.selectionModel().currentRowChanged.connect(self.update_rssi_widget) +############ 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] @@ -109,16 +128,6 @@ class mainwindow(QtGui.QMainWindow): bearing = index.model().data(index.model().index(index.row(), self.datamodel._colnames.index("bearing"))).toDouble()[0] self.ui.compass_bearing.setValue(bearing) - 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 - def compass_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 @@ -131,7 +140,18 @@ class mainwindow(QtGui.QMainWindow): 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() @@ -181,6 +201,7 @@ class mainwindow(QtGui.QMainWindow): 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() @@ -276,178 +297,6 @@ class mainwindow(QtGui.QMainWindow): self.ui.text_livedata.append(msgstr) self.ui.text_livedata.verticalScrollBar().setSliderPosition(self.ui.text_livedata.verticalScrollBar().maximum()) -#fades the ICAOs out as their last report gets older, -#and display ident if available, ICAO otherwise -class ICAOViewDelegate(QtGui.QStyledItemDelegate): - def paint(self, painter, option, index): - #draw selection rectangle - if option.state & QtGui.QStyle.State_Selected: - painter.setBrush(QtGui.QPalette().highlight()) - painter.drawRect(option.rect) - - #if there's an ident available, use it. otherwise print the ICAO - if index.model().data(index.model().index(index.row(), 9)) != QtCore.QVariant(): - paintstr = index.model().data(index.model().index(index.row(), 9)).toString() - else: - paintstr = index.model().data(index.model().index(index.row(), 0)).toString() - last_report = index.model().data(index.model().index(index.row(), 1)).toDouble()[0] - age = (time.time() - last_report) - max_age = 60. #age at which it grays out - #minimum alpha is 0x40 (oldest), max is 0xFF (newest) - age = min(age, max_age) - alpha = int(0xFF - (0xBF / max_age) * age) - painter.setPen(QtGui.QColor(0, 0, 0, alpha)) - painter.drawText(option.rect.left()+3, option.rect.top(), option.rect.width(), option.rect.height(), option.displayAlignment, paintstr) - -#the data model used to display dashboard data. -class dashboard_data_model(QtCore.QAbstractTableModel): - def __init__(self, parent): - QtCore.QAbstractTableModel.__init__(self, parent) - self._data = [] - self.lock = threading.Lock() - self._colnames = ["icao", "seen", "rssi", "latitude", "longitude", "altitude", "speed", "heading", "vertical", "ident", "type", "range", "bearing"] - #custom precision limits for display - self._precisions = [None, None, None, 6, 6, 0, 0, 0, 0, None, None, 2, 0] - for field in self._colnames: - self.setHeaderData(self._colnames.index(field), QtCore.Qt.Horizontal, field) - def rowCount(self, parent=QtCore.QVariant()): - return len(self._data) - def columnCount(self, parent=QtCore.QVariant()): - return len(self._colnames) - def data(self, index, role=QtCore.Qt.DisplayRole): - if not index.isValid(): - return QtCore.QVariant() - if index.row() >= self.rowCount(): - return QtCore.QVariant() - if index.column() >= self.columnCount(): - return QtCore.QVariant() - if (role != QtCore.Qt.DisplayRole) and (role != QtCore.Qt.EditRole): - return QtCore.QVariant() - if self._data[index.row()][index.column()] is None: - return QtCore.QVariant() - else: - #if there's a dedicated precision for that column, print it out with the specified precision. - #this only works well if you DON'T have other views/widgets that depend on numeric data coming out. - #i don't like this, but it works for now. unfortunately it seems like Qt doesn't give you a - #good alternative. - if self._precisions[index.column()] is not None: - return QtCore.QVariant("%.*f" % (self._precisions[index.column()], self._data[index.row()][index.column()])) - else: - if self._colnames[index.column()] == "icao": - return QtCore.QVariant("%06x" % self._data[index.row()][index.column()]) #return as hex string - else: - return QtCore.QVariant(self._data[index.row()][index.column()]) - - def setData(self, index, value, role=QtCore.Qt.EditRole): - self.lock.acquire() - if not index.isValid(): - return False - if index.row() >= self.rowCount(): - return False - if index.column >= self.columnCount(): - return False - if role != QtCore.Qt.EditRole: - return False - self._data[index.row()][index.column()] = value - self.lock.release() - - #addRecord implements an upsert on self._data; that is, - #it updates the row if the ICAO exists, or else it creates a new row. - def addRecord(self, record): - self.lock.acquire() - icaos = [x[0] for x in self._data] - if record["icao"] in icaos: - row = icaos.index(record["icao"]) - for column in record: - self._data[row][self._colnames.index(column)] = record[column] - #create index to existing row and tell the model everything's changed in this row - #or inside the for loop, use dataChanged on each changed field (might be better) - self.dataChanged.emit(self.createIndex(row, 0), self.createIndex(row, len(self._colnames)-1)) - - #only create records for ICAOs with ADS-B reports - elif ("latitude" or "speed" or "ident") in record: - #find new inserted row number - icaos.append(record["icao"]) - newrowoffset = sorted(icaos).index(record["icao"]) - self.beginInsertRows(QtCore.QModelIndex(), newrowoffset, newrowoffset) - newrecord = [None for x in xrange(len(self._colnames))] - for col in xrange(0, len(self._colnames)): - if self._colnames[col] in record: - newrecord[col] = record[self._colnames[col]] - self._data.append(newrecord) - self._data = sorted(self._data, key = lambda x: x[0]) #sort by icao - self.endInsertRows() - self.lock.release() - self.prune() - - #weeds out ICAOs older than 5 minutes - def prune(self): - self.lock.acquire() - for (index,row) in enumerate(self._data): - if time.time() - row[1] >= 60: - self.beginRemoveRows(QtCore.QModelIndex(), index, index) - self._data.pop(index) - self.endRemoveRows() - self.lock.release() - - -class dashboard_output(air_modes.modes_parse.modes_parse): - def __init__(self, mypos, model): - air_modes.modes_parse.modes_parse.__init__(self, mypos) - self.model = model - def output(self, msg): - [data, ecc, reference, timestamp] = msg.split() - data = air_modes.modes_parse.modes_reply(long(data, 16)) - ecc = long(ecc, 16) - rssi = 10.*math.log10(float(reference)) - msgtype = data["df"] - now = time.time() - newrow = {"rssi": rssi, "seen": now} - if msgtype in [0, 4, 20]: - newrow["altitude"] = air_modes.altitude.decode_alt(data["ac"], True) - newrow["icao"] = ecc - self.model.addRecord(newrow) - - elif msgtype == 17: - icao = data["aa"] - newrow["icao"] = icao - subtype = data["ftc"] - if subtype == 4: - (ident, actype) = self.parseBDS08(data) - newrow["ident"] = ident - newrow["type"] = actype - elif 5 <= subtype <= 8: - (ground_track, decoded_lat, decoded_lon, rnge, bearing) = self.parseBDS06(data) - newrow["heading"] = ground_track - newrow["latitude"] = decoded_lat - newrow["longitude"] = decoded_lon - newrow["altitude"] = 0 - if rnge is not None: - newrow["range"] = rnge - newrow["bearing"] = bearing - elif 9 <= subtype <= 18: - (altitude, decoded_lat, decoded_lon, rnge, bearing) = self.parseBDS05(data) - newrow["altitude"] = altitude - newrow["latitude"] = decoded_lat - newrow["longitude"] = decoded_lon - if rnge is not None: - newrow["range"] = rnge - newrow["bearing"] = bearing - elif subtype == 19: - subsubtype = data["sub"] - velocity = None - heading = None - vert_spd = None - if subsubtype == 0: - (velocity, heading, vert_spd) = self.parseBDS09_0(data) - elif 1 <= subsubtype <= 2: - (velocity, heading, vert_spd) = self.parseBDS09_1(data) - newrow["speed"] = velocity - newrow["heading"] = heading - newrow["vertical"] = vert_spd - - self.model.addRecord(newrow) - #the output handler is a thread which runs the various registered output functions when there's a received message. #it also executes registered updates at the sleep rate -- currently 10Hz. diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index cbc76da..1c89e2b 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -36,6 +36,7 @@ GR_PYTHON_INSTALL( mlat.py modes_exceptions.py modes_flightgear.py + modes_gui_model.py modes_kml.py modes_parse.py modes_print.py diff --git a/python/modes_gui_model.py b/python/modes_gui_model.py new file mode 100644 index 0000000..1a661d5 --- /dev/null +++ b/python/modes_gui_model.py @@ -0,0 +1,198 @@ +#!/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. +# + +# This file contains data models, view delegates, and associated classes +# for handling the GUI back end data model. + +from PyQt4 import QtCore, QtGui +import air_modes +import threading, math, time + +#fades the ICAOs out as their last report gets older, +#and display ident if available, ICAO otherwise +class ICAOViewDelegate(QtGui.QStyledItemDelegate): + def paint(self, painter, option, index): + #draw selection rectangle + if option.state & QtGui.QStyle.State_Selected: + painter.setBrush(QtGui.QPalette().highlight()) + painter.drawRect(option.rect) + + #if there's an ident available, use it. otherwise print the ICAO + if index.model().data(index.model().index(index.row(), 9)) != QtCore.QVariant(): + paintstr = index.model().data(index.model().index(index.row(), 9)).toString() + else: + paintstr = index.model().data(index.model().index(index.row(), 0)).toString() + last_report = index.model().data(index.model().index(index.row(), 1)).toDouble()[0] + age = (time.time() - last_report) + max_age = 60. #age at which it grays out + #minimum alpha is 0x40 (oldest), max is 0xFF (newest) + age = min(age, max_age) + alpha = int(0xFF - (0xBF / max_age) * age) + painter.setPen(QtGui.QColor(0, 0, 0, alpha)) + painter.drawText(option.rect.left()+3, option.rect.top(), option.rect.width(), option.rect.height(), option.displayAlignment, paintstr) + +#the data model used to display dashboard data. +class dashboard_data_model(QtCore.QAbstractTableModel): + def __init__(self, parent): + QtCore.QAbstractTableModel.__init__(self, parent) + self._data = [] + self.lock = threading.Lock() + self._colnames = ["icao", "seen", "rssi", "latitude", "longitude", "altitude", "speed", "heading", "vertical", "ident", "type", "range", "bearing"] + #custom precision limits for display + self._precisions = [None, None, None, 6, 6, 0, 0, 0, 0, None, None, 2, 0] + for field in self._colnames: + self.setHeaderData(self._colnames.index(field), QtCore.Qt.Horizontal, field) + def rowCount(self, parent=QtCore.QVariant()): + return len(self._data) + def columnCount(self, parent=QtCore.QVariant()): + return len(self._colnames) + def data(self, index, role=QtCore.Qt.DisplayRole): + if not index.isValid(): + return QtCore.QVariant() + if index.row() >= self.rowCount(): + return QtCore.QVariant() + if index.column() >= self.columnCount(): + return QtCore.QVariant() + if (role != QtCore.Qt.DisplayRole) and (role != QtCore.Qt.EditRole): + return QtCore.QVariant() + if self._data[index.row()][index.column()] is None: + return QtCore.QVariant() + else: + #if there's a dedicated precision for that column, print it out with the specified precision. + #this only works well if you DON'T have other views/widgets that depend on numeric data coming out. + #i don't like this, but it works for now. unfortunately it seems like Qt doesn't give you a + #good alternative. + if self._precisions[index.column()] is not None: + return QtCore.QVariant("%.*f" % (self._precisions[index.column()], self._data[index.row()][index.column()])) + else: + if self._colnames[index.column()] == "icao": + return QtCore.QVariant("%06x" % self._data[index.row()][index.column()]) #return as hex string + else: + return QtCore.QVariant(self._data[index.row()][index.column()]) + + def setData(self, index, value, role=QtCore.Qt.EditRole): + self.lock.acquire() + if not index.isValid(): + return False + if index.row() >= self.rowCount(): + return False + if index.column >= self.columnCount(): + return False + if role != QtCore.Qt.EditRole: + return False + self._data[index.row()][index.column()] = value + self.lock.release() + + #addRecord implements an upsert on self._data; that is, + #it updates the row if the ICAO exists, or else it creates a new row. + def addRecord(self, record): + self.lock.acquire() + icaos = [x[0] for x in self._data] + if record["icao"] in icaos: + row = icaos.index(record["icao"]) + for column in record: + self._data[row][self._colnames.index(column)] = record[column] + #create index to existing row and tell the model everything's changed in this row + #or inside the for loop, use dataChanged on each changed field (might be better) + self.dataChanged.emit(self.createIndex(row, 0), self.createIndex(row, len(self._colnames)-1)) + + #only create records for ICAOs with ADS-B reports + elif ("latitude" or "speed" or "ident") in record: + #find new inserted row number + icaos.append(record["icao"]) + newrowoffset = sorted(icaos).index(record["icao"]) + self.beginInsertRows(QtCore.QModelIndex(), newrowoffset, newrowoffset) + newrecord = [None for x in xrange(len(self._colnames))] + for col in xrange(0, len(self._colnames)): + if self._colnames[col] in record: + newrecord[col] = record[self._colnames[col]] + self._data.append(newrecord) + self._data = sorted(self._data, key = lambda x: x[0]) #sort by icao + self.endInsertRows() + self.lock.release() + self.prune() + + #weeds out ICAOs older than 5 minutes + def prune(self): + self.lock.acquire() + for (index,row) in enumerate(self._data): + if time.time() - row[1] >= 60: + self.beginRemoveRows(QtCore.QModelIndex(), index, index) + self._data.pop(index) + self.endRemoveRows() + self.lock.release() + +class dashboard_output(air_modes.modes_parse.modes_parse): + def __init__(self, mypos, model): + air_modes.modes_parse.modes_parse.__init__(self, mypos) + self.model = model + def output(self, msg): + [data, ecc, reference, timestamp] = msg.split() + data = air_modes.modes_parse.modes_reply(long(data, 16)) + ecc = long(ecc, 16) + rssi = 10.*math.log10(float(reference)) + msgtype = data["df"] + now = time.time() + newrow = {"rssi": rssi, "seen": now} + if msgtype in [0, 4, 20]: + newrow["altitude"] = air_modes.altitude.decode_alt(data["ac"], True) + newrow["icao"] = ecc + self.model.addRecord(newrow) + + elif msgtype == 17: + icao = data["aa"] + newrow["icao"] = icao + subtype = data["ftc"] + if subtype == 4: + (ident, actype) = self.parseBDS08(data) + newrow["ident"] = ident + newrow["type"] = actype + elif 5 <= subtype <= 8: + (ground_track, decoded_lat, decoded_lon, rnge, bearing) = self.parseBDS06(data) + newrow["heading"] = ground_track + newrow["latitude"] = decoded_lat + newrow["longitude"] = decoded_lon + newrow["altitude"] = 0 + if rnge is not None: + newrow["range"] = rnge + newrow["bearing"] = bearing + elif 9 <= subtype <= 18: + (altitude, decoded_lat, decoded_lon, rnge, bearing) = self.parseBDS05(data) + newrow["altitude"] = altitude + newrow["latitude"] = decoded_lat + newrow["longitude"] = decoded_lon + if rnge is not None: + newrow["range"] = rnge + newrow["bearing"] = bearing + elif subtype == 19: + subsubtype = data["sub"] + velocity = None + heading = None + vert_spd = None + if subsubtype == 0: + (velocity, heading, vert_spd) = self.parseBDS09_0(data) + elif 1 <= subsubtype <= 2: + (velocity, heading, vert_spd) = self.parseBDS09_1(data) + newrow["speed"] = velocity + newrow["heading"] = heading + newrow["vertical"] = vert_spd + + self.model.addRecord(newrow)