Split out GUI data model and views into modes_gui_model.py

This commit is contained in:
Nick Foster
2012-07-16 19:10:23 -07:00
parent 373a6f04ee
commit 5442cba206
3 changed files with 234 additions and 186 deletions

View File

@@ -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.

View File

@@ -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

198
python/modes_gui_model.py Normal file
View File

@@ -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)