From d3c06ea2b7715f0f3115a2177fbb6f61cde5e58b Mon Sep 17 00:00:00 2001 From: Artem30801 Date: Tue, 28 Jan 2020 19:05:16 +0300 Subject: [PATCH] CopterData and table update --- Server/copter_table.py | 17 +-- Server/copter_table_models.py | 277 +++++++++++++++++----------------- Server/server_qt.py | 18 +-- 3 files changed, 150 insertions(+), 162 deletions(-) diff --git a/Server/copter_table.py b/Server/copter_table.py index 725c12b..598e884 100644 --- a/Server/copter_table.py +++ b/Server/copter_table.py @@ -13,23 +13,20 @@ import copter_table_models as table class CopterTableWidget(QTableView): - def __init__(self, model, config, data_model=table.StatedCopterData): + def __init__(self, model, config): QTableView.__init__(self) self.config = config self.model = model - self._data_model = data_model self.proxy_model = table.CopterProxyModel() - self.signals = table.SignalManager(self.model) - self.proxy_model.setSourceModel(self.model) self.proxy_model.setDynamicSortFilter(True) # Initiate table and table self.model self.setModel(self.proxy_model) - self.columns = list(table.columns_names.keys()) #[header.strip() for header in self.model.headers] # header keys + self.columns = self.model.columns #[header.strip() for header in self.model.headers] # header keys self.current_columns = self.columns[:] header = self.horizontalHeader() @@ -78,16 +75,6 @@ class CopterTableWidget(QTableView): for name, show in item_dict.items(): # for index, name in enumerate(self.columns): self.setColumnHidden(self.columns.index(name), not show) # self.setColumnHidden(index, not item_dict.get(name, False)) - # Some fancy wrappers to simplify syntax - def add_client(self, **kwargs): - self.signals.add_client_signal.emit(self._data_model(**kwargs)) - - def remove_client_data(self, row_data): - self.signals.remove_client_signal.emit(row_data) - - def update_data(self, row, col, data, role=table.ModelDataRole): - self.signals.update_data_signal.emit(row, col, data, role) - @pyqtSlot(QtCore.QModelIndex) def on_double_click(self, index): col = index.column() diff --git a/Server/copter_table_models.py b/Server/copter_table_models.py index d414760..739c4ad 100644 --- a/Server/copter_table_models.py +++ b/Server/copter_table_models.py @@ -1,14 +1,12 @@ +import os import re import sys -import os import time import math -import indexed from contextlib import suppress from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import Qt as Qt, QUrl, QDir -from PyQt5.QtWidgets import QApplication ModelDataRole = 998 ModelStateRole = 999 @@ -18,8 +16,8 @@ class ModelChecks: checks_dict = {} takeoff_checklist = (3, 4, 6, 7, 8) - battery_min = 50.0 # config.getfloat('CHECKS', 'battery_percentage_min') - start_pos_delta_max = 1.0 # config.getfloat('CHECKS', 'start_pos_delta_max') + battery_min = 50.0 + start_pos_delta_max = 1.0 time_delta_max = 1.0 @classmethod @@ -64,7 +62,7 @@ def check_anim(item): def check_bat(item): if item == "NO_INFO": return False - return item[1]*100 > ModelChecks.battery_min + return item[1] * 100 > ModelChecks.battery_min @ModelChecks.col_check(4) @@ -98,87 +96,76 @@ def check_pos_status(item): 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 -columns_names = {'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_names.keys()) + class CopterData: - class_basic_attrs = indexed.IndexedOrderedDict([('copter_id', None), ('git_ver', None), ('animation_id', None), - ('battery', None), ('fcu_status', None), ('calibration_status', None), - ('mode', None), ('selfcheck', None), ('current_position', None), - ('start_position', None), ('last_task', None), ('time_delta', None), - ("config_version", None), ('client', None)]) + def __init__(self, columns=(), **kwargs): + self._columns = columns + for column in columns: + setattr(self, column, None) - def __init__(self, **kwargs): - self.attrs_dict = self.class_basic_attrs.copy() - self.attrs_dict.update(kwargs) - - for attr, value in self.attrs_dict.items(): + for attr, value in kwargs.items(): setattr(self, attr, value) def __getitem__(self, key): - return getattr(self, self.attrs_dict.keys()[key]) + if key in self._columns: + return getattr(self, key) + return getattr(self, self._columns[key]) def __setitem__(self, key, value): - setattr(self, self.attrs_dict.keys()[key], value) + if key in self._columns: + setattr(self, key, value) + else: + setattr(self, self._columns[key], value) class StatedCopterData(CopterData): - class_basic_states = indexed.IndexedOrderedDict([("checked", 0), ("selfchecked", None), ("takeoff_ready", None), - ("copter_id", True), ]) + def __init__(self, columns=(), checks_defaults=None, checks_class=ModelChecks, **kwargs): + if checks_defaults is None: + checks_defaults = {} - def __init__(self, checks_class=ModelChecks, **kwargs): - self.states = CopterData(**self.class_basic_states) - self.checks = checks_class + self.__dict__['states'] = CopterData(columns, **checks_defaults) + self.__dict__['checks'] = checks_class + self.__dict__['all_checks'] = None - super(StatedCopterData, self).__init__(**kwargs) + super().__init__(columns, **kwargs) def __setattr__(self, key, value): self.__dict__[key] = value - if key in self.class_basic_attrs.keys(): - try: - self.states.__dict__[key] = \ - ModelChecks.checks_dict[self.attrs_dict.keys().index(key)](value) - 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) - except KeyError: # No check present for that col - pass - else: # update selfchecked and takeoff_ready - self.states.__dict__["selfchecked"] = all( - [self.states[i] for i in ModelChecks.checks_dict.keys()] - ) + 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] + # ) - 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': @@ -194,7 +181,7 @@ 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 + delta_squared += (pos1[i] - pos2[i]) ** 2 return math.sqrt(delta_squared) return 'NO_POS' @@ -266,18 +253,20 @@ def place_battery(value): 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 "{:.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) == 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): @@ -285,6 +274,7 @@ def view_selfcheck(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): @@ -292,44 +282,71 @@ def view_selfcheck(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()) + return abs(value - time.time()) + @ModelFormatter.col_format(11, ModelFormatter.VIEW_FORMATTER) def view_time_delta(value): return "{:.3f}".format(value) -def is_column(index, column_name): - return index.column() == columns.index(column_name) - - 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) - def __init__(self, checks=ModelChecks, formatter=ModelFormatter, parent=None): + 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(columns_names.values()) + 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 @@ -345,56 +362,37 @@ class CopterDataModel(QtCore.QAbstractTableModel): 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=()): - contents = contents or self.data_contents - return filter(lambda x: x.states.checked == Qt.Checked, contents) - - def selfchecked_ready(self, contents=()): - contents = contents or self.data_contents - return filter(lambda x: x.states.selfchecked, contents) - - def takeoff_ready(self, contents=()): - contents = contents or self.data_contents - return filter(lambda x: x.states.takeoff_ready, contents) - - def flip_ready(self, contents=()): - contents = contents or self.data_contents - return filter(flip_checks, contents) # possibly change as takeoff checks - - def calibrating(self, contents=()): - contents = contents or self.data_contents - return filter(calibrating_check, contents) - - def calibration_ready(self, contents=()): - contents = contents or self.data_contents - return filter(calibration_ready_check, 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: - data = self.data_contents[row] + return self.data_contents[row] except IndexError: return None - else: - return data def get_row_index(self, row_data): try: - index = self.data_contents.index(row_data) + return self.data_contents.index(row_data) except ValueError: return None - else: - return index def get_row_by_attr(self, attr, value): try: - row_data = next(filter(lambda x: getattr(x, attr, None) == value, self.data_contents)) + return next(filter(lambda x: getattr(x, attr, None) == value, self.data_contents)) except StopIteration: return None - else: - return row_data def rowCount(self, n=None): return len(self.data_contents) @@ -438,12 +436,11 @@ class CopterDataModel(QtCore.QAbstractTableModel): def update_model(self, index=QtCore.QModelIndex(), role=QtCore.Qt.EditRole): selected = set(self.user_selected()) - self.selected_ready_signal.emit(selected.issubset(self.selfchecked_ready())) - self.selected_takeoff_ready_signal.emit(selected.issubset(self.takeoff_ready())) - - self.selected_flip_ready_signal.emit(selected.issubset(self.flip_ready())) - self.selected_calibrating_signal.emit(selected.issubset(self.calibrating())) - self.selected_calibration_ready_signal.emit(selected.issubset(self.calibration_ready())) + 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,)) @@ -467,7 +464,7 @@ class CopterDataModel(QtCore.QAbstractTableModel): 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) + self._remove_row(row) elif role == ModelDataRole: # For inner setting\editing of data self.data_contents[row][col] = value @@ -482,15 +479,15 @@ class CopterDataModel(QtCore.QAbstractTableModel): 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 + 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 is_column(index, "config_version"): - roles |= Qt.ItemIsDragEnabled # | Qt.ItemIsDropEnabled + if self.is_column(index, "config_version"): + roles |= Qt.ItemIsDragEnabled # | Qt.ItemIsDropEnabled return roles @@ -502,7 +499,7 @@ class CopterDataModel(QtCore.QAbstractTableModel): def mimeData(self, indexes): index = indexes[0] - if is_column(index, "config_version"): + if self.is_column(index, "config_version"): return self._config_mime(index) return None @@ -515,25 +512,41 @@ class CopterDataModel(QtCore.QAbstractTableModel): with suppress(OSError): # remove if file exists os.remove(path) - self.data_contents[index.row()].client.get_file("config/client.ini", 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): + 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): + def _add_client(self, client): self.insertRows([client]) @QtCore.pyqtSlot(int) # Probably deprecated now - def remove_row(self, row): + def _remove_row(self, row): self.removeRows(row) @QtCore.pyqtSlot(object) - def remove_row_data(self, data): + def _remove_row_data(self, data): row = self.get_row_index(data) if row is not None: self.removeRows(row) @@ -577,25 +590,12 @@ class CopterProxyModel(QtCore.QSortFilterProxyModel): return self.human_sort_prepare(leftData) < self.human_sort_prepare(rightData) -class SignalManager(QtCore.QObject): - 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, model): - super().__init__() - - self.update_data_signal.connect(model.update_item) - self.add_client_signal.connect(model.add_client) - self.remove_row_signal.connect(model.remove_row) - self.remove_client_signal.connect(model.remove_row_data) - if __name__ == '__main__': import threading import time + def timer(): idc = 1001 while True: @@ -603,11 +603,12 @@ if __name__ == '__main__': idc += 1 time.sleep(1) + app = QtWidgets.QApplication.instance() if app is None: app = QtWidgets.QApplication(sys.argv) tableView = QtWidgets.QTableView() - myModel = CopterDataModel(None) + myModel = CopterDataModel() proxyModel = CopterProxyModel() proxyModel.setDynamicSortFilter(True) @@ -628,14 +629,14 @@ if __name__ == '__main__': 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(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") - myModel.setData(myModel.index(0, 1), "test") - - t = threading.Thread(target=timer, daemon=True) - t.start() + # t = threading.Thread(target=timer, daemon=True) + #t.start() print(QtCore.QT_VERSION_STR) app.exec_() diff --git a/Server/server_qt.py b/Server/server_qt.py index 8292258..84c59e5 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -187,7 +187,7 @@ class MainWindow(QtWidgets.QMainWindow): # TODO if any connected copters reply = QMessageBox.question(self, "Confirm exit", "There are copters connected to the server. " "Are you sure you want to exit?", - QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + QMessageBox.No | QMessageBox.Yes, QMessageBox.No) if reply != QMessageBox.Yes: event.ignore() @@ -206,7 +206,7 @@ class MainWindow(QtWidgets.QMainWindow): def new_client_connected(self, client: Client): logging.debug("Added client {}".format(client)) - self.ui.copter_table.add_client(copter_id=client.copter_id, client=client) + self.model.add_client(copter_id=client.copter_id, client=client) def client_connection_changed(self, client: Client): logging.debug("Connection {} changed {}".format(client, client.connected)) @@ -218,12 +218,12 @@ class MainWindow(QtWidgets.QMainWindow): if self.server.config.table_remove_disconnected and (not client.connected): client.remove() - self.ui.copter_table.remove_client_data(row_data) + self.model.remove_client_data(row_data) logging.debug("Removing from table") else: row_num = self.model.get_row_index(row_data) if row_num is not None: - self.ui.copter_table.update_data(row_num, 0, client.connected, table.ModelStateRole) + self.model.update_data(row_num, 0, client.connected, table.ModelStateRole) logging.debug("Client status updated") @pyqtSlot() @@ -257,14 +257,14 @@ class MainWindow(QtWidgets.QMainWindow): row_data = self.model.get_row_by_attr("client", client) row_num = self.model.get_row_index(row_data) if row_num is not None: - self.ui.copter_table.update_data(row_num, col, value, Qt.EditRole) + self.model.update_data(row_num, col, value, Qt.EditRole) @pyqtSlot() def remove_selected(self): for copter in self.model.user_selected(): copter.client.remove() if not self.server.config.table_remove_disconnected: - self.ui.copter_table.remove_client_data(copter) + self.model.remove_client_data(copter) logging.info("Client removed from table!") @pyqtSlot() @@ -316,7 +316,7 @@ class MainWindow(QtWidgets.QMainWindow): row = self.model.get_row_index(copter_data_row) col = 5 data = 'CALIBRATING' - self.ui.copter_table.update_data(row, col, data, table.ModelDataRole) + self.model.update_data(row, col, data, table.ModelDataRole) # Send request client.get_response("calibrate_gyro", self._get_calibration_info) @@ -328,7 +328,7 @@ class MainWindow(QtWidgets.QMainWindow): row = self.model.get_row_index(copter_data_row) col = 5 data = 'CALIBRATING' - self.ui.copter_table.update_data(row, col, data, table.ModelDataRole) + self.model.update_data(row, col, data, table.ModelDataRole) # Send request client.get_response("calibrate_level", self._get_calibration_info) @@ -338,7 +338,7 @@ class MainWindow(QtWidgets.QMainWindow): row = self.model.get_row_index(row_data) if row is not None: data = str(value) - self.ui.copter_table.update_data(row, col, data, table.ModelDataRole) + self.model.update_data(row, col, data, table.ModelDataRole) def _send_files(self, files, copters=None, client_path="", client_filename="", match_id=False, callback=None): if copters is None: