Files
clever-show/Server/copter_table_models.py
2020-01-28 19:05:16 +03:00

643 lines
20 KiB
Python

import os
import re
import sys
import time
import math
from contextlib import suppress
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt as Qt, QUrl, QDir
ModelDataRole = 998
ModelStateRole = 999
class ModelChecks:
checks_dict = {}
takeoff_checklist = (3, 4, 6, 7, 8)
battery_min = 50.0
start_pos_delta_max = 1.0
time_delta_max = 1.0
@classmethod
def col_check(cls, col):
def inner(f):
def wrapper(item):
if item is not None:
return f(item)
return None
cls.checks_dict[col] = wrapper
return wrapper
return inner
@classmethod
def all_checks(cls, copter_item):
for col, check in cls.checks_dict.items():
if not check(copter_item[col]):
return False
return True
@classmethod
def takeoff_checks(cls, copter_item):
for col in cls.takeoff_checklist:
if not cls.checks_dict[col](copter_item[col]):
return False
return True
@ModelChecks.col_check(1)
def check_ver(item):
return True # TODO git version!
@ModelChecks.col_check(2)
def check_anim(item):
return str(item) != 'No animation'
@ModelChecks.col_check(3)
def check_bat(item):
if item == "NO_INFO":
return False
return item[1] * 100 > ModelChecks.battery_min
@ModelChecks.col_check(4)
def check_sys_status(item):
return item == "STANDBY"
@ModelChecks.col_check(5)
def check_cal_status(item):
return item == "OK"
@ModelChecks.col_check(6)
def check_mode(item):
return (item != "NO_FCU") and not ("CMODE" in item)
@ModelChecks.col_check(7)
def check_selfcheck(item):
return item == "OK"
@ModelChecks.col_check(8)
def check_pos_status(item):
if item == 'NO_POS':
return False
return not math.isnan(item[0])
@ModelChecks.col_check(9)
def check_start_pos_status(item):
return item != 'NO_POS'
@ModelChecks.col_check(10)
def check_selfcheck(item):
return True
@ModelChecks.col_check(11)
def check_time_delta(item):
return abs(item) < ModelChecks.time_delta_max
class CopterData:
def __init__(self, columns=(), **kwargs):
self._columns = columns
for column in columns:
setattr(self, column, None)
for attr, value in kwargs.items():
setattr(self, attr, value)
def __getitem__(self, key):
if key in self._columns:
return getattr(self, key)
return getattr(self, self._columns[key])
def __setitem__(self, key, value):
if key in self._columns:
setattr(self, key, value)
else:
setattr(self, self._columns[key], value)
class StatedCopterData(CopterData):
def __init__(self, columns=(), checks_defaults=None, checks_class=ModelChecks, **kwargs):
if checks_defaults is None:
checks_defaults = {}
self.__dict__['states'] = CopterData(columns, **checks_defaults)
self.__dict__['checks'] = checks_class
self.__dict__['all_checks'] = None
super().__init__(columns, **kwargs)
def __setattr__(self, key, value):
self.__dict__[key] = value
if key in self._columns:
with suppress(KeyError):
# print(self.__dict__)
self.states.__dict__[key] = self.checks.checks_dict[self._columns.index(key)](value)
self.states.__dict__["all_checks"] = all([self.states[i] for i in ModelChecks.checks_dict.keys()])
# if key == 'start_position':
# if (self.__dict__['current_position'] is not None) and (
# self.__dict__['start_position'] is not None):
# current_pos = get_position(self.__dict__['current_position'])
# start_pos = get_position(self.__dict__['start_position'])
# delta = get_position_delta(current_pos, start_pos)
# if delta != 'NO_POS':
# self.states.__dict__[key] = (delta < ModelChecks.start_pos_delta_max)
# update all_checks and takeoff_ready
# self.states.__dict__["takeoff_ready"] = all(
# [self.states[i] for i in ModelChecks.takeoff_checklist]
# )
def get_position(pos_array):
if pos_array[0] != 'nan' and pos_array != 'NO_POS':
pos = []
for i in range(3):
pos.append(pos_array[i])
else:
pos = 'NO_POS'
return pos
def get_position_delta(pos1, pos2):
if pos1 != 'NO_POS' and pos2 != 'NO_POS':
delta_squared = 0
for i in range(3):
delta_squared += (pos1[i] - pos2[i]) ** 2
return math.sqrt(delta_squared)
return 'NO_POS'
class ModelFormatter:
view_formatters = {}
place_formatters = {}
VIEW_FORMATTER = False
PLACE_FORMATTER = True
@classmethod
def format_view(cls, col, value):
if col in cls.view_formatters:
return cls.view_formatters[col](value)
return value
@classmethod
def format_place(cls, col, value):
if col in cls.place_formatters:
return cls.place_formatters[col](value)
return value
@classmethod
def col_format(cls, col, format_type):
def inner(f):
if format_type:
cls.place_formatters[col] = f
else:
cls.view_formatters[col] = f
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
return inner
@ModelFormatter.col_format(0, ModelFormatter.PLACE_FORMATTER)
def place_id(value):
value = str(value).strip()
# check user hostname spelling http://man7.org/linux/man-pages/man7/hostname.7.html
# '-' (hyphen) not first; latin letters/numbers/hyphens; length form 1 to 63
# or matches command pattern
if re.match("^(?!-)[A-Za-z0-9-]{1,63}$", value) or re.match("^/[A-Za-z0-9]*$", value):
return value
else:
msgbox = QtWidgets.QMessageBox()
msgbox.setWindowTitle("Wrong input for the copter name!")
msgbox.setIcon(QtWidgets.QMessageBox.Critical)
msgbox.setText(
"Wrong input for the copter name!\n"
"Please use only A-Z, a-z, 0-9, and '-' chars.\n"
"Don't use '-' as first char.")
msgbox.exec_()
return None
@ModelFormatter.col_format(3, ModelFormatter.PLACE_FORMATTER)
def place_battery(value):
if isinstance(value, list):
battery_v, battery_p = value
if math.isnan(battery_v) or math.isnan(battery_p):
return "NO_INFO"
return value
@ModelFormatter.col_format(3, ModelFormatter.VIEW_FORMATTER)
def view_battery(value):
if isinstance(value, list):
battery_v, battery_p = value
return "{:.1f}V {:d}%".format(battery_v, int(battery_p * 100))
return value
@ModelFormatter.col_format(7, ModelFormatter.VIEW_FORMATTER)
def view_selfcheck(value):
if isinstance(value, list):
if len(value) == 1:
if len(value[0]) <= 8:
return value[0]
return "ERROR"
return value
@ModelFormatter.col_format(8, ModelFormatter.VIEW_FORMATTER)
def view_selfcheck(value):
if isinstance(value, list):
x, y, z, yaw, frame = value
return "{:.2f} {:.2f} {:.2f} {:d} {}".format(x, y, z, int(yaw), frame)
return value
@ModelFormatter.col_format(9, ModelFormatter.VIEW_FORMATTER)
def view_selfcheck(value):
if isinstance(value, list):
x, y, z = value
return "{:.2f} {:.2f} {:.2f}".format(x, y, z)
return value
@ModelFormatter.col_format(10, ModelFormatter.PLACE_FORMATTER)
def view_last_task(value):
if value is None:
return 'No task'
return value
@ModelFormatter.col_format(11, ModelFormatter.PLACE_FORMATTER)
def place_time_delta(value):
return abs(value - time.time())
@ModelFormatter.col_format(11, ModelFormatter.VIEW_FORMATTER)
def view_time_delta(value):
return "{:.3f}".format(value)
class CopterDataModel(QtCore.QAbstractTableModel):
columns_dict = {'copter_id': 'copter ID',
'git_version': 'version',
'animation_id': ' animation ID ',
'battery': ' battery ',
'fcu_status': 'FCU status',
'calibration_status': 'sensors',
'mode': ' mode ',
'selfcheck': ' checks ',
'current_position': 'current x y z yaw frame_id',
'start_position': ' start x y z ',
'last_task': 'last task',
'time_delta': 'dt',
'config_version': 'configuration',
}
columns = list(columns_dict.keys())
selected_ready_signal = QtCore.pyqtSignal(bool)
selected_takeoff_ready_signal = QtCore.pyqtSignal(bool)
selected_flip_ready_signal = QtCore.pyqtSignal(bool) # TODO fix this signals
selected_calibrating_signal = QtCore.pyqtSignal(bool)
selected_calibration_ready_signal = QtCore.pyqtSignal(bool)
update_data_signal = QtCore.pyqtSignal(int, int, QtCore.QVariant, QtCore.QVariant)
add_client_signal = QtCore.pyqtSignal(object)
remove_row_signal = QtCore.pyqtSignal(int)
remove_client_signal = QtCore.pyqtSignal(object)
def __init__(self, checks=ModelChecks, formatter=ModelFormatter, data_model=StatedCopterData, parent=None):
super(CopterDataModel, self).__init__(parent)
# self.headers = (' copter ID ', ' version ', ' animation ID ', ' battery ', ' fcu_status ', ' sensors ',
# ' mode ', ' checks ', ' current x y z yaw frame_id ', ' start x y z ', ' task ', 'dt')
self.headers = list(self.columns_dict.values())
self.data_contents = []
self.checks = checks
self.formatter = formatter
self.data_model = data_model
self.first_col_is_checked = False
self.update_data_signal.connect(self._update_item)
self.add_client_signal.connect(self._add_client)
self.remove_row_signal.connect(self._remove_row)
self.remove_client_signal.connect(self._remove_row_data)
def insertRows(self, contents, position='last', parent=QtCore.QModelIndex()):
rows = len(contents)
position = len(self.data_contents) if position == 'last' else position
self.beginInsertRows(parent, position, position + rows - 1)
self.data_contents[position:position] = contents
self.endInsertRows()
def removeRows(self, position, rows=1, index=QtCore.QModelIndex()):
self.beginRemoveRows(QtCore.QModelIndex(), position, position + rows - 1)
self.data_contents = self.data_contents[:position] + self.data_contents[position + rows:]
self.endRemoveRows()
return True
@classmethod
def is_column(cls, index, column_name):
return index.column() == cls.columns.index(column_name)
def filter(self, f, contents=()):
contents = contents or self.data_contents
return filter(f, contents)
def user_selected(self, contents=()):
return self.filter(lambda x: x.states.checked == Qt.Checked, contents)
def get_row_data(self, index):
row = index.row()
if row == -1:
return None
try:
return self.data_contents[row]
except IndexError:
return None
def get_row_index(self, row_data):
try:
return self.data_contents.index(row_data)
except ValueError:
return None
def get_row_by_attr(self, attr, value):
try:
return next(filter(lambda x: getattr(x, attr, None) == value, self.data_contents))
except StopIteration:
return None
def rowCount(self, n=None):
return len(self.data_contents)
def columnCount(self, n=None):
return len(self.headers)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.headers[section]
def data(self, index, role=Qt.DisplayRole):
row = index.row()
col = index.column()
if role == Qt.DisplayRole or role == Qt.EditRole: # Separate editRole in case of editing non-text
item = self.data_contents[row][col]
return str(self.formatter.format_view(col, item)) if item is not None else ""
elif role == ModelDataRole:
return self.data_contents[row][col]
elif role == Qt.BackgroundRole:
try:
item = self.data_contents[row]
result = item.states[col]
except KeyError:
return QtGui.QBrush(Qt.white)
else:
if result is None:
return QtGui.QBrush(Qt.yellow)
if result:
return QtGui.QBrush(Qt.green)
else:
return QtGui.QBrush(Qt.red)
elif role == Qt.CheckStateRole and col == 0:
return self.data_contents[row].states.checked
if role == QtCore.Qt.TextAlignmentRole and col != 0:
return QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
def update_model(self, index=QtCore.QModelIndex(), role=QtCore.Qt.EditRole):
selected = set(self.user_selected())
self.selected_ready_signal.emit(selected.issubset(self.filter(lambda x: x.states.all_checks)))
#self.selected_takeoff_ready_signal.emit(selected.issubset(self.filter(lambda x: x.states.takeoff_ready)))
self.selected_flip_ready_signal.emit(selected.issubset(self.filter(flip_checks)))
self.selected_calibrating_signal.emit(selected.issubset(self.filter(calibrating_check)))
self.selected_calibration_ready_signal.emit(selected.issubset(self.filter(calibration_ready_check)))
self.dataChanged.emit(index, index, (role,))
@QtCore.pyqtSlot()
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
col = index.column()
row = index.row()
if role == Qt.CheckStateRole:
self.data_contents[row].states.checked = value
elif role == Qt.EditRole: # For user/outer actions with data, place modifiers applied
formatted_value = self.formatter.format_place(col, value)
if formatted_value is None: # todo use new := syntax
return False
self.data_contents[row][col] = formatted_value
if col == 0:
self.data_contents[row].client.send_message("id", {"new_id": formatted_value})
self.data_contents[row].client.remove() # TODO change
self._remove_row(row)
elif role == ModelDataRole: # For inner setting\editing of data
self.data_contents[row][col] = value
elif role == ModelStateRole:
self.data_contents[row].states[col] = value
else:
return False
self.update_model(index, role)
return True
def select_all(self): # probably NOT thread-safe! TODO remake
self.first_col_is_checked = not self.first_col_is_checked
for row_num, copter in enumerate(self.data_contents):
copter.states.checked = int(self.first_col_is_checked) * 2
self.update_model(self.index(row_num, 0), Qt.CheckStateRole)
def flags(self, index):
roles = Qt.ItemIsSelectable | Qt.ItemIsEnabled
if index.column() == 0:
roles |= Qt.ItemIsUserCheckable | Qt.ItemIsEditable
if self.is_column(index, "config_version"):
roles |= Qt.ItemIsDragEnabled # | Qt.ItemIsDropEnabled
return roles
def supportedDropActions(self):
return QtCore.Qt.CopyAction
def mimeTypes(self):
return ['text/plain']
def mimeData(self, indexes):
index = indexes[0]
if self.is_column(index, "config_version"):
return self._config_mime(index)
return None
def _config_mime(self, index):
mimedata = QtCore.QMimeData()
path = os.path.join(QDir.tempPath(), "config_{}.ini".format(
self.data_contents[index.row()].copter_id))
with suppress(OSError): # remove if file exists
os.remove(path)
self.data_contents[index.row()].client.get_file("config/client.ini", path, )
mimedata.setUrls([QUrl.fromLocalFile(path)])
return mimedata
# Thread-safe wrappers
def add_client(self, **kwargs):
default_states = {"checked": 0, "copter_id": True}
# class_basic_attrs = {'client': None}
# class_basic_states = OrderedDict([("checked", 0), ("selfchecked", None), ("takeoff_ready", None)])
self.add_client_signal.emit(self.data_model(self.columns, default_states, **kwargs))
def remove_client_data(self, row_data):
self.remove_client_signal.emit(row_data)
def remove_row(self, row):
self.remove_row_signal.emit(row)
def update_data(self, row, col, data, role=ModelDataRole):
self.update_data_signal.emit(row, col, data, role)
@QtCore.pyqtSlot(int, int, QtCore.QVariant, QtCore.QVariant)
def _update_item(self, row, col, value, role=Qt.EditRole):
self.setData(self.index(row, col), value, role)
@QtCore.pyqtSlot(object)
def _add_client(self, client):
self.insertRows([client])
@QtCore.pyqtSlot(int) # Probably deprecated now
def _remove_row(self, row):
self.removeRows(row)
@QtCore.pyqtSlot(object)
def _remove_row_data(self, data):
row = self.get_row_index(data)
if row is not None:
self.removeRows(row)
def flip_checks(copter_item):
for col in ModelChecks.takeoff_checklist:
if col != 4 or col != 7:
if not ModelChecks.checks_dict[col](copter_item[col]):
return False
elif copter_item[4] != "ACTIVE":
return False
return True
def calibrating_check(copter_item):
return copter_item[5] == "CALIBRATING"
def calibration_ready_check(copter_item):
if not ModelChecks.checks_dict[4](copter_item[4]):
return False
return not calibrating_check(copter_item)
class CopterProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super(CopterProxyModel, self).__init__(parent)
@staticmethod
def human_sort_prepare(item):
if item:
item = [int(x) if x.isdigit() else x.lower() for x in re.split('([0-9]+)', str(item))]
else:
item = []
return item
def lessThan(self, left, right):
leftData = self.sourceModel().data(left)
rightData = self.sourceModel().data(right)
return self.human_sort_prepare(leftData) < self.human_sort_prepare(rightData)
if __name__ == '__main__':
import threading
import time
def timer():
idc = 1001
while True:
myModel.setData(myModel.index(0, 0), idc)
idc += 1
time.sleep(1)
app = QtWidgets.QApplication.instance()
if app is None:
app = QtWidgets.QApplication(sys.argv)
tableView = QtWidgets.QTableView()
myModel = CopterDataModel()
proxyModel = CopterProxyModel()
proxyModel.setDynamicSortFilter(True)
proxyModel.setSourceModel(myModel)
tableView.setModel(proxyModel)
tableView.verticalHeader().hide()
tableView.setSortingEnabled(True)
tableView.show()
msgs = []
msg = "[{}]: Failure: {}".format("FCU connection", "Angular velocities estimation is not available")
msgs.append(msg)
msg = "[{}]: Failure: {}".format("FCU connection1", "Angular velocities estimation is not available")
msgs.append(msg)
msg = "[{}]: Failure: {}".format("FCU connection2", "Angular velocities estimation is not available")
msgs.append(msg)
#myModel._add_client(StatedCopterData(copter_id=1000, checked=0, selfcheck=msgs, time_utc=1))
#myModel._add_client(StatedCopterData(checked=2, selfcheck="OK", time_utc=2))
#myModel._add_client(StatedCopterData(checked=2, selfcheck="not ok", time_utc="no"))
myModel.add_client(copter_id=1000, client=None)
#myModel.setData(myModel.index(0, 1), "test")
# t = threading.Thread(target=timer, daemon=True)
#t.start()
print(QtCore.QT_VERSION_STR)
app.exec_()