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:
artem30801
2019-12-05 15:10:21 +03:00
committed by Arthur Golubtsov
parent 53dad0e3fd
commit ce36c6f1e3
9 changed files with 635 additions and 425 deletions

View File

@@ -37,7 +37,6 @@ def wait(end, interrupter=threading.Event(), maxsleep=0.1):
def confirmation_required(text="Are you sure?", label="Confirm operation?"):
def inner(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
reply = QMessageBox.question(
@@ -55,6 +54,7 @@ def confirmation_required(text="Are you sure?", label="Confirm operation?"):
return inner
# noinspection PyArgumentList,PyCallByClass
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
@@ -70,9 +70,9 @@ class MainWindow(QtWidgets.QMainWindow):
self.player = QtMultimedia.QMediaPlayer()
self.init_model()
self.show()
def init_model(self):
# self.model.on_id_changed = self.set_copter_id
@@ -88,7 +88,8 @@ class MainWindow(QtWidgets.QMainWindow):
# Connect signals to manipulate model from threads
self.signals.update_data_signal.connect(self.model.update_item)
self.signals.add_client_signal.connect(self.model.add_client)
self.signals.remove_client_signal.connect(self.model.remove_client)
self.signals.remove_row_signal.connect(self.model.remove_row)
self.signals.remove_client_signal.connect(self.model.remove_row_data)
# Connect model signals to UI
self.model.selected_ready_signal.connect(self.ui.start_button.setEnabled)
@@ -110,20 +111,26 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.action_select_all_rows.triggered.connect(self.model.select_all)
def new_client_connected(self, client: Client):
logging.debug("Added client {}".format(client))
self.signals.add_client_signal.emit(StatedCopterData(copter_id=client.copter_id, client=client))
def client_connection_changed(self, client: Client):
logging.debug("Start remove {}".format(client.copter_id))
logging.debug("Connection {} changed {}".format(client, client.connected), )
row_data = self.model.get_row_by_attr("client", client)
row_num = self.model.get_row_index(row_data)
logging.debug("Removing {}".format(client.copter_id))
if row_num is not None:
if Server().remove_disconnected and (not client.connected):
client.remove()
self.signals.remove_client_signal.emit(row_num)
else:
if row_data is None:
logging.error("No row for client presented")
return
if Server().remove_disconnected and (not client.connected):
client.remove()
self.signals.remove_client_signal.emit(row_data)
logging.debug("Removing from table")
else:
row_num = self.model.get_row_index(row_data)
if row_num is not None:
self.signals.update_data_signal.emit(row_num, 0, client.connected, ModelStateRole)
logging.debug("{} removed".format(client.copter_id))
logging.debug("DATA: connected")
def init_ui(self):
# Connecting
@@ -139,7 +146,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.ui.leds_button.clicked.connect(self.test_leds_selected)
self.ui.takeoff_button.clicked.connect(self.takeoff_selected)
self.ui.flip_button.clicked.connect(self.flip_selected)
self.ui.land_button.clicked.connect(self.land_selected)
self.ui.land_button.clicked.connect(self.land_selected)
self.ui.reboot_fcu.clicked.connect(self.reboot_selected)
self.ui.calibrate_gyro.clicked.connect(self.calibrate_gyro_selected)
@@ -179,43 +186,36 @@ class MainWindow(QtWidgets.QMainWindow):
client = copter_data_row.client
client.get_response("telemetry", self.update_table_data)
@pyqtSlot(str)
def update_table_data(self, message):
fields = message.split('`')
# copter_id git_version animation_id battery_v battery_p system_status calibration_status mode selfcheck current_position start_position copter_time
copter_id = fields[0]
git_version = fields[1]
animation_id = fields[2]
battery_v = fields[3]
battery_p = fields[4]
if battery_v == 'nan' or battery_p == 'nan':
battery_info = "NO_INFO"
else:
battery_info = "{}V {}%".format(battery_v, battery_p)
sys_status = fields[5]
cal_status = fields[6]
mode = fields[7]
selfcheck = fields[8]
current_pos = fields[9]
start_pos = fields[10]
copter_time = fields[11]
time_delta = "{}".format(round(float(copter_time) - time.time(), 3))
row = self.model.get_row_index(self.model.get_row_by_attr('copter_id', copter_id))
self.signals.update_data_signal.emit(row, 1, git_version, ModelDataRole)
self.signals.update_data_signal.emit(row, 2, animation_id, ModelDataRole)
self.signals.update_data_signal.emit(row, 3, battery_info, ModelDataRole)
self.signals.update_data_signal.emit(row, 4, sys_status, ModelDataRole)
self.signals.update_data_signal.emit(row, 5, cal_status, ModelDataRole)
self.signals.update_data_signal.emit(row, 6, mode, ModelDataRole)
self.signals.update_data_signal.emit(row, 7, selfcheck, ModelDataRole)
self.signals.update_data_signal.emit(row, 8, current_pos, ModelDataRole)
self.signals.update_data_signal.emit(row, 9, start_pos, ModelDataRole)
self.signals.update_data_signal.emit(row, 10, time_delta, ModelDataRole)
@pyqtSlot(object, dict)
def update_table_data(self, client, telems: dict):
cols_dict = {
"git_version": 1,
"animation_id": 2,
"battery": 3,
"system_status": 4,
"calibration_status": 5,
"mode": 6,
"selfcheck": 7,
"current_position": 8,
"start_position": 9,
"time": 10,
}
for key, value in telems.items():
col = cols_dict.get(key, None)
if col is None:
logging.error("No column {} present!".format(key))
continue
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.signals.update_data_signal.emit(row_num, col, value, Qt.EditRole)
@pyqtSlot(QtCore.QModelIndex)
def selfcheck_info_dialog(self, index):
col = index.column()
if col == 6:
if col == 7:
data = self.proxy_model.data(index, role=ModelDataRole)
if data and data != "OK":
dialog = QMessageBox()
@@ -226,7 +226,7 @@ class MainWindow(QtWidgets.QMainWindow):
dialog.setDetailedText("\n".join(data))
dialog.exec()
def _selfcheck_shortener(self, data):
def _selfcheck_shortener(self, data): # TODO!!!
shortened = []
for line in data:
if len(line) > 89:
@@ -236,16 +236,11 @@ class MainWindow(QtWidgets.QMainWindow):
@pyqtSlot()
def remove_selected(self):
for copter in self.model.user_selected():
row_num = self.model.get_row_index(copter)
if row_num is not None:
copter.client.remove()
copter.client.remove()
if not Server().remove_disconnected:
self.signals.remove_client_signal.emit(row_num)
logging.info("Client removed from table!")
else:
logging.error("Client is not in table!")
if not Server().remove_disconnected:
self.signals.remove_client_signal.emit(copter)
logging.info("Client removed from table!")
@pyqtSlot()
@confirmation_required("This operation will takeoff selected copters with delay and start animation. Proceed?")
@@ -255,12 +250,12 @@ class MainWindow(QtWidgets.QMainWindow):
logging.info('Wait {} seconds to start animation'.format(dt))
if self.ui.music_checkbox.isChecked():
music_dt = self.ui.music_delay_spin.value()
asyncio.ensure_future(self.play_music_at_time(music_dt+time_now), loop=loop)
asyncio.ensure_future(self.play_music_at_time(music_dt + time_now), loop=loop)
logging.info('Wait {} seconds to play music'.format(music_dt))
# self.selfcheck_selected()
for copter in self.model.user_selected():
if all_checks(copter):
server.send_starttime(copter.client, dt+time_now)
if self.model.checks.all_checks(copter):
server.send_starttime(copter.client, dt + time_now)
@pyqtSlot()
def pause_resume_selected(self):
@@ -290,7 +285,7 @@ class MainWindow(QtWidgets.QMainWindow):
def test_leds_selected(self):
for copter in self.model.user_selected():
copter.client.send_message("led_test")
@pyqtSlot()
def disarm_all(self):
Client.broadcast_message("disarm")
@@ -299,9 +294,9 @@ class MainWindow(QtWidgets.QMainWindow):
@confirmation_required("This operation will takeoff copters immediately. Proceed?")
def takeoff_selected(self, **kwargs):
for copter in self.model.user_selected():
if takeoff_checks(copter):
if self.model.checks.takeoff_checks(copter):
if self.ui.z_checkbox.isChecked():
copter.client.send_message("takeoff_z", {"z":str(self.ui.z_spin.value())})
copter.client.send_message("takeoff_z", {"z": str(self.ui.z_spin.value())}) # todo int
else:
copter.client.send_message("takeoff")
@@ -320,7 +315,7 @@ class MainWindow(QtWidgets.QMainWindow):
@pyqtSlot()
def reboot_selected(self):
for copter in self.model.user_selected():
copter.client.send_message("reboot_fcu")
copter.client.send_message("reboot_fcu")
@pyqtSlot()
def calibrate_gyro_selected(self):
@@ -332,7 +327,7 @@ class MainWindow(QtWidgets.QMainWindow):
data = 'CALIBRATING'
self.signals.update_data_signal.emit(row, col, data, ModelDataRole)
# Send request
client.get_response("calibrate_gyro", self._get_calibration_info, callback_args=(copter_data_row, ))
client.get_response("calibrate_gyro", self._get_calibration_info)
@pyqtSlot()
def calibrate_level_selected(self):
@@ -344,13 +339,15 @@ class MainWindow(QtWidgets.QMainWindow):
data = 'CALIBRATING'
self.signals.update_data_signal.emit(row, col, data, ModelDataRole)
# Send request
client.get_response("calibrate_level", self._get_calibration_info, callback_args=(copter_data_row, ))
client.get_response("calibrate_level", self._get_calibration_info)
def _get_calibration_info(self, value, copter_data_row):
def _get_calibration_info(self, client, value):
col = 5
row = self.model.get_row_index(copter_data_row)
data = str(value)
self.signals.update_data_signal.emit(row, col, data, ModelDataRole)
row_data = self.model.get_row_by_attr("client", client)
row = self.model.get_row_index(row_data)
if row is not None:
data = str(value)
self.signals.update_data_signal.emit(row, col, data, ModelDataRole)
@pyqtSlot()
def send_animations(self):
@@ -379,7 +376,8 @@ class MainWindow(QtWidgets.QMainWindow):
for file, name in zip(files, names):
for copter in self.model.user_selected():
if name == copter.copter_id:
copter.client.send_file(file, "/home/pi/catkin_ws/src/clever/clever/camera_info/calibration.yaml")
copter.client.send_file(file,
"/home/pi/catkin_ws/src/clever/clever/camera_info/calibration.yaml")
else:
logging.info("Filename has no matches with any drone selected")
@@ -402,7 +400,8 @@ class MainWindow(QtWidgets.QMainWindow):
@pyqtSlot()
def send_aruco(self):
path = QFileDialog.getOpenFileName(self, "Select aruco map configuration file", filter="Aruco map files (*.txt)")[0]
path = \
QFileDialog.getOpenFileName(self, "Select aruco map configuration file", filter="Aruco map files (*.txt)")[0]
if path:
filename = os.path.basename(path)
print("Selected file:", path, filename)
@@ -455,7 +454,7 @@ class MainWindow(QtWidgets.QMainWindow):
def restart_clever(self):
for copter in self.model.user_selected():
copter.client.send_message("service_restart", {"name": "clever"})
@pyqtSlot()
def restart_clever_show(self):
for copter in self.model.user_selected():
@@ -464,12 +463,12 @@ class MainWindow(QtWidgets.QMainWindow):
@pyqtSlot()
def update_client_repo(self):
for copter in self.model.user_selected():
copter.client.send_message("update_repo")
copter.client.send_message("update_repo")
@pyqtSlot()
def reboot_all_on_selected(self):
for copter in self.model.user_selected():
copter.client.send_message("reboot_all")
copter.client.send_message("reboot_all")
@pyqtSlot()
def update_start_to_current_position(self):
@@ -501,7 +500,7 @@ class MainWindow(QtWidgets.QMainWindow):
path = QFileDialog.getOpenFileName(self, "Select music file", filter="Music files (*.mp3 *.wav)")[0]
if path:
media = QUrl.fromLocalFile(path)
content = QtMultimedia.QMediaContent(media)
content = QtMultimedia.QMediaContent(media)
self.player.setMedia(content)
self.ui.action_select_music_file.setText(self.ui.action_select_music_file.text() + " (selected)")
@@ -513,9 +512,9 @@ class MainWindow(QtWidgets.QMainWindow):
if self.player.mediaStatus() == QtMultimedia.QMediaPlayer.NoMedia:
logging.info("No media file")
return
if self.player.state() == QtMultimedia.QMediaPlayer.StoppedState or \
self.player.state() == QtMultimedia.QMediaPlayer.PausedState:
self.player.state() == QtMultimedia.QMediaPlayer.PausedState:
self.ui.action_play_music.setText("Pause music")
self.player.play()
else:
@@ -552,16 +551,16 @@ class MainWindow(QtWidgets.QMainWindow):
result = -1
while (result != 0) and (result != 3) and (result != 4):
# light_green_red(min, max)
client_row_mid = int(math.ceil((client_row_max+client_row_min) / 2.0))
client_row_mid = int(math.ceil((client_row_max + client_row_min) / 2.0))
print(client_row_min, client_row_mid, client_row_max)
for row_num in range(client_row_min, client_row_mid):
self.model.data_contents[row_num].client\
self.model.data_contents[row_num].client \
.send_message("led_fill", {"green": 255})
for row_num in range(client_row_mid, client_row_max + 1):
self.model.data_contents[row_num].client \
.send_message("led_fill", {"red": 255})
Dialog = QtWidgets.QDialog()
Dialog = QtWidgets.QDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
@@ -574,7 +573,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.model.data_contents[row_num].client \
.send_message("led_fill")
client_row_max = client_row_mid - 1
elif result == 2:
for row_num in range(client_row_min, client_row_mid):
self.model.data_contents[row_num].client \
@@ -592,18 +591,25 @@ class MainWindow(QtWidgets.QMainWindow):
self.model.data_contents[row_num].client \
.send_message("disarm")
@messaging.message_callback("telem")
def get_telem_data(*args, **kwargs):
message = kwargs.get("message", None)
window.update_table_data(message)
@messaging.message_callback("telemetry")
def get_telem_data(self, **kwargs):
message = kwargs.get("value")
window.update_table_data(self, message)
def except_hook(cls, exception, traceback):
sys.__excepthook__(cls, exception, traceback)
if __name__ == "__main__":
sys.excepthook = except_hook # for debugging (exceptions traceback)
app = QtWidgets.QApplication(sys.argv)
loop = QEventLoop(app)
asyncio.set_event_loop(loop)
#app.exec_()
# app.exec_()
with loop:
window = MainWindow()