mirror of
https://github.com/CopterExpress/clever-show.git
synced 2026-05-27 07:29:33 +00:00
New! Realization of table model for storing all copter data. Auto refresh, sorting, data checks and coloring based on checks, thread-safety
This commit is contained in:
10
.gitignore
vendored
10
.gitignore
vendored
@@ -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
|
||||
|
||||
247
Server/copter_table_models.py
Normal file
247
Server/copter_table_models.py
Normal file
@@ -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_()
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user