diff --git a/.gitignore b/.gitignore index 2c2068e..88cd199 100644 --- a/.gitignore +++ b/.gitignore @@ -111,3 +111,13 @@ Drone/client_logs images/ \.idea/ + +Drone/_copter_client_old_\.py + +Drone/test_cl\.py + +Server/testj\.ipynb + +Server/tst_client\.py + +Server/tst\.py diff --git a/Server/copter_table_models.py b/Server/copter_table_models.py new file mode 100644 index 0000000..b36332e --- /dev/null +++ b/Server/copter_table_models.py @@ -0,0 +1,247 @@ +import sys +import re +from operator import itemgetter + +from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtCore import Qt as Qt + + +class CopterData: + class_attrs = {'copter_id': None, 'anim_id': None, 'batt_v': None, 'batt_p': None, 'selfcheck': None, + 'time_utc': None, "time_delta": None, "client": None, "checked": 0} + + def __init__(self, **kwargs): + self.attrs = self.class_attrs.copy() + self.attrs.update(kwargs) + + for attr, value in self.attrs.items(): + setattr(self, attr, value) + + def __getitem__(self, key): + return getattr(self, list(self.attrs.keys())[key]) + + def __setitem__(self, key, value): + setattr(self, list(self.attrs.keys())[key], value) + + +class CopterDataModel(QtCore.QAbstractTableModel): + checks = {} + #selected_available = QtCore.pyqtSignal(bool) + + def __init__(self, parent=None): + super(CopterDataModel, self).__init__(parent) + self.headers = ('copter ID', 'animation ID', 'battery (V.)', 'battery (%)', 'selfcheck', 'time UTC', "time delta") + self.data_contents = [] + + 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 user_selected(self): + return filter(lambda x: x.checked == Qt.Checked, self.data_contents) + + def self_checked(self): + return filter(lambda x: all_checks(x), self.data_contents) + + 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: + if orientation == Qt.Horizontal: + return self.headers[section] + + def data(self, index, role=Qt.DisplayRole): + row = index.row() + col = index.column() + #print('row {}, col {}, role {}'.format(row, col, role)) + if role == Qt.DisplayRole: + #print(self.data_contents[row][col]) + return self.data_contents[row][col] or "" + + elif role == Qt.BackgroundRole: + if col in self.checks.keys(): + item = self.data_contents[row][col] + result = self.checks[col](item) + 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].checked + + def update_model(self, index=QtCore.QModelIndex()): + #self.modelReset.emit() + self.dataChanged.emit(index, index, (QtCore.Qt.EditRole,)) + + @QtCore.pyqtSlot() + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid(): + return False + + if role == Qt.CheckStateRole: + self.data_contents[index.row()].checked = value + elif role == Qt.EditRole: + self.data_contents[index.row()][index.column()] = value + self.update_model(index) + else: + return False + + return True + + def flags(self, index): + roles = Qt.ItemIsSelectable | Qt.ItemIsEnabled + if index.column() == 0: + roles |= Qt.ItemIsUserCheckable + return roles + + @QtCore.pyqtSlot(int, int, QtCore.QVariant) + def update_item(self, row, col, value): + self.setData(self.index(row, col), value) + + @QtCore.pyqtSlot(object) + def add_client(self, client): + self.insertRows([client]) + + +def col_check(col): + def inner(f): + CopterDataModel.checks[col] = f + + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + + return wrapper + + return inner + + +@col_check(1) +def check_anim(item): + if not item: + return None + else: + return True + + +@col_check(2) +def check_bat_v(item): + if not item: + return None + if float(item) > 3.2: # todo config + return True + else: + return False + + +@col_check(3) +def check_bat_v(item): + if not item: + return None + if float(item) > 15: # todo config + return True + else: + return False + + +@col_check(4) +def check_selfcheck(item): + if not item: + return None + if item == "OK": + return True + else: + return False + + +def all_checks(copter_item): + for col, check in CopterDataModel.checks.items(): + if not check(copter_item[col]): + return False + return True + + +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) + + +''' + def sort(self, col, order): + self.layoutAboutToBeChanged.emit() + + self.data_contents = sorted(self.data_contents, key=lambda item: self.sorter(item[col]), + reverse=(order == Qt.DescendingOrder)) + + self.layoutChanged.emit() +''' + + +class SignalManager(QtCore.QObject): + update_data_signal = QtCore.pyqtSignal(int, int, QtCore.QVariant) + add_client_signal = QtCore.pyqtSignal(object) + + +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(None) + proxyModel = CopterProxyModel() + + proxyModel.setDynamicSortFilter(True) + proxyModel.setSourceModel(myModel) + + tableView.setModel(proxyModel) + + tableView.verticalHeader().hide() + tableView.setSortingEnabled(True) + + tableView.show() + myModel.add_client(CopterData(copter_id=1000, checked=0, time_utc=1)) + myModel.add_client(CopterData(checked=2, selfcheck="OK", time_utc=2)) + myModel.add_client(CopterData(checked=2, selfcheck="not ok", time_utc="no")) + + myModel.setData(myModel.index(0, 1), "test") + + 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 b0c9888..6dc7804 100644 --- a/Server/server_qt.py +++ b/Server/server_qt.py @@ -3,7 +3,7 @@ import glob from PyQt5 import QtWidgets from PyQt5.QtGui import QStandardItemModel, QStandardItem -from PyQt5.QtCore import Qt, pyqtSlot +from PyQt5.QtCore import Qt, pyqtSlot, QAbstractTableModel from PyQt5.QtWidgets import QFileDialog, QMessageBox @@ -11,21 +11,50 @@ from PyQt5.QtWidgets import QFileDialog, QMessageBox from server_gui import Ui_MainWindow from server import * +from copter_table_models import * from emergency import * +class MyTableModel(QAbstractTableModel): + def __init__(self, parent, headers, *args): + QAbstractTableModel.__init__(self, parent, *args) + + # noinspection PyArgumentList,PyCallByClass class MainWindow(QtWidgets.QMainWindow): def __init__(self): super(MainWindow, self).__init__() + self.ui = Ui_MainWindow() self.ui.setupUi(self) self.init_ui() + + self.model = CopterDataModel() + self.proxy_model = CopterProxyModel() + self.signals = SignalManager() + + self.init_model() + self.show() + + def init_model(self): + self.proxy_model.setDynamicSortFilter(True) + self.proxy_model.setSourceModel(self.model) + + # Initing table and table self.model + self.ui.tableView.setModel(self.proxy_model) + self.ui.tableView.horizontalHeader().setStretchLastSection(True) + self.ui.tableView.setSortingEnabled(True) + + self.signals.update_data_signal.connect(self.model.update_item) + self.signals.add_client_signal.connect(self.model.add_client) + + def client_connected(self, client: Client): + self.signals.add_client_signal.emit(CopterData(copter_id=client.copter_id, client=client)) def init_ui(self): # Connecting - self.ui.check_button.clicked.connect(self.check_selected) + self.ui.check_button.clicked.connect(self.selfcheck_selected) self.ui.start_button.clicked.connect(self.send_starttime) self.ui.pause_button.clicked.connect(self.pause_all) self.ui.stop_button.clicked.connect(self.stop_all) @@ -40,41 +69,42 @@ class MainWindow(QtWidgets.QMainWindow): self.ui.action_send_configurations.triggered.connect(self.send_configurations) self.ui.action_send_Aruco_map.triggered.connect(self.send_aruco) - # Initing table and table model - self.ui.tableView.setModel(model) - self.ui.tableView.horizontalHeader().setStretchLastSection(True) - @pyqtSlot() - def check_selected(self): - for row_num in range(model.rowCount()): - item = model.item(row_num, 0) - if item.isCheckable() and item.checkState() == Qt.Checked: - print("Copter {} checked".format(model.item(row_num, 0).text())) - copter = Client.get_by_id(item.text()) - copter.get_response("anim_id", self._set_copter_data, callback_args=(row_num, 1)) - copter.get_response("batt_voltage", self._set_copter_data, callback_args=(row_num, 2)) - copter.get_response("cell_voltage", self._set_copter_data, callback_args=(row_num, 3)) - copter.get_response("selfcheck", self._set_copter_data, callback_args=(row_num, 4)) - copter.get_response("time", self._set_copter_data, callback_args=(row_num, 5)) + def selfcheck_selected(self): + for copter_data in self.model.user_selected(): + copter = copter_data.client - self.ui.start_button.setEnabled(True) - self.ui.takeoff_button.setEnabled(True) + copter.get_response("anim_id", self._set_copter_data, callback_args=(1, copter_data.copter_id)) + copter.get_response("batt_voltage", self._set_copter_data, callback_args=(2, copter_data.copter_id)) + copter.get_response("cell_voltage", self._set_copter_data, callback_args=(3, copter_data.copter_id)) + copter.get_response("selfcheck", self._set_copter_data, callback_args=(4, copter_data.copter_id)) + copter.get_response("time", self._set_copter_data, callback_args=(5, copter_data.copter_id)) + + #self.ui.start_button.setEnabled(True) + #self.ui.takeoff_button.setEnabled(True) + + def _set_copter_data(self, value, col, copter_id): + row = self.model.data_contents.index(next( + filter(lambda x: x.copter_id == copter_id, self.model.data_contents))) - def _set_copter_data(self, value, row, col): if col == 1: - model.setData(model.index(row, col), value) + data = value elif col == 2: - model.setData(model.index(row, col), "{} V".format(round(float(value), 3))) + data = "{} V.".format(round(float(value), 3)) elif col == 3: - batt_percent = ((float(value) - 3.2) / (4.2 - 3.2)) * 100 - model.setData(model.index(row, col), "{} %".format(round(batt_percent, 3))) + batt_percent = ((float(value) - 3.2) / (4.2 - 3.2)) * 100 # TODO config + data = "{} %".format(round(batt_percent, 3)) elif col == 4: - if value != "OK": - model.setData(model.index(row, col), str(value)) # TODO different handling - else: - model.setData(model.index(row, col), str(value)) + data = str(value) elif col == 5: - model.setData(model.index(row, col), time.ctime(int(value))) + data = time.ctime(int(value)) + data2 = "{} sec.".format(round(int(value) - time.time(), 3)) + self.signals.update_data_signal.emit(row, col + 1, data2) + else: + print("No column matched for response") + return + + self.signals.update_data_signal.emit(row, col, data) @pyqtSlot() def send_starttime(self): @@ -86,8 +116,8 @@ class MainWindow(QtWidgets.QMainWindow): ) if reply == QMessageBox.Yes: print("Accepted") - for row_num in range(model.rowCount()): - item = model.item(row_num, 0) + for row_num in range(self.model.rowCount()): + item = self.model.item(row_num, 0) if item.isCheckable() and item.checkState() == Qt.Checked: if True: # TODO checks for batt/selfckeck here copter = Client.get_by_id(item.text()) @@ -110,10 +140,10 @@ class MainWindow(QtWidgets.QMainWindow): @pyqtSlot() def test_leds(self): - for row_num in range(model.rowCount()): - item = model.item(row_num, 0) + for row_num in range(self.model.rowCount()): + item = self.model.item(row_num, 0) if item.isCheckable() and item.checkState() == Qt.Checked: - if True: # TODO checks for batt/selfckeck here + if True: copter = Client.get_by_id(item.text()) copter.send_message("led_test") @@ -126,8 +156,8 @@ class MainWindow(QtWidgets.QMainWindow): ) if reply == QMessageBox.Yes: print("Accepted") - for row_num in range(model.rowCount()): - item = model.item(row_num, 0) + for row_num in range(self.model.rowCount()): + item = self.model.item(row_num, 0) if item.isCheckable() and item.checkState() == Qt.Checked: if True: # TODO checks for batt/selfckeck here copter = Client.get_by_id(item.text()) @@ -154,8 +184,8 @@ class MainWindow(QtWidgets.QMainWindow): names = [os.path.basename(file).split(".")[0] for file in files] print(files) for file, name in zip(files, names): - for row_num in range(model.rowCount()): - item = model.item(row_num, 0) + for row_num in range(self.model.rowCount()): + item = self.model.item(row_num, 0) if item.isCheckable() and item.checkState() == Qt.Checked: copter = Client.get_by_id(item.text()) if name == copter.copter_id: @@ -176,8 +206,9 @@ class MainWindow(QtWidgets.QMainWindow): value = sendable_config[section][option] logging.debug("Got item from config:".format(section, option, value)) options.append(ConfigOption(section, option, value)) - for row_num in range(model.rowCount()): - item = model.item(row_num, 0) + + for row_num in range(self.model.rowCount()): + item = self.model.item(row_num, 0) if item.isCheckable() and item.checkState() == Qt.Checked: copter = Client.get_by_id(item.text()) copter.send_config_options(*options) @@ -188,16 +219,16 @@ class MainWindow(QtWidgets.QMainWindow): if path: filename = os.path.basename(path) print("Selected file:", path, filename) - for row_num in range(model.rowCount()): - item = model.item(row_num, 0) + for row_num in range(self.model.rowCount()): + item = self.model.item(row_num, 0) if item.isCheckable() and item.checkState() == Qt.Checked: copter = Client.get_by_id(item.text()) copter.send_file(path, "/home/pi/catkin_ws/src/clever/aruco_pose/map/animation_map.txt") copter.send_message("service_restart", {"name": "clever"}) @pyqtSlot() def emergency(self): - for row_num in range(model.rowCount()): - item = model.item(row_num, 0) + for row_num in range(self.model.rowCount()): + item = self.model.item(row_num, 0) if item.isCheckable() and item.checkState() == Qt.Checked: copter = Client.get_by_id(item.text()) copter.send_message("emergency") @@ -216,8 +247,8 @@ class MainWindow(QtWidgets.QMainWindow): ) if reply == QMessageBox.Yes: print("Accepted") - for row_num in range(model.rowCount()): - item = model.item(row_num, 0) + for row_num in range(self.model.rowCount()): + item = self.model.item(row_num, 0) if item.isCheckable() and item.checkState() == Qt.Checked: if True: # TODO checks for batt/selfckeck here copter = Client.get_by_id(item.text()) @@ -226,29 +257,13 @@ class MainWindow(QtWidgets.QMainWindow): print("Cancelled") pass - - -model = QStandardItemModel() -model.setHorizontalHeaderLabels( - ('copter ID', 'animation ID', 'battery V', 'battery %', 'selfcheck', 'time UTC') -) -model.setColumnCount(6) -model.setRowCount(0) - - -def client_connected(self: Client): - copter_id_item = QStandardItem(self.copter_id) - copter_id_item.setCheckable(True) - model.appendRow((copter_id_item, )) - - -Client.on_first_connect = client_connected - if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) window = MainWindow() + Client.on_first_connect = window.client_connected + server = Server(on_stop=app.quit) server.start()