2012-07-17 10:10:23 +08:00
|
|
|
#!/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
|
2013-06-10 23:57:12 +08:00
|
|
|
from air_modes.exceptions import *
|
2012-07-17 10:10:23 +08:00
|
|
|
|
|
|
|
#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()
|
|
|
|
|
2013-07-23 09:15:26 +08:00
|
|
|
#weeds out ICAOs older than 1 minute
|
2012-07-17 10:10:23 +08:00
|
|
|
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()
|
|
|
|
|
2013-06-20 02:24:11 +08:00
|
|
|
class dashboard_output:
|
|
|
|
def __init__(self, cprdec, model, pub):
|
2012-07-17 10:10:23 +08:00
|
|
|
self.model = model
|
2013-06-20 02:24:11 +08:00
|
|
|
self._cpr = cprdec
|
|
|
|
pub.subscribe("modes_dl", self.output)
|
2012-07-17 10:10:23 +08:00
|
|
|
def output(self, msg):
|
2013-06-10 23:57:12 +08:00
|
|
|
try:
|
2013-06-20 02:24:11 +08:00
|
|
|
msgtype = msg.data["df"]
|
2013-06-10 23:57:12 +08:00
|
|
|
now = time.time()
|
2013-06-20 02:24:11 +08:00
|
|
|
newrow = {"rssi": msg.rssi, "seen": now}
|
2013-06-10 23:57:12 +08:00
|
|
|
if msgtype in [0, 4, 20]:
|
2013-06-20 02:24:11 +08:00
|
|
|
newrow["altitude"] = air_modes.altitude.decode_alt(msg.data["ac"], True)
|
|
|
|
newrow["icao"] = msg.ecc
|
2013-06-10 23:57:12 +08:00
|
|
|
self.model.addRecord(newrow)
|
|
|
|
|
|
|
|
elif msgtype == 17:
|
2013-06-20 02:24:11 +08:00
|
|
|
icao = msg.data["aa"]
|
2013-06-10 23:57:12 +08:00
|
|
|
newrow["icao"] = icao
|
2013-06-20 02:24:11 +08:00
|
|
|
subtype = msg.data["ftc"]
|
2013-06-10 23:57:12 +08:00
|
|
|
if subtype == 4:
|
2013-06-20 02:24:11 +08:00
|
|
|
(ident, actype) = air_modes.parseBDS08(msg.data)
|
2013-06-10 23:57:12 +08:00
|
|
|
newrow["ident"] = ident
|
|
|
|
newrow["type"] = actype
|
|
|
|
elif 5 <= subtype <= 8:
|
2013-06-20 02:24:11 +08:00
|
|
|
(ground_track, decoded_lat, decoded_lon, rnge, bearing) = air_modes.parseBDS06(msg.data, self._cpr)
|
2013-06-10 23:57:12 +08:00
|
|
|
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:
|
2013-06-20 02:24:11 +08:00
|
|
|
(altitude, decoded_lat, decoded_lon, rnge, bearing) = air_modes.parseBDS05(msg.data, self._cpr)
|
2013-06-10 23:57:12 +08:00
|
|
|
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:
|
2013-06-20 02:24:11 +08:00
|
|
|
subsubtype = msg.data["sub"]
|
2013-06-10 23:57:12 +08:00
|
|
|
velocity = None
|
|
|
|
heading = None
|
|
|
|
vert_spd = None
|
|
|
|
if subsubtype == 0:
|
2013-06-20 02:24:11 +08:00
|
|
|
(velocity, heading, vert_spd) = air_modes.parseBDS09_0(msg.data)
|
2013-06-10 23:57:12 +08:00
|
|
|
elif 1 <= subsubtype <= 2:
|
2013-06-20 02:24:11 +08:00
|
|
|
(velocity, heading, vert_spd) = air_modes.parseBDS09_1(msg.data)
|
2013-06-10 23:57:12 +08:00
|
|
|
newrow["speed"] = velocity
|
|
|
|
newrow["heading"] = heading
|
|
|
|
newrow["vertical"] = vert_spd
|
|
|
|
|
|
|
|
self.model.addRecord(newrow)
|
|
|
|
|
|
|
|
except ADSBError:
|
|
|
|
return
|
2012-07-17 10:10:23 +08:00
|
|
|
|