mirror of
https://github.com/CopterExpress/clever-show.git
synced 2026-06-03 18:49:32 +00:00
Feature branch: IMPORTANT connection+telemetry+table fixes and improvements (#55)
* .client_connected > .new_client_connected
* Fixed 'confirmation_required' wrapper
* Logging impr
* Changed and optimized a lot checks behaviour
* Added indication of connected/disconnected copters
* update_data_signal changed signature
* Added client removing functionality
* Option for automatically remove disconnected copters from table
* Renaming copters from QT server table on the go + some improvements
* Server: Check if self.clients list is not empty when trying to pop element from it
* Probably fixes behaviour of non-immidiate data sending from server
* Added changing hostname of copter
* Updated config
* Preview of selfchecheck results on double click
* Delete doc_2019-10-16_17-57-17.bashrc
* Update table data models for selfcheck
* Server: modify set id request to message
* Update client_config default file
* Client: modify set new id function
* Client: add avahi-daemon to restart when restarting network
* Client: add new hostname to ssh motd message, do not change hostname if no network restart in config
* Client: add newline to motd message
* Optimized request behaviour
* Client: fix service file and restart order
* Client: Add SO_KEEPALIVE and TCP_NODELAY options to client socket
* Modify to last tests with ping
* Client: remove ping
* Client: select reboot option when change id and add execute command
* Server: Add SO_KEEPALIVE option to server socket
* Server: Change removing copter
* Request resending after disconnection
* Resending improval (for furthrer functionality & fixes
* Fix of client removing behaviour
* Debugging
* Revert dubug code; 'Remove' fix confirmed
* do not clear requests queue
* Update requirements.txt
* Added namespace class to fix resend
* Improvements and simplification of notifier + port to client
* Refactor of telemetry thread
* Simplify lambdas
* Compress hostname check to single regex
* Changes in telemetry
* Refactored formatting of telemetry in table. NOT DONE
* Fix
* Git checkout. REVERT later!
* Conection fix
* Compability fixes
* Update start position
* Fix for reconnection with notifier socket
* Added traceback for pyqt5
* Fixes in new telemetry display
* Added lock to Telemetry
* Fixes for table display
* Fix of doubling line of client in table
* Fix of mass-removing clients from table
* Fix for clinet double-connection+removal
* Fix lock in Telemetry
* Changed signature of response callbacks for better syntax & fixes (all tested)
* Revert "Git checkout. REVERT later!"
This reverts commit 6122352380.
* Server: fix formatters
* Client: Remove telemetry_loop, small refactor of Telemetry class
* Server: Add formatters
* Server: Very small refactor
* Server: Fix checks and formatters
* Client: Fix check_failsafe function, small code refactor
* Client: update default config file
This commit is contained in:
committed by
Arthur Golubtsov
parent
53dad0e3fd
commit
ce36c6f1e3
@@ -1,10 +1,10 @@
|
||||
import sys
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import math
|
||||
import configparser
|
||||
import collections
|
||||
import indexed
|
||||
from server import ConfigOption
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from PyQt5.QtCore import Qt as Qt
|
||||
@@ -16,11 +16,100 @@ ModelStateRole = 999
|
||||
config = configparser.ConfigParser()
|
||||
config.read("server_config.ini")
|
||||
|
||||
battery_min = config.getfloat('CHECKS', 'battery_percentage_min')
|
||||
start_pos_delta_max = config.getfloat('CHECKS', 'start_pos_delta_max')
|
||||
time_delta_max = config.getfloat('CHECKS', 'time_delta_max')
|
||||
|
||||
class ModelChecks:
|
||||
checks_dict = {}
|
||||
takeoff_checklist = (3, 4, 6, 7, 8)
|
||||
|
||||
@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 > 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_time_delta(item):
|
||||
return abs(item) < time_delta_max
|
||||
|
||||
|
||||
class CopterData:
|
||||
class_basic_attrs = indexed.IndexedOrderedDict([('copter_id', None), ('git_ver', None), ('anim_id', None),
|
||||
('battery', None), ('sys_status', None), ('cal_status', None),
|
||||
('mode', None), ('selfcheck', None), ('position', None),
|
||||
('mode', None), ('selfcheck', None), ('position', None),
|
||||
('start_pos', None), ('time_delta', None), ('client', None)])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -41,8 +130,9 @@ class StatedCopterData(CopterData):
|
||||
class_basic_states = indexed.IndexedOrderedDict([("checked", 0), ("selfchecked", None), ("takeoff_ready", None),
|
||||
("copter_id", True), ])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, checks_class=ModelChecks, **kwargs):
|
||||
self.states = CopterData(**self.class_basic_states)
|
||||
self.checks = ModelChecks
|
||||
|
||||
super(StatedCopterData, self).__init__(**kwargs)
|
||||
|
||||
@@ -52,31 +142,30 @@ class StatedCopterData(CopterData):
|
||||
if key in self.class_basic_attrs.keys():
|
||||
try:
|
||||
self.states.__dict__[key] = \
|
||||
Checks.all_checks[self.attrs_dict.keys().index(key)](value)
|
||||
ModelChecks.checks_dict[self.attrs_dict.keys().index(key)](value)
|
||||
if key == 'start_pos':
|
||||
if (self.__dict__['position'] is not None) and (self.__dict__['start_pos'] is not None):
|
||||
current_pos = get_position(self.__dict__['position'])
|
||||
start_pos = get_position(self.__dict__['start_pos'])
|
||||
delta = get_position_delta(current_pos, start_pos)
|
||||
if delta != 'NO_POS':
|
||||
self.states.__dict__[key] = (delta < Checks.start_pos_delta_max)
|
||||
self.states.__dict__[key] = (delta < 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 Checks.all_checks.keys()]
|
||||
[self.states[i] for i in ModelChecks.checks_dict.keys()]
|
||||
)
|
||||
|
||||
self.states.__dict__["takeoff_ready"] = all(
|
||||
[self.states[i] for i in Checks.takeoff_checklist]
|
||||
[self.states[i] for i in ModelChecks.takeoff_checklist]
|
||||
)
|
||||
|
||||
def get_position(pos_string):
|
||||
pos = []
|
||||
pos_str = pos_string.split(' ')
|
||||
if pos_str[0] != 'nan' and pos_str[0] != 'NO_POS':
|
||||
def get_position(pos_array):
|
||||
if pos_array[0] != 'nan' and pos_array != 'NO_POS':
|
||||
pos = []
|
||||
for i in range(3):
|
||||
pos.append(float(pos_str[i]))
|
||||
pos.append(pos_array[i])
|
||||
else:
|
||||
pos = 'NO_POS'
|
||||
return pos
|
||||
@@ -91,27 +180,120 @@ def get_position_delta(pos1, pos2):
|
||||
return 'NO_POS'
|
||||
|
||||
|
||||
class Checks:
|
||||
all_checks = {}
|
||||
takeoff_checklist = (3, 4, 6, 7, 8)
|
||||
battery_min = config.getfloat('CHECKS', 'battery_percentage_min')
|
||||
start_pos_delta_max = config.getfloat('CHECKS', 'start_pos_delta_max')
|
||||
time_delta_max = config.getfloat('CHECKS', 'time_delta_max')
|
||||
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 = value.stip()
|
||||
# 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):
|
||||
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 place_time_delta(value):
|
||||
return abs(value - time.time())
|
||||
|
||||
|
||||
@ModelFormatter.col_format(10, ModelFormatter.VIEW_FORMATTER)
|
||||
def view_time_delta(value):
|
||||
return "{:.3f}".format(value)
|
||||
|
||||
class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
selected_ready_signal = QtCore.pyqtSignal(bool)
|
||||
selected_takeoff_ready_signal = QtCore.pyqtSignal(bool)
|
||||
selected_flip_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, parent=None):
|
||||
def __init__(self, checks=ModelChecks, formatter=ModelFormatter, parent=None):
|
||||
super(CopterDataModel, self).__init__(parent)
|
||||
self.headers = ('copter ID', 'version', ' animation ID ', ' battery ', ' system ', 'sensors',
|
||||
' mode ', 'checks', 'current x y z yaw frame_id', ' start x y z ', 'dt')
|
||||
self.data_contents = []
|
||||
|
||||
self.on_id_changed = None
|
||||
self.checks = checks
|
||||
self.formatter = formatter
|
||||
|
||||
self.first_col_is_checked = False
|
||||
|
||||
@@ -144,15 +326,15 @@ class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
|
||||
def flip_ready(self, contents=()):
|
||||
contents = contents or self.data_contents
|
||||
return filter(lambda x: flip_checks(x), contents) # possibly change as takeoff checks
|
||||
return filter(flip_checks, contents) # possibly change as takeoff checks
|
||||
|
||||
def calibrating(self, contents=()):
|
||||
contents = contents or self.data_contents
|
||||
return filter(lambda x: calibrating_check(x), contents)
|
||||
return filter(calibrating_check, contents)
|
||||
|
||||
def calibration_ready(self, contents=()):
|
||||
contents = contents or self.data_contents
|
||||
return filter(lambda x: calibration_ready_check(x), contents)
|
||||
return filter(calibration_ready_check, contents)
|
||||
|
||||
def get_row_index(self, row_data):
|
||||
try:
|
||||
@@ -186,7 +368,7 @@ class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
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(item) if item is not None else ""
|
||||
return str(self.formatter.format_view(col, item)) if item is not None else ""
|
||||
elif role == ModelDataRole:
|
||||
return self.data_contents[row][col]
|
||||
|
||||
@@ -208,7 +390,7 @@ class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
return self.data_contents[row].states.checked
|
||||
|
||||
if role == QtCore.Qt.TextAlignmentRole and col != 0:
|
||||
return QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
|
||||
return QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter
|
||||
|
||||
def update_model(self, index=QtCore.QModelIndex(), role=QtCore.Qt.EditRole):
|
||||
selected = set(self.user_selected())
|
||||
@@ -232,19 +414,14 @@ class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
|
||||
if role == Qt.CheckStateRole:
|
||||
self.data_contents[row].states.checked = value
|
||||
elif role == Qt.EditRole: # For user actions with data
|
||||
if col == 0:
|
||||
# check user hostname spelling http://man7.org/linux/man-pages/man7/hostname.7.html
|
||||
if value[0] != '-' and len(value) <= 63 and re.match("^[A-Za-z0-9-]*$", value):
|
||||
self.data_contents[row].client.send_message("id", {"new_id": 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 not None: # todo use new := syntax
|
||||
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()
|
||||
else:
|
||||
msg = QtWidgets.QMessageBox()
|
||||
msg.setIcon(QtWidgets.QMessageBox.Critical)
|
||||
msg.setText("Wrong input for the copter name!\nPlease use only A-Z, a-z, 0-9, and '-' chars.\nDon't use '-' as first char.")
|
||||
msg.exec_()
|
||||
else:
|
||||
self.data_contents[row][col] = value
|
||||
|
||||
elif role == ModelDataRole: # For inner setting\editing of data
|
||||
self.data_contents[row][col] = value
|
||||
@@ -256,7 +433,7 @@ class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
self.update_model(index, role)
|
||||
return True
|
||||
|
||||
def select_all(self):
|
||||
def select_all(self): # probably NOT thread-safe!
|
||||
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
|
||||
@@ -276,109 +453,23 @@ class CopterDataModel(QtCore.QAbstractTableModel):
|
||||
def add_client(self, client):
|
||||
self.insertRows([client])
|
||||
|
||||
@QtCore.pyqtSlot(int)
|
||||
def remove_client(self, row):
|
||||
@QtCore.pyqtSlot(int) # Probably deprecated now
|
||||
def remove_row(self, row):
|
||||
self.removeRows(row)
|
||||
|
||||
|
||||
def col_check(col):
|
||||
def inner(f):
|
||||
Checks.all_checks[col] = f
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
@col_check(1)
|
||||
def check_ver(item):
|
||||
if not item:
|
||||
return None
|
||||
return True
|
||||
|
||||
@col_check(2)
|
||||
def check_anim(item):
|
||||
if not item:
|
||||
return None
|
||||
return str(item) != 'No animation'
|
||||
|
||||
@col_check(3)
|
||||
def check_bat(item):
|
||||
if not item:
|
||||
return None
|
||||
if item == "NO_INFO":
|
||||
return False
|
||||
else:
|
||||
return float(item.split(' ')[1][:-1]) > Checks.battery_min
|
||||
|
||||
@col_check(4)
|
||||
def check_sys_status(item):
|
||||
if not item:
|
||||
return None
|
||||
return item == "STANDBY"
|
||||
|
||||
@col_check(5)
|
||||
def check_cal_status(item):
|
||||
if not item:
|
||||
return None
|
||||
return item == "OK"
|
||||
|
||||
@col_check(6)
|
||||
def check_mode(item):
|
||||
if not item:
|
||||
return None
|
||||
return (item != "NO_FCU") and not ("CMODE" in item)
|
||||
|
||||
@col_check(7)
|
||||
def check_selfcheck(item):
|
||||
if not item:
|
||||
return None
|
||||
return item == "OK"
|
||||
|
||||
@col_check(8)
|
||||
def check_pos_status(item):
|
||||
if not item:
|
||||
return None
|
||||
str_pos = item.split(' ')
|
||||
return str_pos[0] != 'nan' and str_pos[0] != 'NO_POS'
|
||||
|
||||
@col_check(9)
|
||||
def check_start_pos_status(item):
|
||||
if not item:
|
||||
return None
|
||||
str_start_pos = item.split(' ')
|
||||
return str_start_pos[0] != 'nan' and str_start_pos[0] != 'NO_POS'
|
||||
|
||||
@col_check(10)
|
||||
def check_time_delta(item):
|
||||
if not item:
|
||||
return None
|
||||
return abs(float(item)) < Checks.time_delta_max
|
||||
|
||||
|
||||
def all_checks(copter_item):
|
||||
for col, check in Checks.all_checks.items():
|
||||
if not check(copter_item[col]):
|
||||
return False
|
||||
return True
|
||||
|
||||
def takeoff_checks(copter_item):
|
||||
for col in Checks.takeoff_checklist:
|
||||
if not Checks.all_checks[col](copter_item[col]):
|
||||
return False
|
||||
return True
|
||||
@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 Checks.takeoff_checklist:
|
||||
for col in ModelChecks.takeoff_checklist:
|
||||
if col != 4 or col != 7:
|
||||
if not Checks.all_checks[col](copter_item[col]):
|
||||
return False
|
||||
else:
|
||||
if copter_item[4] != "ACTIVE":
|
||||
if not ModelChecks.checks_dict[col](copter_item[col]):
|
||||
return False
|
||||
elif copter_item[4] != "ACTIVE":
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@@ -387,7 +478,7 @@ def calibrating_check(copter_item):
|
||||
|
||||
|
||||
def calibration_ready_check(copter_item):
|
||||
if not Checks.all_checks[4](copter_item[4]):
|
||||
if not ModelChecks.checks_dict[4](copter_item[4]):
|
||||
return False
|
||||
return not calibrating_check(copter_item)
|
||||
|
||||
@@ -414,7 +505,9 @@ class CopterProxyModel(QtCore.QSortFilterProxyModel):
|
||||
class SignalManager(QtCore.QObject):
|
||||
update_data_signal = QtCore.pyqtSignal(int, int, QtCore.QVariant, QtCore.QVariant)
|
||||
add_client_signal = QtCore.pyqtSignal(object)
|
||||
remove_client_signal = QtCore.pyqtSignal(int)
|
||||
remove_row_signal = QtCore.pyqtSignal(int)
|
||||
remove_client_signal = QtCore.pyqtSignal(object)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user